• 由客户端内部通讯引发的插件化开发的随想和实践


    背景

    最近在写一个基于Android的IPC实现的一个小工具,主要实现的就是能够在手机查看被监视程序的值的变化和日志等。因为用了入侵的方式,所以需要被监视APK集成一个SDK。程序界面一览:

    大概还是一个半成品的样子,后续会写一些Root以后才有的功能。

    遇到的问题

    在实际的开发中,因为主程序中会包含各个集成SDK的Client端的数据,所以主程序的数据接受到最后UI的呈现,就面临了一个传输的选择。在客户端的开发中,我们解决内部的通信问题一般有三种方式:

    • 基于事件总线(EventBus, PubSubEvent等);
    • 协议化(內建Server,接收方和发送方约定好数据格式);
    • 接口。

    第一种方式极大的提高了我们的开发效率,但是后续带来整体代码的恶化简直是我们维护代码调试代码的灾难。第二种方式內建Server的话,确实很好的解决了我们耦合的问题,但是也带来了一些问题,其一是性能,数据模型的转换在频繁的通信中会带来性能的损耗,其二是开发效率,因为是协议传输,所以一方有变更,另一方也要做相应的变更。那第三种方式接口,因为本身是强引用,所以易于调试和维护,其次也没有数据模型的转换。所以我倾向于用接口去解决数据传输的问题。

    过往的经验

    在过往开发桌面端的经验中,我们大量运用依赖注入的方式来解决模块与模块的耦合问题,同时也可以用来解决模块与仓储之间传输数据的问题。这也是传统的桌面端开发中,插件式开发的经典实现。用之前写过的一个程序举个例子:

    07

    整个工程的结构是这样的:MailAccount作为主程序,MailAccount.InterfaceMailAccount唯一的引用项,MailAccount.Extra.TrialMailAccount.Extra.Standard是完全独立的dll的项目。

    整个程序实现的效果是这样的,当MailAccount的exe文件运行时,如果目录下没有任何其他的dll,则不运行任何内容,只是一个空白页面,但是当目录下有Trial或者Standard任意一个dll时,则运行相应dll中的内容。

    首先看下Trail和Standard的实现:

    namespace MailAccount.Extra.Trial
    {
        [Export("Trial", typeof(IUserAction))]
        public class UserTrial : IUserAction
        {
            public bool DoWork<T>(IEnumerable<T> source)
            {
                if(source.Count() > 5)
                {
                    return false;
                }
                return true;
            }
        }
    }
    
    namespace MailAccount.Extra.Standard
    {
        [Export("Standard", typeof(IUserAction))]
        public class UserStandard : IUserAction
        {
            public bool DoWork<T>(IEnumerable<T> source)
            {
                return true;
            }
        }
    }
    

    Trail和Standard都实现了IUserAction的接口,并对其中功能做了自己的具体实现。

    再看下MailAccount中启动的时候做了什么:

    public class Bootstrapper
        {
            private const string SEARCH_PATTERN = "MailAccount.Extra.*.dll";
    
            protected CompositionContainer MainContainer { get; private set; }
    
            public void Run()
            {
                Container container = this.CreateContainer();
    
                InfrastructureCatalog baseCatalog = this.CreateBaseCatalog();
    
                VarifyAndLoadBaseCatalog(baseCatalog, container);
    
                container.Configure();
    
                this.MainContainer = container.MainContainer;
    
                CreateMainWindow();
            }
    
            private Container CreateContainer()
            {
                return new Container();
            }
    
            private InfrastructureCatalog CreateBaseCatalog()
            {
                InfrastructureCatalog baseCatalog = new InfrastructureCatalog();
                baseCatalog.Add(this.GetType().Assembly);
                DirectoryInfo dirInfo = new DirectoryInfo(@".");
                foreach (FileInfo fileInfo in dirInfo.EnumerateFiles(SEARCH_PATTERN))
                {
                    try
                    {
                        baseCatalog.Add(fileInfo.FullName);
                    }
                    catch(Exception ex)
                    {
                        LogHelper.Error(ex.Message);
                    }
                }
    
    
                return baseCatalog;
            }
    
            private void VarifyAndLoadBaseCatalog(InfrastructureCatalog baseCatalog, Container container)
            {
                if (baseCatalog != null && baseCatalog.Items != null)
                {
                    foreach (AssemblyCatalog catalog in baseCatalog.Items.Distinct())
                    {
                        if (container.AggregateCatalog.Catalogs.FirstOrDefault(c => c.ToString() == catalog.ToString()) == null)
                        {
                            container.AggregateCatalog.Catalogs.Add(catalog);
                        }
                    }
                }
            }
    
            public void CreateMainWindow()
            {
                MainWindow mainWindow = MainContainer.GetExportedValue<MainWindow>("MainWindow");
    
                IUserAction trial = null;
                IUserAction standard = null;
    
                try
                {
                    trial = MainContainer.GetExportedValue<IUserAction>("Trial");
                }
                catch(Exception ex)
                {
                    LogHelper.Error(ex.Message);
                }
    
                try
                {
                    standard = MainContainer.GetExportedValue<IUserAction>("Standard");
                }
                catch (Exception ex)
                {
                    LogHelper.Error(ex.Message);
                }
    
                if (standard != null)
                {
                    mainWindow.userAction = standard;
                }
                else if(trial != null)
                {
                    mainWindow.userAction = trial;
                }
                else
                {
                    MessageBox.Show("程序验证失败");
                    Environment.Exit(0);
                }
    
                Application.Current.MainWindow = mainWindow;
                Application.Current.MainWindow.Show();
            }
        }
    }
    

    在MailAccount启动时,我们初始化一个容器,然后遍历当前目录下的与MailAccount.Extra.*.dll能匹配的dll,在放入到容器中。因为我们在dll中显式的Export并声明了key为Standard,所以容器能够在MainContainer.GetExportedValue<IUserAction>("Standard")的时候找到这个类并初始化。当然我们也可以生成新的符合要求的dll,实现热拔插,当然这是后话。

    Android中实践的前期准备

    因为过往的经验,所以我在检索Android这边信息的时候,是尝试用插件化或者组件化这种字眼来搜索的。但是我却发现了一个有趣的现象,在Android这个圈子里,组件化或者插件化,大家都默认这个技术是用来实现热更新的,并且当我看了各个开源repo的开始文档后,发现总有各种限制或者缺陷,比如在特定手机上无法进行资源转换,不支持Activity(process、configChanges)的部分属性等等。总之真正的工程实践中会面临很多缺陷。

    08

    所以我放弃了这些开源repo,继续走依赖注入框架的方式。在Android的开发中,我们常用的依赖注入的框架其实有两个RoboGuiceDagger。不过真正意义上讲,虽然Dagger2也叫Dagger,但是开发商变了(square->google),实现方式也变了(运行时反射->编译时生成),所以可以理解为我们其实有三个依赖注入的框架可选。在框架的选择上,我最终还是选择了Dagger2,原因有两个,第一个是大厂质量能保证,第二个是我不存在热拔插的需求,运行时编译生成能提高程序的运行效率。

    实践

    选择完依赖注入的框架后,我定义了整个程序的层次结构。每一个框都作为一个独立的Module存在,方便单独的Module的管理。那么我在Plugin这块是怎么实践的呢?假设我们要新增一个plugin,我需要做些什么?正如开始所说的,因为通信方式中,我选择了使用接口,所以我首先要定义一个接口。以出参监视功能为例,我需要定义一个IOutParaPlugin接口:

    public interface IOutParaPlugin extends IPlugin {
        boolean isGathering();
        void setIsGathering(boolean isGathering);
        void registerOutPara(OutPara outPara);
        void setOutPara(OutPara outPara, String value);
        void clientDisconnect(String pkgName);
    }
    

    IPlugin是我们所有插件的接口类,其主要功能是提供功能的名称和功能入口UI:

    public interface IPlugin {
        String getPluginName();
        Fragment getPluginFragment();
        ILBApp getApp();
    }
    

    这个定义的接口放置在我们的Abs的Module中,我们可以新建一个plugin.op的module来实现我们的功能。

    除了实现了基本的Plugin的功能外,我们还要声明一个module的类来供Dagger生成编译时的信息。

    @Module
    public abstract class OutParaModule {
        @Provides
        @Named(AliasName.OUT_PARA_PLUGIN)
        @Singleton
        public static IOutParaPlugin provideOutParaPlugin(ILBApp app,
                                                          @Named(AliasName.OUT_PARA_BRIDGE) Lazy<UIOutParaBridge> outParaBridgeLazy) {
            return new OutParaPlugin(app, outParaBridgeLazy);
        }
    
        @Provides
        @Named(AliasName.OUT_PARA_BRIDGE)
        @Singleton
        public static UIOutParaBridge provideOutParaBridge(ILBApp app,
                                                           @Named(AliasName.CLIENT_MANAGER) IClientManager clientManager,
                                                           @Named(AliasName.OUT_PARA_PLUGIN) Lazy<IOutParaPlugin> outParaPluginLazy) {
            return new UIOutParaBridge(app, clientManager, outParaPluginLazy);
        }
    
        @PreActivity
        @ContributesAndroidInjector
        abstract OutParaDetailActivity outParaDetailActivityInjector();
    
        @PreFragment
        @ContributesAndroidInjector
        abstract OutParaFragment outParaFragmentInjector();
    }
    

    在Module中,我们定义了我们的OutParaPlugin在外部可以被注入,当然我们还给了它一个别名,当有多处注入不同实现IOutParaPlugin类时,我们可以用别名来区分。

    定义完Module以后,我们要在主app中引用:

    @Singleton
    @Component(modules = {
            LBAppModule.class,
            ClientModule.class,
            OutParaModule.class,
            InParaModule.class,
            LogModule.class,
            FloatingModule.class
    })
    interface LBComponent extends AndroidInjector<LBApp> {
        @Component.Builder
        abstract class Builder extends AndroidInjector.Builder<LBApp> {
    
        }
    }
    

    这样,在主app的MainActiviy中我们可以这样引用:

    @Inject @Named(AliasName.OUT_PARA_PLUGIN) IOutParaPlugin outPlugin;
    

    在Activity被onCreate的时候注入并获取OutParaPlugin的实例:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        active = true;
    
        initPlugin(outPlugin, inPlugin, logPlugin);
        initDrawer();
        initFragment();
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestDrawOverLays();
        }
    }
    

    此时,我们获取plugin后,可以用getPluginName()初始化侧滑栏的菜单,当用户点击时,再通过getPluginFragment()进入Plugin内部相关的UI。我们也可以实现新的IOutParaPlugin的module来快速替换现有的plugin。

    总结

    将工程插件化,是约束代码边界的一个很好的实践。将庞大的工程拆分成一个个子工程,从编译上做到隔离,将不同的工程交由不同的人负责,避免了相互之间代码更改,同时提高了代码的可维护性。

    参考信息

    微信Android模块化架构重构实践
    Prism6下的MEF:第一个Hello World
    Android Dagger (2.10/2.11) Butterknife MVP
    Prism PubSub Event

  • 相关阅读:
    web前端开发(4)
    web前端开发(3)
    web前端开发(2)
    【计算机算法设计与分析】——SVM
    【计算机算法与分析】——7.1分枝-限界法
    【模式识别与机器学习】——判别式和产生式模型
    【模式识别与机器学习】——logistic regression
    【模式识别与机器学习】——最小二乘回归
    【模式识别与机器学习】——似然函数
    【计算机算法设计与分析】——6.4图的着色
  • 原文地址:https://www.cnblogs.com/youngytj/p/7455829.html
Copyright © 2020-2023  润新知