{"id":20271905,"url":"https://github.com/north2016/moduler","last_synced_at":"2025-04-11T04:32:39.916Z","repository":{"id":81054463,"uuid":"124362916","full_name":"north2016/Moduler","owner":"north2016","description":"Android Moduler  组件化demo","archived":false,"fork":false,"pushed_at":"2018-03-12T09:20:55.000Z","size":176,"stargazers_count":33,"open_issues_count":1,"forks_count":12,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-25T02:39:40.422Z","etag":null,"topics":["android-component","component","module","moduler"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/north2016.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-08T08:44:57.000Z","updated_at":"2021-08-03T03:23:15.000Z","dependencies_parsed_at":"2023-03-25T13:34:28.206Z","dependency_job_id":null,"html_url":"https://github.com/north2016/Moduler","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/north2016%2FModuler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/north2016%2FModuler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/north2016%2FModuler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/north2016%2FModuler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/north2016","download_url":"https://codeload.github.com/north2016/Moduler/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248345202,"owners_count":21088231,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android-component","component","module","moduler"],"created_at":"2024-11-14T12:39:57.891Z","updated_at":"2025-04-11T04:32:39.868Z","avatar_url":"https://github.com/north2016.png","language":"Java","readme":"关于组件化，相信很多人耳熟能详，网上的组件化框架也如雨后春笋，最近做了一些 [组件化调研](https://www.jianshu.com/p/0d45f2a894ba)，开始着手探索适合自己项目的一条组件化之路，在此分享一下，欢迎指正交流。\n\n###组件化之核心技术点\n\n    在阅读了大部分组件化相关的文章和框架之后，大致能总结出以下几点：\n        1、组件的路由的注册和中央路由的采集\n        2、组件间的通信(跨进程)和打包之后的模块间通信(同进程)，以及其兼容(跨/同进程)\n        3、组件向外提供服务，以及组建间的服务的同异步互调，以及其兼容(跨/同进程)\n\n\n我们的目标以及达到的成果：\n   \n    1、最小代价组件化，最简单的配置、最灵活的切换\n    2、组件路由自动化注册，中央路由自动化采集\n    3、服务自动注册，兼容同异步，兼容跨/同进程\n    4、OkBus实现的通信机制，兼容同异步，兼容跨/同进程，与传统使用方式完全   一样，无感知无差别\n    5、跨组件调用时自动唤醒，单个组件调试时无需手动打开目标组件，即使目标开启后被杀掉进程，同样可以唤醒加通信一步到位\n    6、代码量少的可怜，实在一句多余的代码都不想写的懒人体验\n\n\n\n\n\n###一、组建自动注册和中央路由自动收集APT+SPI\n\n   ####第一步，路由Map自动化注册交给(APT)annotationProcessor\n\n   具体细节无需多言，注解标识目标url＋注解处理器集中处理生成代码,借助javapoet和auto-service，实现自动注册路由到组件Map。\n\n\n注意、这一步，生成的代码类都会被标注上@AutoService，成为路由注册服务的提供者\n\n   ####第二步，中央路由采用SPI自动收集\n\n  Java提供的SPI全名就是Service Provider Interface,下面是一段官方的解释,，其实就是为某个接口寻找服务的机制，有点类似IOC的思想，将装配的控制权移交给ServiceLoader。\n    \nSPI在平时我们用到的会比较少，但是在Android模块开发中就会比较有用，不同的模块可以基于接口编程，每个模块有不同的实现service provider,然后通过SPI机制自动注册到一个配置文件中，就可以实现在程序运行时扫描加载同一接口的不同service provider。这样模块之间不会基于实现类硬编码，可插拔。\n\n\n注上@AutoService的接口实现类，会在META-INF下自动生成接口的服务实现列表\n![META-INF下自动生成接口的服务实现列表](http://upload-images.jianshu.io/upload_images/751860-f56efdf290ae5856.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n在最终打包的Application自动采集子组件的路由器:\n```  \n        ServiceLoader\u003cIRouterRulesCreator\u003e loader =  ServiceLoader.load(IRouterRulesCreator.class);\n        for (IRouterRulesCreator rules : loader) Router.addRouterRule(rules);\n\n```  \n\n在独立运行的组件Application也是如此。\n\n   ####第三步，Messager扩展OkBus实现跨/同进程的无差别操作\n\n在未组件化之前，使用OkBus的APP架构图为：\n\n![未组件化之前OkBus的APP架构图](http://upload-images.jianshu.io/upload_images/751860-099667ed60035ce9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n在同一进程中，使用OKBus在不同模块间传递Message数据。\n\n\n组件化之后的架构图为：\n\n\n![Messager扩展OkBus实现跨进程](http://upload-images.jianshu.io/upload_images/751860-e28bf7ef7d91fbbe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n\n特点：\n\n    1、组件化和非组件化对OkBus来说使用方式完全一样，无感知无差别，旧代码基本不用改\n    2、Messenger 相对于ContentProvider、Socket、AIDL。操作最简单，它是对AIDL的Message传递做了封装，Message可以作为任何序列数据的载体\n    3、只有一个服务器，再多组件整体架构也不冗乱\n    4、自动判断单组件运行和多组件打包状态，\n    5、模块自动化注册，数据自动经过服务器转发，可以到达APP内的组件的任何一环\n\n\n\n实现原理：\n     服务器保存客户端注册的信使，收到消息时，遍历转发\n``` \n //1.根据模块ID保存所有的客户端信使 \nprivate ConcurrentHashMap\u003cInteger, Messenger\u003e mClientMessengers = new ConcurrentHashMap\u003c\u003e();\n\n``` \n``` \n //2.收到消息时转发给其他模块的处理器，来源模块除外 \n                Enumeration keys = mClientMessengers.keys();\n                while (keys.hasMoreElements()) {\n                    int moduleId = (int) keys.nextElement();\n                    Messenger mMessenger = mClientMessengers.get(moduleId);\n                    if (moduleId != msg.arg1) {//不是目标来源模块，进行分发\n                        Message _msg = Message.obtain(msg);\n                        try {\n                            mMessenger.send(_msg);\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n                }\n``` \n\n\n\n\n\n\n   ####第四步，OkBus实现同异步服务互调\n\n服务互调就是OkBus的两个消息，触发服务一个消息，返回结果一个消息\n\n\n异步互调就是两个消息，对应两个回调\n\n``` \n   /**\n     * 服务规则：\n     * 1、服务的请求ID必须是负值(正值表示事件)\n     * 2、服务的请求ID必须是奇数，偶数表示该服务的返回事件，\n     * 即：   requestID－1 ＝ returnID\n     * 例如  -0xa001表示服务请求  -0xa002表示-0xa001的服务返回\n     */\n\n``` \n\n``` \n\n    /**\n     * 注册服务\n     *\n     * @param serviceId 服务id\n     * @param callback  服务调用的回调\n     * @param \u003cT\u003e       服务返回的数据范型\n     */\n    public \u003cT\u003e void registerService(final int serviceId, final CallBack\u003cT\u003e callback) {\n        okBus.unRegister(serviceId);//服务提供者只能有一个\n        okBus.register(serviceId, new Event() {\n            @Override\n            public void call(Message msg) {\n                //TODO 优化到子线程\n                OkBus.getInstance().onEvent(serviceId - 1, callback.onCall(msg));\n            }\n        });\n    }\n\n\n\n/**\n     * 异步调用服务\n     *\n     * @param serviceId 服务id\n     * @param callback  回调\n     */\n    public void fetchService(final int serviceId, final Event callback) {\n        if (serviceId \u003e 0 || serviceId % 2 == 0) {\n            assert false : \"请求ID必须是负奇值!\";\n            return;\n        }\n        //1、先注册回调\n        okBus.register(serviceId - 1, new Event() {\n            @Override\n            public void call(Message msg) {\n                callback.call(msg);\n                okBus.unRegister(serviceId - 1);//服务是单次调用，触发后即取消注册\n            }\n        }, Bus.BG);\n        //2、通知目标模块\n        okBus.onEvent(serviceId);\n    }\n``` \n\n\n两个即时的消息一来一回，就完成了服务的互调，\n服务因为是实时调用，因此调用完之后立马注销回调即可。\n\n\n同步调用则是在异步调用的基础上加了锁：\n\n``` \n/**\n     * 同步调用服务\n     *\n     * @param serviceId 服务ID\n     * @param timeout   超时时间\n     * @return\n     */\n    public synchronized \u003cT\u003e T fetchService(final int serviceId, int timeout) {\n        final CountDownLatch latch = new CountDownLatch(1);\n        final AtomicReference\u003cT\u003e resultRef = new AtomicReference\u003c\u003e();\n        service.execute(new Runnable() {\n            @Override\n            public void run() {\n                fetchService(serviceId, new Event() {\n                    @Override\n                    public void call(Message msg) {\n                        try {\n                            resultRef.set((T) msg.obj);\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        } finally {\n                            latch.countDown();\n                        }\n                    }\n                });\n            }\n        });\n        try {\n            latch.await(timeout, TimeUnit.SECONDS); //最多等待timeout秒\n        } catch (Exception e) { //等待中断\n            e.printStackTrace();\n        }\n        return resultRef.get();\n    }\n``` \n\n\n由于主线程加锁来实现的同步，所以要根据不同组件的ANR触发上限传入timeout\n\n同时，所有数据接收的处理必须放到子线程，否则就是死锁：\n\n``` \n private class WorkThread extends Thread {\n        Handler mHandler;\n\n        @Override\n        public void run() {\n            Looper.prepare();\n            mHandler = new ServiceHandler();\n            mMessenger = new Messenger(mHandler);\n            Looper.loop();\n        }\n\n        public void quit() {\n            mHandler.getLooper().quit();\n        }\n    }\n``` \n\n注意：子线程Handler需要自己Looper.prepare\n\n\n\n\n\n   ####第五步，组件和服务的自动化注册\n\n    原理也是SPI，1、声明一个组件：\n``` \n@AutoService(IModule.class)\npublic class Module extends BaseModule {\n    @Override\n    public void afterConnected() {\n\n    }\n\n    @Override\n    public int getModuleIdId() {\n        return Constants.MODULE_B;\n    }\n}\n``` \n\n\n2、SPI自动注册\n``` \n        //自动注册组件服务\n        ServiceLoader\u003cIModule\u003e modules = ServiceLoader.load(IModule.class);\n        for (IModule module : modules) module.init();\n``` \n   \n####第六步，自动唤醒，按需加载\n\n\n单个组件调试时，自动唤醒服务器，需要调用某个组件服务时，自动唤醒目标组件，服务器和目标组件打不打开，有没有被杀死，都能正常唤醒继续通信\n\n实现方案：\n   1、任意组件打开时，自动唤醒服务器\n\n\n   BaseAppModuleApp里面：\n\n``` \n      Intent intent = new Intent(MessengerService.class.getCanonicalName());// 5.0+ need explicit intent\n      intent.setPackage(Constants.SERVICE_PACKAGE_NAME); // the package name of Remote Service\n      boolean mIsBound = bindService(intent, mBaseModule.mConnection, BIND_AUTO_CREATE);\n        \n``` \n\n\n2、调用目标组件的服务时，自动唤醒目标组件\n``` \n\n        Intent ait = new Intent(NoticeService.class.getCanonicalName()); //唤醒目标进程的服务Action名\n        ait.setPackage(Constants.MODULE_PACKAGE_PRE + module_name);   //唤醒目标进程的包名\n        BaseAppModuleApp.getBaseApplication().bindService(ait, new ServiceConnection() {\n            @Override\n            public void onServiceConnected(ComponentName name, IBinder service) {\n                if (service != null) {\n                    LogUtils.logOnUI(Constants.TAG, \"已经自动唤醒\" + module_name);\n                    Messenger moduleNameMessenger = new Messenger(service);\n                    Message _msg = Message.obtain();\n                    Bundle _data = new Bundle();\n                    _data.putBoolean(Constants.NOTICE_MSG, true);\n                    _msg.setData(_data);\n                    _msg.replyTo = okBus.mServiceMessenger;//把服务器的信使给目标组件的信使，让他俩自己联系，这里仅仅是通知\n                    try {\n                        moduleNameMessenger.send(_msg);\n                    } catch (Exception e) {\n                        e.printStackTrace();\n                    }\n                    //唤醒成功，继续发送异步请求，通知目标模块\n                    okBus.onEvent(serviceId);\n                } else {\n                    LogUtils.logOnUI(Constants.TAG, module_name + \"进程,本来就是醒的\");\n                }\n            }\n\n            @Override\n            public void onServiceDisconnected(ComponentName name) {\n                LogUtils.logOnUI(Constants.TAG, \"自动唤醒目标进程失败 module_name:\" + module_name);\n            }\n        }, BIND_AUTO_CREATE);\n    }\n``` \n####其它\n\n   1、主app壳的处理：\n``` \ndependencies {\n    if (isDebug.toBoolean()) {//调试阶段，只保证基本逻辑不报错\n        implementation project(\":lib\")\n    } else {//打包阶段，才真正的引入业务逻辑模块\n        implementation project(\":module_a\")\n        implementation project(\":module_b\")\n        implementation project(\":module_service\")\n    }\n}\n``` \n\n     开发阶段，模块不稳定，直接引用，避免各种麻烦。\n\n     稍微稳定之后，可以直接使用一个编译好的aar包，减少编译工作量提升编译速度。\n\n     对于完全稳定，基本不会改的模块，直接引用仓库上的内容，在gradle中声明依赖就行了。\n\n2、组件只有当做单独APP运行时才有自己的application\n``` \nsourceSets {\n        main {\n            if (isDebug.toBoolean()) {\n                manifest.srcFile 'src/main/debug/AndroidManifest.xml'//这里面才有application\n            } else {\n                manifest.srcFile 'src/main/AndroidManifest.xml'\n            }\n        }\n    }\n``` \n\n3、全局组件开关 gradle.properties设置isDebug，gradle自动切换\n``` \nif (isDebug.toBoolean()) {\n    apply plugin: 'com.android.application'\n} else {\n    apply plugin: 'com.android.library'\n}\n``` ","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnorth2016%2Fmoduler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnorth2016%2Fmoduler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnorth2016%2Fmoduler/lists"}