当前位置: 首页 > 科技观察

Android组件架构设计从原理到实战

时间:2023-03-13 07:37:53 科技观察

为什么要组件化小项目不需要组件化。当一个项目由几十个人开发时,编译项目需要10分钟。修改一个bug可能会影响到其他业务。小的变化需要回归测试。如果是这种项目,那么我们就需要将它组件化。组件化和模块化首先出现在技术架构演进的过程中,然后才是组件化,因为组件化解决了模块化的问题。模块化架构创建一个Project后,可以创建多个Module,这个Module称为一个模块。一个简单的例子,可能在写代码的时候,我们会拆解主页、新闻、我的模块。每个选项卡中包含的内容是一个模块,可以减少模块中的代码量,但每个模块之间。肯定会有页面跳转,数据传递等,比如模块A需要模块B的数据,那么我们会通过模块A的gradle文件中的实现project(':B')来依赖模块B,但是模块B需要跳转到模块A的某个页面,所以模块B依赖于模块A。这种开发模式还是没有解耦,改一个bug还是会改很多模块,解决不了大型项目的问题。组件架构这里先介绍几个概念。我们日常业务需求所开发的组件称为业务组件。如果业务需求可以普遍复用,则称为业务基础组件,如图片加载、网络请求等框架组件。我们称它们为基础组件。构建所有组件的应用程序组件称为外壳组件/项目。这里先介绍几个概念。针对我们日常业务需求而开发的组件称为业务组件。如果这种业务需求能够被普遍复用,就称为基础业务组件。比如图片加载、网络请求等框架组件,我们称之为基础组件。.构建所有组件的应用程序组件称为外壳组件/项目。接下来看一张架构图:实线表示直接依赖,虚线表示间接依赖。比如shell项目必须依赖业务基础组件、业务组件、module_common公共库。业务组件依赖业务基础组件,但不是直接依赖,而是通过“sink接口”实现间接调用。业务组件之间的依赖也是间接依赖。最后,公共组件依赖于所有需要的基础组件。Common也属于基本组件。它只是统一了基础组件的版本,同时也为应用提供了一些抽象基类,如BaseActivity、BaseFragment,以及基础组件的初始化。组件化带来的优势**加快编译速度:**各业务组件可独立运行调试,速度提升数倍。例如:视频组件单独运行时间为3s,因为此时AS只会运行视频组件的任务以及视频组件所依赖的组件,如果综合编译时间为10s,则任务将执行应用程序引用的所有组件。可以看出效率提高了3倍。**提高协作效率:**每个组件都有专人维护,你不需要关心其他组件是如何实现的,你只需要暴露对方需要的数据即可。测试不需要整个回归,只需要重点测试修改的组件。**功能复用:**一个代码可以到处复用,不再需要复制代码。尤其是基础组件和业务基础组件,调用者基本上可以根据文档一键集成使用。前面说了,非大型项目一般不会组件化,但是就像上面说的功能复用一样,这个优势不仅仅适用于大型项目。我们在写需求或者库的时候完全可以有组件思维,单独写成一个基础组件或者业务基础组件。当第二个项目来的时候,也需要这个组件,所以我们省去拆这个组件的时间(因为写需求的时候很可能会造成很多耦合,后续的拆分需要时间),比如登录组件,共享组件等都可以一开始就写成组件。组件化要解决的问题如何实现业务组件的单独调试?业务组件之间没有依赖关系,如何实现页面跳转?业务组件之间没有依赖关系,如何实现数据通信?如何交付shell项目应用生命周期?独立调试单项目方案所谓单项目方案,就是把所有的组件都放在一个项目下。先看一下整体目录:ps:module_开头表示基础组件,fun_前缀表示基础业务组件,biz_前缀表示业务组件。export_前缀表示业务组件暴露接口。单个项目优劣分析:优点:模块修改后,只需要编译,其他依赖它的模块可以立即感知到变化。缺点:没有实现职责的完全分离,每个模块的开发者都有修改其他模块的权限。首先在gradle.properties文件中声明一个变量://gradle.propertiesisModule=trueisModule如果为true表示该组件可以作为apk运行,false表示该组件只能作为库使用。我们可以根据需要更改这个值,然后同步gradle。然后在一个module的build.gradle文件中使用这个变量在三个地方进行判断://build.gradle//区分是应用还是库if(isModule.toBoolean()){applyplugin:'com。安卓。application'}else{applyplugin:'com.android.library'}android{defaultConfig{//如果是应用,需要指定applicationif(isModule.toBoolean()){applicationId"com.xxx.xxx"}}sourceSets{main{//在应用程序和库之间分离AndroidManifest文件if(isModule.toBoolean()){manifest.srcFile'src/main/debug/AndroidManifest.xml'}else{manifest.srcFile'src/main/AndroidManifest.xml'}}}}由于该库不需要Application和启动Activity页面,所以我们需要区分这个文件。applicationmanifest指定的路径并不具体,随便找个路径创建即可。在应用程序AndroidManifest.xml中我们需要设置启动页::allowBackup="true"android:label="@string/home_app_name"android:supportsRtl="true"android:theme="@style/home_AppTheme">库的AndroidManifest.xml不需要这些:库的AndroidManifest.xml不需要这些:gradle依赖modules主要有两种方式:implementation:A实现B,B实现C,但是A不能访问C里面的东西。api:AapiB,BapiC,A可以访问C中的东西。一般来说,我们只需要使用实现即可。api会导致项目编译时间变长,并且会引入模块不需要的功能,代码之间的耦合会变得严重。而module_common是一个公共库,统一了基础组件的版本。所有的组件都应该需要依赖它,具备基础组件的能力,所以基本上每个业务组件和业务基础组件都应该依赖公共库:dependencies{implementationproject(':module_common')}和普通组件依赖基础组件应该使用api,因为基础组件的能力传递给上层业务组件:dependencies{apiproject(':module_base')apiproject(':module_util')}是一个项目。比如创建项目后,app作为shell组件,依赖biz_home运行,不需要isModule控制独立调试。它是一个项目本身,可以独立调试。多项目的优缺点与单项目相反:优点:职责完全拆分,其他项目复用更方便,直接引入单行依赖。缺点:修改后需要上传到maven仓库,其他项目需要重新编译才能感知到变化,上传和编译需要更多的时间。多项目组件依赖需要使用maven仓库。将各个组件的aar上传到公司内网的maven仓库,然后这样依赖:implementation'com.xxx.xxx:module_common:1.0.0'我们将三方库放到config.gradle中进行管理:ext{dependencies=["glide":"com.github.bumptech.glide:glide:4.12.0","glide-compiler":"com.github.bumptech.glide:compiler:4.12.0","okhttp3“:”com.squareup.okhttp3:okhttp:4.9.0”,“改造”:“com.squareup.retrofit2:retrofit:2.9.0”,“retrofit-converter-gson”:“com.squareup.retrofit2:converter-gson:2.9.0","retrofit-adapter-rxjava2":"com.squareup.retrofit2:adapter-rxjava2:2.9.0","rxjava2":"io.reactivex.rxjava2:rxjava:2.2.21","arouter":"com.alibaba:arouter-api:1.5.1","arouter-compiler":"com.alibaba:arouter-compiler:1.5.1",//我们的库"module_util":"com.sun.module:module_util:1.0.0","module_common":"com.sun.module:module_common:1.0.0","module_base":"com.sun.module:module_base:1.0.0","fun_splash":"com.sun.fun:fun_splash:1.0.0","fun_share":"com.sun.fun:fun_share:1.0.0","export_biz_home":"com.sun.export:export_biz_home:1.0.0","export_biz_me":"com.sun.export:export_biz_me:1.0.0","export_biz_msg":"com.sun.export:export_biz_msg:1.0.0","biz_home":"com.sun.biz:biz_home:1.0.0","biz_me":"com.sun.biz:biz_me:1.0.0","biz_msg":"com.sun.biz:biz_msg:1.0.0"]}喜欢这样方便统一版本管理,然后在根目录的build.gradle中导入:applyfrom:'config.gradle'最后在各自模块中导入依赖,比如在module_common中通过这种方式导入依赖dependencies{apirootProject.ext.dependencies["arouter"]kaptrootProject.ext.dependencies["arouter-compiler"]apirootProject.ext.dependencies["glide"]apirootProject.ext.dependencies["okhttp3"]apirootProject。ext.dependencies["retrofit"]apirootProject.ext.dependencies["retrofit-converter-gson"]apirootProject.ext.dependencies["retrofit-适配器-rxjava2"]apirootProject.ext.dependencies["rxjava2"]apirootProject.ext.dependencies["module_util"]apirootProject.ext.dependencies["module_base"]}我个人认为多项目适合“大”项目,各业务component可能都需要一个团队来开发。淘宝之类的APP,只是业务组件。业务基础组件和基础组件的频率不会有太大的修改。最好将单个项目上传到maven仓库中使用。本文的例子为了方便,将所有的组件都写在了一起。最好的方式是将fun_和module_开头的组件拆分成一个个项目独立开发,将业务组件写到一个项目中。页面跳转组件间隔离完成后,暴露出来最明显的问题就是页面跳转和数据通信的问题。一般来说,页面跳转都是showstartActivity跳转,在组件化的项目中是不适用的。可以使用隐式跳转,但是为每个Activity写intent-filter有点麻烦,所以最好的方式是使用路由框架。其实市面上已经有比较成熟的专门针对组件化设计的路由框架,比如美团的WMRouter,阿里的ARouter等,本例以ARouter框架为例,看看ARouter页面跳转的基本操作。首先,它必须引入依赖关系。以module_common引入ARouter为例,build.gradle中应添加:android{defaultConfig{javaCompileOptions{annotationProcessorOptions{arguments=[AROUTER_MODULE_NAME:project.getName()]}}}compileOptions{sourceCompatibilityJavaVersion.VERib_Compileget_1_8tilCompatibilityJavaVersion.VERib_Compileget_1_8til}}dependencies{apirootProject.ext.dependencies["arouter"]kaptrootProject.ext.dependencies["arouter-compiler"]}没有办法传递kapt注解依赖,所以我们不可避免地需要在每个模块中声明这些配置,除了apirootProject.ext.dependencies["arouter"]行。然后需要全局注册ARouter,我注册的是module_common。classAppCommon:BaseApp{overridefunonCreate(application:Application){MLog.d(TAG,"BaseAppAppCommoninit")initARouter(application)}privatefuninitARouter(application:Application){if(BuildConfig.DEBUG){ARouter.openLog()ARouter.openDebug()}ARouter.init(application)}}然后我们在module_common模块中声明一个路由表作为统一管理路径。//RouterPath.ktclassRouterPath{constvalAPP_MAIN="/app/MainActivity"constvalHOME_FRAGMENT="/home/HomeFragment"constvalMSG_FRAGMENT="/msg/MsgFragment"constvalME_FRAGMENT="/me/MeFragment"constvalMSG_PROVIDER="/msg/MsgProviderImpl"}}然后在MainActivity类文件中注解:@Route(path=RouterPath.APP_MAIN)classMainActivity:AppCompatActivity(){}任何模块只需要调用ARouter.getInstance().build(RouterPath.APP_MAIN).navigation()可以实现跳转。如果我们要添加数据传输也很方便:ARouter.getInstance().build(RouterPath.APP_MAIN).withString("key","value").withObject("key1",obj).navigation()和然后在MainActivity中使用依赖注入接受数据:classMainActivity:AppCompatActivity(){@AutowiredStringkey=""}Arouterscheme在export_biz_msg组件下声明了IMsgProvider,该接口必须实现IProvider接口:interfaceIMsgProvider:IProvider{funonCountFromHome(count:Int=1)}然后在biz_msg组件中实现这个接口:@Route(path=RouterPath.MSG_PROVIDER)classMsgProviderImpl:IMsgProvider{overridefunonCountFromHome(count:Int){//这里只是分发数据,和有监控计数的对象会收到ToMsgCount.instance.addCount(count)}overridefuninit(context:Context?){//对象初始化时调用}}在biz_home首页组件发送计数:valprovider=ARouter.getInstance().build(RouterP啊。MSG_PROVIDER).navigation()asIMsgProviderprovider.onCountFromHome(count)可以看到和页面跳转的方式基本一致,包括Fragment实例的获取方式。ARouter通过一个API实现了所有通信方式,用户上手非常简单。应用程序生命周期分布当appshell项目开始应用程序初始化时,需要通知其他组件初始化一些功能。这是一个简单的方法。首先我们在module_common公共库中声明一个接口BaseApp:interfaceBaseApp{funonCreate(application:Application)}然后每个组件都要创建一个App类来实现这个接口,比如biz_home组件:classHomeApp:BaseApp{overridefunonCreate(application:Application){//初始化放在这里MLog.d(TAG,"BaseAppHomeAppinit")}}最后一步是从appshell项目分发应用生命周期。这里使用了反射技术:valmoduleInitArr=arrayOf("com.sun.module_common.AppCommon","com.sun.biz_home.HomeApp","com.sun.biz_msg.MsgApp","com.sun.biz_me.MeApp")classApp:Application(){overridefunonCreate(){super.onCreate()initModuleApp(this)}privatefuninitModuleApp(application:Application){try{for(appNameinmoduleInitArr){valclazz=Class.forName(appName)valmodule=clazz.getConstructor().newInstance()作为BaseAppmodule.onCreate(application)}}catch(e:Exception){e.printStackTrace()我们只需要知道每个实现了BaseApp接口的类的全限定名写入moduleInitArr数组,然后通过反射获取Class对象得到创建实体对象的构造函数,最后调用onCreateBaseApp的方法传入应用,每个Application生命周期方法都可以这样传入