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

仔细研究一下Android开发代码集中带来的问题

时间:2023-03-12 18:39:02 科技观察

将一个大项目拆分成多个模块或者新开的组件项目,期望的期望是这些模块之间有层级关系。这样业务就可以相对集中,大家可以专注做一件事。同时,代码的耦合度也会相应降低,达到高度解耦,因为同级模块之间没有依赖关系,在编译时是隔离的,这会让组件之间的依赖关系非常清晰,具有更高的解耦性。组件的可重用性强调重用,模块强调职责的划分。他们没有很严格的划分。一个满足可复用性要求的模块,那么这个模块就是一个组件。每个组件的可替换、可热插拔、独立编译将成为可能。Android组件化中代码集中化的问题说明Android组件化是非常简单可行的。AS提供的模块创建方式,通过添加gradle.properies自定义属性,或者ext全局可配置项目属性,或者kotlindsl中kotlin的语法糖,为我们提供了application和library之间的切换。然后把代码放在不同的仓库位置,最好是单独的git仓库级管理隔离,实现我们要解决的一系列问题。然而,事情并没有想象的那么简单……一系列问题接踵而至。对我来说,影响最大的是在应用程序设计中使用映射数据库,导致集成模式和组件模式的复用问题;finally使用注解Java特性的代码生成,虽然不完美,但还是解决了这个问题。随即,一个重要而紧迫的问题出现了,代码中心化的问题。这个问题是怎么产生的?在微信Android模块化架构重构实践中有介绍。然而,随着代码不断膨胀,一些问题开始出现。第一个问题是基础工程libnetscene和libplugin。基础项目一直处于不断扩张的状态,而主体项目也在不断做大。同时,基础工程存在中心化问题。很多业务存储类都依附于一个核心类,久而久之这个类就变得不可读了。另外,为了顺利切换到gradle,避免结构改动太多,模块太多,我们把所有的项目都连接到一个模块上。在没有编译隔离的情况下,模块之间的代码边界有所恶化。虽然开发了工具来限制紧随其后的模块之间的错误依赖关系,但这段时间的影响已经显现。在以上种种问题之下,很多模块已经不能再称得上“独立”了。所以当我们重新审视代码架构时,之前良好的模块化架构设计已经逐渐发生了变化。看他们分析问题的原因:看基础项目的代码,我们可以看到除了符合设计初衷的存储、网络等配套组件外,还有相当多的业务相关的代码。这些代码是膨胀的根源。但是代码是从哪里来的呢?你必须把它放在这里吗?一切不合理的事情背后都有一个逻辑。在之前的架构中,我们广泛使用Event事件总线作为模块间的通信方式,基本上是唯一的方式。当使用Event作为通信媒介时,自然要有一个地方来定义它,这样所有的模块都可以知道Event结构是什么。这时,基础项目似乎是存储Events的唯一选择——Event定义放在基础项目中;那么,当某个模块A要使用模块B的数据结构类时,怎么办呢?下沉班级到基础项目Engineering;当模块A要使用模块B的接口返回数据时,Event似乎不合适?然后把代码下沉到基础工程中……于是越来越多的代码“自然而然”下沉到基础工程中。再来看主项目,其扩容的原因各不相同。经过分析,可以确认的是,首先,作为主业,还有待发展,扩张在所难免。缺乏适当的内部改造暂时不是问题的核心。另一部分原因是模块的生命周期设计似乎已经不能满足使用的需要。之前的模块生命周期是从“账户初始化”到“账户已注销”,由此可见一定有超越这个时序的逻辑。在过去,这不是什么大问题。在“账户初始化”开始之前,需要执行的逻辑非常多。但是现在不一样了,再简单的逻辑堆起来也会变得复杂。这时候模块生命周期之外的逻辑基本上只能放在主工程中。其他的问题,比如模块边界破坏、基础工程集中化,都是代码不断恶化的帮凶……看完之后,我陷入了沉思。这个问题不就是我们面临的问题吗?不仅是组件化,在很多形成依赖的场景中都存在这样的问题。假设有用户组件和共享组件,共享组件需要用户组件提供数据。具体如何体现?看一组图:解决方案是共享组件依赖用户组件,可以解决问题。假设有一个组件A需要引用共享组件,那么它必须依赖共享组件和用户组件。这一下子打破了组件编译隔离的愿景,组件化也就失去了味道。用户组件中的公共数据部分被淹没到基础组件中,共享组件依赖于基础组件提供数据。但是,当很多组件需要相互提供数据时,就会出现中心化的问题,只需要共享组件的B组件,就不得不依赖基础组件引入其他数据。也造成了代码中心化的下沉,失去了组件化的意义。/如何解决代码中心化问题/微信在面对这个痛彻心扉的问题时,表达了“有病不治怕死”的感慨,但也发布了一个非常强大的操作-.api。这个操作很高级,方法很腾讯,但是这个文档只提了本质,没有具体的操作步骤,对我们来说还是有挑战的。代码中心化问题的.api解决方案是什么?我们先来看看具体的操作过程。在上面的图3中,我们使用一些技术将用户组件中需要共享数据的部分抽象成一个接口,使用AS配置文件类型将(kotlin)改回.api,然后创建一个模块相同的包名-api组件用于使其他组件依赖于它。共享组件和其他组件以及自身的组件都以模块的方式依赖于这个组件,使得需要共享的数据可以完美的单独使用。SPI实现(来自上面的文档)大致意思是我们可以先将要共享的数据抽象成接口,形成一个标准的服务接口,然后在具体的实现中在相应的block中实现接口。当服务提供者提供了接??口的具体实现后,在jar包的META-INF/services目录下创建名为“接口完全限定名”的文件,内容为实现类的完全限定名;然后使用ServiceLoader加载配置文件中指定的实现,此时我们在不同的组件之间通过ServiceLoader加载需要的文件。使用ARouter使用ARouter组件间传输数据的方式+gralde自动生成module-api组件,形成中心化问题的.api。假设我们满足上述所有关系并正确构建它们,那么我们如何处理组件之间的通信呢?Arouter阿里通信路由@Route(path="/test/activity")publicclassYourActivityextendActivity{...}跳转:ARouter.getInstance().build("/test/activity").withLong("key1",666L).navigation()//声明接口,其他组件通过接口调用服务publicinterfaceHelloServiceextendsIProvider{StringsayHello(Stringname);}//实现接口@Route(path="/yourservicegroupname/hello",name="TestService")publicclassHelloServiceImplimplementsHelloService{@OverridepublicStringsayHello(Stringname){return"hello,"+name;}@Overridepublicvoidinit(Contextcontext){}}//测试publicclassTest{@AutowiredHelloServicehelloService;@Autowired(name="/yourservicegroupname/hello")HelloServicehelloService2;HelloServicehelloService3;HelloServicehelloService4;publicTest(){ARouter.getInstance().inject(this);}publicvoidtestService(){//1。(推荐)使用依赖注入通过注解字段即可使用发现服务,无需主动获取//Autowired注解中标注名称后,会使用byName方法将对应字段注入对应字段。如果不设置name属性,默认会使用byType方法来发现服务(当同一个接口有多个实现时,必须使用byName的方法来发现服务)helloService.sayHello("Vergil");helloService2.sayHello("维吉尔");//2.使用依赖搜索的方法发现服务,主动发现服务并使用,下面两个方法分别是byName和byTypehelloService3=ARouter.getInstance().navigation(HelloService.class);helloService4=(HelloService)ARouter。getInstance().build("/yourservicegroupname/hello").navigation();helloService3.sayHello("Vergil");helloService4.sayHello("Vergil");}}如果需要使用用户组件的用户信息通过支付组件,我们如何处理它?ARouter可以通过上面的IProvider注入服务进行通信,或者使用EventBus的方式dataclassUserInfo(valuid:Int,valname:String)/***@authorkpa*@date2021/7/212:15pm*@emailbillkp@yeah.net*@description用户登录、信息获取等*/interfaceIAccountService:IProvider{//获取账户信息并提供信息*fungetUserEntity():UserInfo?}//注入服务@Route(path="/user/user-service")classUserServiceImpl:IAccountService{//...}支付组件中IAccountServiceaccountService=ARouter.getInstance().navigation(IAccountService.class);UserInfobean=accountService.getUserEntity();问题就暴露在我们眼前。支付组件中的IAccountService和UserInfo从哪里来?这就是module-api需要解决的问题。方面:将需要共享的类文件和初始化数据设计成.api文件。打开AS->Prefernces->FileTypes,找到kotlin(Java),在Filenamepatterns中选择Add".api"(注意,如果你对这个后缀满意,两者都可以设置为.kpa)例如:dataclassUserInfo(valuserName:String,valud:Int)interfaceUserService{fungetUserInfo():UserInfo}生成一个包含共享数据和初始化数据类文件的module-api组件这一步有以下实现方法。自己手动创建一个module-api组件。显然这是不可取的,但却是可行的。使用shell、python等脚本语言扫描指定路径生成对应的module-api。使用Android编译环境和语言groovy编写gradle脚本。优点是不用考虑什么时候编译,不破坏编译环境,写起来很简单github大牛提供的脚本,完全符合我们的预期。definecludeWithApi(StringmoduleName){defpackageName="com/xxx/xxx"//首先正常加载这个模块include(moduleName)//找到这个模块的路径StringoriginDir=project(moduleName).projectDir//这是新的路径StringtargetDir="${originDir}-api"//原模块的名称StringoriginName=project(moduleName).name//新模块的名称defsdkName="${originName}-api"//这里是位置publicmodule,我提前放了一个新的api.gradle文件进去StringapiGradle=project(":apilibrary").projectDir//每次编译删除之前的文件deleteDir(targetDir)//将.api文件复制到新的pathcopy(){fromoriginDirintotargetDirexclude'**/build/'exclude'**/res/'include'**/*.api'}//直接将publicmodule的AndroidManifest文件复制到新路径作为模块文件copy(){from"${apiGradle}/src/main/AndroidManifest.xml"into"${targetDir}/src/main/"}//复制gradle文件到新路径作为模块的gradlecopy(){from"${apiGradle}/api.gradle"into"${targetDir}/"}//删除空文件夹deleteEmptyDir(*new*File(targetDir))//todo替换成自己的包名//新建AndroidManifest路径,路径在原路径下package在AndroidManifest中新建一个api包作为包名StringpackagePath="${targetDir}/src/main/java/"+packageName+"${originName}/api"//todo替换成自己的包名,这里是apilibrary模块复制的AndroidManifest,替换里面的包名//修改AndroidManifest文件包路径fileReader("${targetDir}/src/main/AndroidManifest.xml","commonlibrary","${originName}.api")newFile(packagePath).mkdirs()//重命名gradledefbuild=new*File(targetDir+"/api.gradle")if(build.exists()){build.renameTo(newFile(targetDir+"/build.gradle"))}//重命名.api文件生成普通的.java文件renameApiFiles(targetDir,'.api','.java')//正常加载新模块包括":$sdkName"}privatevoiddeleteEmptyDir(Filedir){if(dir.isDirectory()){File[]fs=dir.listFiles()if(fs!=null&&fs.长度>0){for(inti=0;ifile.delete()}}/***renameapifiles(java,kotlin...)**/privatedefrenameApiFiles(root_dir,Stringsuffix,Stringreplace){FileTree*files=fileTree(root_dir).include("**/*$suffix")files.each{Filefile->file.renameTo(*new*File(file.absolutePath.replace(suffix,replace)))}}//替换AndroidManifest里的文字部分*deffileReader(path,name,sdkName){defreaderString=""defhasReplace=falsefile(path).withReader('UTF-8'){reader->reader.eachLine{if(it.find(name)){it=it.replace(name,sdkName)hasReplace=true}readerString<<=itreaderString<<'\n'}if(hasReplace){file(path).withWriter('UTF-8'){within->within.append(readerString)}}returnreaderString}}使用includeWithApi":user"Democomponent-api地址为:https://github.com/kongxiaoan/component-api