|
一种按照library的维度进行Android包大小分析的方法和实践
一种按照library的维度进行Android包大小分析的方法和实践
刘宇@贝壳找房
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年03月06日 00:20
1 背景最近贝壳APP在做瘦身,需要对包体大小进行分析,但刚开始Android只能分析出apk中包含哪些文件,并不知道文件来源于哪个library,更不知道library对应的维护组,那么APK瘦身项目在实施时就没有一个明确的责任划分。经过调研发现Android业内还没有一个包体按照library维度进行分析的方案,比较有名的是腾讯开源的matrix,它也只能做到按照文件类型进行包体分析,并不能到按照library的维度进行分析整个包体。2 apk文件结构做包体分析之前,我们先了解一下apk文件的结构,其实就是一个压缩文件,大概结构如下:assert目录:存放我们app/src/main/assets目录下的资源文件,另外flutter的资源文件也会放在该目录下res目录:存放我们项目的资源文件,例如:图片,xml布局,values.xml和音频等资源lib目录:存放我们项目中所有的so文件.dex:所有的java代码先会通过javac命令编译成.class文件,然后通过dx工具转成dex文件resources.arsc:资源映射表,通过AndroidStudio我们可以看到apk文件中资源文件类型,id,名称和资源所在目录其它:主要是一些java库下的META-INFO目录下的文件。我们都知道Android打包的过程中,会把Android工程和它引用的library的代码和资源进行校验和merge,如果发生冲突那么将会打包失败,如果打包成功的话将会生成我们的apk文件,那么在这个打包过程中会不会存在一些中间文件,来记录工程中library和它包含的文件之间的映射关系呢?通过查看所有app/build/目录下的所有文件,发现确实存在这个中间文件,记录着library下有有哪些res资源,asset资源,so文件,META-INFO文件。那么在打包过程中我们可以收集这些中间文件,并上传到我们的maven;打包完成之后触发我们的分析job,先拿到apk文件中的所有文件,然后解析收集上来的这些中间文件,这样就可以实现按照library的维度来进行包大小分析了。3 解析中间文件3.1 中间文件位置这些文件全部存放在app/build/intermediates/incremental目录中:文件名称位置res资源合并的映射文件app/build/intermediates/incremental/mergeDebugResources/merger.xmlassertassert资源合并映射表app/build/intermediates/incremental/mergeDebugAssets/merger.xmlso文件合并映射表app/build/intermediates/incremental/a_plusDebug-mergeNativeLibs/merge-statejava资源合并映射表app/build/intermediates/incremental/debug-mergeJavaRes/merge-state3.2 收集中间文件如果文件路径写死,那会存在兼容性问题,通过gradle api获取的这些文件路径的话,那么就不存在兼容性问题了。defextension_merge_state=project.extensions.getByName("android")extension_merge_state.applicationVariants.all{variant->defvariant_name=variant.name//1、res资源合并的映射文件:app/build/intermediates/incremental/mergeDebugResources/merger.xmldefmergeResourcesTask=variant.getMergeResources()mergeResourcesTask.doLast{defmergeResFile=mergeResourcesTask.incrementalFolder.absolutePath+"/merger.xml"println(mergeResFile)appendFilePath("mergeResources.xml",mergeResFile)}//2、assert资源合并映射表:app/build/intermediates/incremental/mergeDebugAssets/merger.xmldefmergeAssetsTask=variant.getMergeAssets()mergeAssetsTask.doLast{defassertMergerFile=mergeAssetsTask.incrementalFolder.absolutePath+"/merger.xml"println(assertMergerFile)appendFilePath("mergeAssets.xml",assertMergerFile)}//3、so文件合并映射表,通过序列化IncrementalFileMergerState实现:app/build/intermediates/incremental/a_plusDebug-mergeNativeLibs/merge-statedefcontainer=variant.variantData.taskManager.taskFactory.taskContainerTaskmergeNativeLibsTask=container.getByName("merge${variant_name.capitalize()}NativeLibs")mergeNativeLibsTask.doLast{defcache_merge_state=mergeNativeLibsTask.cacheDir.getParent()+"/merge-state"println(cache_merge_state)appendFilePath("mergeNativeLibs_merge_state",cache_merge_state)}//4、java资源合并映射表,通过序列化IncrementalFileMergerState实现app/build/intermediates/incremental/debug-mergeJavaRes/merge-stateTaskmergeJavaResourceTask=container.getByName("merge${variant_name.capitalize()}JavaResource")mergeJavaResourceTask.doLast{defcache_merge_state=mergeJavaResourceTask.cacheDir.getParent()+"/merge-state"println(cache_merge_state)appendFilePath("mergeJavaRes_merge_state",cache_merge_state)}}defvoidappendFilePath(file_key,file_path){Stringinput_dir=project.buildDir.toPath().toString()+"/merge_state"Filedir=newFile(input_dir)if(!dir.exists()){dir.mkdir()}definputFile=newFile(dir.absolutePath,"merge_files.txt")if(!inputFile.exists()){inputFile.createNewFile()}inputFile.append("${file_key}{file_path}\n")}在打包过程中,我会给app/build.gradle文件注入上面gradle脚本,这样做的好处是省去app接入的成本。知道这些文件的路径之后,我会通过python打包脚本上传这些文件到maven中。3.3 中间文件解析其中merger.xml文件比较好解析,但这个merge-state是个什么文件,通过在命令行中执行 【file merge-state文件路径】命令,发现这个文件是一个持久化java序列化对象产生的文件。那这个文件到底是哪个对象序列化生成的呢,通过用AndroidStudio打开merge-state文件,可以猜到是com.android.builder.merge.IncrementalFileMergerState序列化生成的。通过在gradle plugin项目中解析发现,这确实是com.android.builder.merge.IncrementalFileMergerState这个类序列化生成的文件。那么问题来了,分析是在python环境中进行的,那么我们怎么去解析这个merge-state文件呢?通过了解腾讯matrix的分析包体的方案,我们可以把解析的代码打成一个jar包,然后通过在python中执行调用这个jar的命令就可以完成解析了。解析merge_state的代码如下:publicclassMergeStateParser{publicstaticvoidmain(String[]args){parseObject(args[0],args[1]);}publicstaticvoidparseObject(StringmergeStatePath,StringoutputJsonPath){ObjectInputStreamois=null;try{ois=newObjectInputStream(newFileInputStream(mergeStatePath));IncrementalFileMergerStatemerge_state=(IncrementalFileMergerState)ois.readObject();System.out.println(merge_state);Stringjson=newGson().toJson(merge_state);FileWriterfw=null;try{fw=newFileWriter(outputJsonPath);fw.write(json);fw.flush();fw.close();}catch(IOExceptione){e.printStackTrace();}}catch(Exceptionex){System.out.println(ex.getMessage());}finally{try{if(ois!=null){ois.close();}}catch(IOExceptione){e.printStackTrace();}}}}解析的时候还要注意,com.android.builder.merge.IncrementalFileMergerState这个类是在第三方库中的,所以需要添加以下依赖才能打出jar包。implementation"com.google.guava:guava:27.0.1-jre"3.4 中间文件内容merger.xml:dataSet中 config属性的value值为library的信息,每个source下的file文件就是library中的文件。当然这里解析的时候,我们需要过滤一些特殊的字符,才能拿到library的group_id,artifact_id和version。...下面是res资源文件解析后的结果如下:合并res资源的过程中需要注意:所有library和工程中的values文件夹下的字符串,color这些属性,最终合并成一个文件,这里没有分类,责任划分的时候最终会算到APP维护组下名下。同样所有aar库和工程中的AndroidManifest.xml在打包过程中也会进行合并merge_state:解析出来的class实例,包含三个字段、两个map和一个List。/***Namesofallinputstomerge,inorder.*/privatefinalImmutableListinputNames;/***MapsOS-independentpathstothenamesoftheinputsetsthatwereusedtoconstructthe*mergedoutput.*/privatefinalImmutableMap>origin;privatefinalImmutableMap>byInput;对我们有用的是byInput字段,结构如下:library产物路径对应着多个文件:我们在KeOnes(贝壳自研的持续集成系统)中会注册这些library的信息,通过merge.xml可以拿到library的group_id和artifact_id,通过调用api接口,就可以解析出library在数据库中的名称。但是通过解析merge_state文件,我们可以拿到jar和aar在gradle缓存目录的路径,例如:~/gradle/gradle-4.1/caches/transforms-2/files-2.1/cd6b3a2f4da4a2ecf7cedbc4998ac5b2/jetified-lib_castscreen-1.1.1/jars/classes.jar~/gradle/gradle-4.1/caches/modules-2/files-2.1/com.ke.crashly/collector/1.6.5/9176c716a002a64fe90bc9787272228b362a5d4a/collector-1.6.5.jar备注:带有jetified这种一般是aar中class.jar的路径,如果依赖对应的产物是jar包的话,那么就没有jetified。解析这个路径只能拿到artifact_id和library版本号,通过artifact_id请求KeOnes接口便可获取到library在数据库中的组件名称。4 library与维护组对应关系维护上面讲到通过解析gradle构建过程中生成的中间文件,可以拿到文件和library的对应关系,但这还不够,如果不知道library是谁维护的,那整个包体分析将没多大价值,为了解决这个问题,我们在KeOnes中维护了library和library对应的关系,维护界面如下:library名称:library名称一般为group_id:artifact_namedep_name:group_id:artifact_name:{version}@[aar/jar]针对维护组与library对应关系的维护,我们遵循一个规则:一般情况下,谁维护的library维护组就是谁,对于一些第三方库那么谁引用的就算谁,系统和公共的库属于架构组。5 该方案在包体分析功能的落地5.1 业务线包体大小占比前面我们拿到了res资源、assert资源、so文件和META-INF文件与library对应的关系,那么就好办了,我们可以通过拿到apk下所有的文件,根据这个对应关系就可以分析出在apk文件中library有哪些文件了;同时在KeOnes我们会记录library和维护组之间的关系,这样我们就知道每个维护组包体大小的占比。目前KeOnes系统已经上线了包体分析功能,效果如下所示:5.2 大文件分析apk中文件总共有res资源、assert资源、so文件和其它四个大类,每个大类下面我们会按照文件的后缀进行分类,并且会跟业务的同学沟通并设置一个合理的阈值。目前在KeOnes的分析效果如下:6 踩过的坑通过解析merge-state文件只能分析出library的artifact_id和version,而不能获取到group_id,那么可能会造成在KeOnes中找出来的library名称会存在多个。解决办法:构建时收集项目所有runtime依赖,然后再通过artifact_id和version进行匹配,最终找到group_id,这样从KeOnes获取到的library名称就唯一了。针对放在工程lib目录下的jar和aar,是没有group_id和artifact_id的。解决办法:针对这种情况,建议把这些依赖库上传到maven,手动地给这些library设定一个group_id和artifact_id,同时也要在KeOnes注册这些library,这样就工程所有的library都管理起来了。dex文件不能按照library维度继续进行包体的分析了,但我们可以分析出每个library下有多少个类,多少个方法。项目构建统一是在gradle5.4.1进行的,经过测试gradle6.0+也没有问题,但一些低版本的gradle构建的时候,可能会没有这些中间文件。7 总结要做到按照library的维度去分析包体,关键在于发现并解析资源和代码合并过程中产生的中间文件。同时也需要注册项目中library和维护组的关系,贝壳B、C两端总注册了400多个library,注册这块工作量还是不小的。有了library和维护组的对应关系之后,后面我们对外还可以提供一个精准的分流服务了,应用场景将覆盖crash精准分流、SNAPSHOT依赖检测、权限检测等。
预览时标签不可点
移动端37大前端69移动端 · 目录#移动端上一篇iOS14 动态配置Widget开发与实践下一篇Android中WebView栏风格在新装修业务场景下的实践关闭更多小程序广告搜索「undefined」网络结果
|
|