Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包 一.组件化的静态变量R.java的生成各个module会生成aar文件并且被引用到Application module中最终合并为apk文件。当各个次级module在Application module中被解压后在编译时资源R.java会被重新解压到build/generated/source/r/debug(release)/包名/R.java中。当每个组件中的aar文件汇总到App module中时也就是编译的初期解析资源阶段其每个module的R.java释放的同时会检测到全部的R.java文件然后通过合并最后合并成唯一的一份R.java资源。R2.java及ButterKnife:ButterKnife是一个专注于Android View的注入框架可以大量的减少findViewById和setOnClickListener操作的第三方库。注解中只能使用常量如不是常量会提示attribute value must be contant的错误。可以在使用替代方法原理是将R.java文件复制一份命名为R2.java。然后给R2.java变量加上final修饰符在相关的地方直接引用R2资源。如项目中已经使用ButterKnife维护迭代了一段时间那么使用R2.java的方案适配成本是最低的。最好的解决方式还是使用findViewById不使用注解生成的机制。下面可以使用泛型来封装findViewById以减少编写的代码量Override protected void onCreate(androidx.annotation.Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView textView generateFindViewById(R.id.rl_full_view); } protected T extends View T generateFindViewById(int id) { //return 返回view时加上泛型T return (T)findViewById(id); }二.资源冲突在组件化中Base module和功能module的根本是Library module编译时会依次通过依赖规则进行编译最底层的Base module会被先编译成aar文件然后上一层编译时因为通过compile依赖也会将依赖的aar文件解压到模块的build中。AndroidMainfest冲突问题AndroidMainfest中引用了application的app:name属性当出现冲突时需要使用tool:replace android:name来声明application是可被替代的。某些AndroidMainfest.xml中的属性被替代的问题可以使用tool:replace来解决冲突。包冲突如想使用优先级低的依赖可以使用exclude排除依赖的方式。compile() { exclude group: }资源名冲突在多个module开发中无法保证多个module中全部资源的命名是不同的。假如出现相同的情况就可能造成资源引用错误的问题。一般是后后编译的模块会覆盖之前编译的模块的资源字段中的内容。解决方法一种是当资源出现冲突时使用重命名的方式解决。这就要要求我们在一开始命名的时候不同的模块间的资源命名都不一样这是代码编写规范的约束另一种时Gradle的命名提示机制使用字段android { resourcePrefix 组件名_ }所有的资源名必须以指定的字符串作为前缀否者会报错resourcePrefix这个值只能限定xml中资源并不能限定图片资源所有图片资源仍然需要手动去修改资源名。三.组件化混淆混淆基础混淆包括了代码压缩/代码混淆及资源压缩等优化过程。Android Studio使用ProGuard进行混淆ProGuard是一个压缩/优化和混淆Java字节码文件的工具可以删除无用的类/字段/方法和属性还可以删除无用的注释最大限度地优化字节码文件。它还可以使用简短并无意义的名称来重命名已经存在的类/字段/方法和属性。混淆的流程针对Android项目将其主项目及依赖库未被使用的类/类成员/方法/属性移除有助于规避64k方法的瓶颈同时将类/类成员/方法重命名为无意义的简短名称增加了逆向工程的难度。混淆会删除项目无用的资源有效减少apk安装包的大小。混淆有Shrinking(压缩)/Optimiztion(优化)/Obfuscation(混淆)/Preverfication(预校验)四项操作。buildTypes { release { minifyEnabled false //是否打开混淆 shrinkResources true //是否打开资源混淆 proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro //用于设置proguard的规则历经 } }每个module在创建时就会创建出混淆文件proguard-rules.pro里面基本是空的。#指定压缩级别-optimizationpasses 5#不跳过非公共的库的类成员-dontskipnonpubliclibraryclassmembers#混淆时采用的算法-optimization !code/simpliffcation/arithetic,!field/*,!class/merging/*#把混淆类中的方法名也混淆了-useuniqueclassmembernames#优化时允许访问并修改修饰符的类和类成员-allowaccessmodification#将文件来源重命名为“SourceFile”字符串-renamesourefileattribute SoureFile#保留行号-keepattributes SoureFile,LineNumberTable以下时打印出的关键的流程日志-dontpreverify#混淆时是否记录日志-verbose#apk包内所有class的内部结构-dump class_files.txt#未混淆的类和成员-printseeds seed.txt#列出从apk中删除的代码-printusage unused.txt#混淆前后的映射-printmapping mapping.txt以下情形不能使用混淆反射中使用的元素需要保证类名/方法名/属性名不变否则混淆后会反射不了最好不让一些bean对象混淆四大组件不建议混淆四大组件在AndroidManifest中注册申明而混淆后类名会发生更改这样不符合四大组件的注册机制-keep public class * extend android.app.Activity -keep public class * extend android.app.Application -keep public class * extend android.app.Service -keep public class * extend android.app.content.BroadcastReceiver -keep public class * extend android.app.content.ContentProvider -keep public class * extend android.app.backup.BroadAgentHelper -keep public class * extend android.app.preference.Preference -keep public class * extend android.app.view.View -keep public class * extend android.app.verding.licensing.ILicensingService注解不能混淆很多场景下注解被用于在运行时反射一些元素-keepattributes *Annotation不能混淆枚举中的value和valueOf方法因为这两个方法时静态添加到代码中运行也会被反射使用所以无法混淆这两种方法。应用使用枚举将添加很多方法增加了包中的方法数将增加dex的大小-keepclassmembers enum * { public static **[] values(); public static ** vauleOf(java.lang.String); }JNI调用Java方法需要通过类名和方法名构成的地址形成Java使用Native方法Native是C/C编写的方法是无法一同混淆的-keepclasswithmembername class * { native methods; }JS调用Java方法-keepattributes *JavascriptInterface*WebView中JavaScript调用方法不能混淆-keepclassmembers class fqcn.of.javascript.interface.for.Webview { public *; } -keepclassmembers class * extends android.webkit.WebViewClient { public void *(android.webkit.Web,java.lang.String,android.graphics.Bitmap); public boolean *(android.webkit.Web,java.lang.String); } -keepclassmembers class * extends android.webkit.WebViewClicent { public void *(android.webkit.Web,java.lang.String); }第三方库建议使用其自身混淆规则Parcelable的子类和Creator的静态成员变量不能混淆否则会出现android.os.Bad-ParcelableExeception;-keep class * implement android.os.Parcelable { public static final android.os.Parcelable$Creator *; } -keepclassmembers class * implements java.io.Seriablizable { static final long seriablVersonUID; private static final java.io.ObjectStreamField[] seriablPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readOject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); }Gson的序列号和反序列化其实质上是使用反射获取类解析的-keep class com.google.gson.** {*;} -keep class sun.misc.Unsafe {*;} -keep class com.google.gson.stream.** {*;} -keep class com.google.gson.examples.android.modle.**{*;} -keep class com.google.** { fields; methods; } -dontwarn com.google.gson.**使用keep注解的方式哪里不想混淆就“keep”哪里先建立注解类package com.demo.annotation; //Target(ElementType.METHOD) public interface Keep { }Target可以控制其可用范围为类/方法变量。人后在proguard-rules.pro声明-dontskipnonpubliclibrayclassmember -printconfiguration -keep,allowobfusation interfaces android.support.annotation.Keep -keep andriod.support.annotation.Keep class * -keepclassmenmbers class * { android.support.annotation.Keep *; }只要记住一个混淆原则混淆改变Java路径名那么保持所在路径不被混淆就是至关重要的。资源混淆ProGuard是Java混淆工具而它只能混淆Java文件事实上还可以继续深入混淆可以混淆资源文件路径。资源混淆其实也是资源名的混淆。可以采取的方式有三种源码级别上的修改将代码和XML中的R.string.xxx替换为R.string.a并将一些图片资源xxx.png重命名为a.png然后再交给Android进行编译所有的资源ID都编译为32位int值可以看到R.java文件保存了资源数值直接修改为resources.arsc的二进制数据不改变打包流程在生成resources.arsc之后修改它同时重命名资源文件直接处理安装包解压后直接修改resources.arsc文件修改后重新打包。微信的AndResGuard的资源混淆机制。组件化混淆每个module在创建之后都会自带一个proguard-rule.pro的自定义混淆文件。每个module也可以有自己混淆的规则。但在组件化中如果每个module都是用自身的混淆则会出现重复混淆的现象造成查询不到资源文件的问题。解决这个问题是需要保证apk生成的时候有且只有一次混淆。第一种方案是最简单也是最直观的只在Application module中设置混淆其他module都关闭混淆。那么混淆的规则就都会放到Application module的proguard-rule.pro文件中。这种混淆方式的缺点是当某些模块移除后混淆规则需要手动移除。虽然理论上混淆添加多了不会造成奔溃或者编译不通过但是不需要的混淆过滤还是会对编译效率造成影响第二种方案是当Application module混淆时启动一个命令将引用的多个module的proguard-rule.pro文件合成然后再覆盖Application module中的混淆文件。这种方式可以把混淆条件解耦到每个module中但是需要编写Gradle命令来配置操作每次生成都会添加合成操作也会对编译效率造成影响第三种方案是Library module自身拥有将proguard-rule.pro文件打包到aar中的设置。 开源库中可以依赖consumerProguardFiles标志来指定库的混淆方式consumerProguardFiles属性会将*.pro文件打包进aar中库混淆时会自动使用此混淆配置文件。当Application module将全部打代码汇总混淆的时候Library module会打包为release.aar然后被引用汇总通过proguard.txt规则各自混淆保证只混淆一次。这里将固定的第三方混淆放到Base module proguard-rule.pro中每个module独有的引用库混淆放到各自的proguard-rule.pro中。最后再App module的proguard-rule.pro文件中放入Android基础属性混淆声明。四.多渠道打包将开发工具看作生产工厂让代码和资源作为原料利用最少的代码消耗去构建不同渠道不同版本的产品。多渠道基础当需要统计哪个渠道用户多变哪个渠道用户粘性强哪个渠道又需要更加个性化的设计时通过Android系统的方法可以获取到应用版本号/版本名称/系统版本/机型等各种信息唯独应用商店渠道的信息时没办法从系统获取到的我们只能认为在apk中添加渠道信息。多渠道打包中我们需要关注有两件事情将渠道信息写入apk文件将apk中的渠道信息传输到后台。打包必须经过签名这个步骤而Android的签名有两种不同的方法Android7.0以前使用v1签名方式是jar signature源于JDK;Android7.0以后引入v2签名方式是Android独有的apk signature,只对Android7.0以上有效Android7.0以下无效。signingConfigs{ release{ v2SigningEnabled false } }apk本省是zip格式文件v2签名与普通zip格式打包的不同在于普通的zip文件有三个区块而v2签名的apk拥有四个区块多出来的区块用于v2签名验证。如其他三个区块被修改了都逃不过v2验证直接导致验证失败所以这是v2签名比v1更加安全的原因。批量打包使用原生的Gradle进行打包工程大打多渠道包将非常耗时如打包过程中发现错误需要继续修复问题那么速度将增倍。因此批量打包技术就开始流行。1.使用Python打包下载安装Python环境推荐使用AndroidMultiChanneBuildTool。这个工具只支持v1签名将ChannelUtil.Java代码即成到工程中在app启动时获取渠道号并传送给后台AnalyticsConfig.setChannel(ChannelUtil.getChannel(this))把生成好的apk包项目/build/outputs/release.apk放到PythonTool文件夹中在PythonTool/info/channel.txt中编辑渠道列表以换行隔开PythonTool目录下有一个AndroidMultiChannelBuildTool.py文件双击运行该文件就会开始打包。完成后在PythonTool目录下会心出现一个output_app-release文件夹里面就是打包的渠道包了。2.使用官方提供的方式实现多渠道打包在AndroidManifest.xml中加入渠道区分标识写入一个meta标签meta-data android:namechannel android:value${channel}/在app目录的build.gradle中配置productFlavors:productFlavors { qihu360{} yingyongbao{} productFlavors.all { flavor - flavor.manifestPlaceholders [channel : name] } }在Android Studio Build -Generate signed apk中选择设置渠道。这样就可以打包不同渠道的包了在Android Studio左下角Build Variants之后还可以选择编译debug版本和release版本一次打出全部的包只需使用Gradle命令./gradlew build3.在apk文件后添加zip Commentapk文件本质上是一个带签名信息zip文件符合zip文件的格式规范。签过名的apk文件拥有四个区块签名区块的末尾就是zip文件注释包含Comment Length和File Comment两个字段前者表示注释长度后者表示注释内容正确修改这两个内容不会对zip文件造成破坏。利用这个字段可以添加渠道信息的数据推荐使用packer-ng-pugin进行打包。4.兼容v2签名的美团批量打包工具walle以上四种打包在速度和兼容性上zip comment和美团的walle的打包方式无须重新编译只做解压/添加渠道信息在打包的操作并且能兼容v1和v2签名打包。兼容最好的是原生的Gradle打包。多渠道模块配置当需要多渠道或者多场景定制一些需求时就必须使用原生Gradle来构建app了。以下是演示例子productFlavors { //用户版本 client { manifestPlacehoders [ channel:10086, //渠道号 verNum:1, //版本号 app_name:Gank //app名 ] } //服务版本 server { manifestPlacehoders [ channel:10087, //渠道号 verNum:1, //版本号 app_name:Gank服务版 //app名 ] } } dependencies { clientCompile project(:settings) //引入客户版特定module clientCompile project(:submit) clientCompile project(:server_settings) //引入服务版特定module }这里通过productFlavors属性来设置多渠道而manifestPlaceholders设置不同渠道中的不同属性这些属性需要在AndroidMainfest中声明才能使用。设置xxxCompile来配置不同渠道需要引用的module文件。接下来在app module的AndroidMainfest.xml中声明?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:toolshttp://schemas.android.com/tools packagecom.example.demo1 application android:name.basemodule.BaseApplication android:allowBackuptrue android:extractNativeLibstrue !--app名引用-- android:label${app_name} tools:replacelabel android:supportsRtltrue/ !--版本号声明-- meta-data android:nameverNum android:value${verNum}/ !--渠道名声明-- meta-data android:namechannel android:value${channel}/ /manifestandroidlabel属性用于更改签名${xxx}会自动引用manifestPlaceholders对应的key值。最后替换属性名需要添加tool:replace属性提示编译器需要替换的属性。声明meta-data用于某些额外自定义的属性这些属性都可以通过代码读取包信息来获取public class AppMetaUtils { public static int channelNum 0; /** * 获取meta-data值 * param context * param metaName * return */ public static Object getMetaData(Context context,String metaName) { Object obj null; try { if (context ! null) { String pkgName context.getPackageName(); ApplicationInfo applicationInfo context.getPackageManager().getApplicationInfo(pkgName , PackageManager.GET_META_DATA); } }catch (Exception e){ Log.e(AppMetaUtils,e.toString()); }finally { return obj; } } /** * 获取渠道号 * param context * return */ public static int getChannelNum(Context context) { if (channelNum 0) { Object object AppMetaUtils.getMetaData(context,channel); if (object ! null object instanceof Integer){ return (int)object; } } return channelNum; } }使用getApplicationInfo方法来获取应用信息然后读取meta-data中不同的key值来进一步获取渠道号。/** * 跳转到设置页面 */ public void navigationSettings() { String path /gank_setting; if (channel 10086) { path /1; }else if (channel 10087){ path _server/1; } ARouter.getInstance().build(path).navigation(); }以上是值调用的实例。如需要使用某个类调用则可以直接将路径以值的形式来传递然后使用反射的方式就能完成对象的创建productFlavors { //用户版本 client { manifestPlacehoders [ channel:10086, //渠道号 verNum:1, //版本号 app_name:Gank //app名 setting_info:material.com.setting.SettingInfo//设置数据文件 ] } //服务版本 server { if(!project.ext.isLib) { application project.ext.applicationId .server //appId } manifestPlacehoders [ channel:10087, //渠道号 verNum:1, //版本号 app_name:Gank服务版 //app名 setting_info:material.com.server_setting.ServerSettingInfo//设置数据文件 ] } }声明一个用于传递类名的meta-data:meta-data android:namesetting_info android:value${setting_info}/通过之前封装好的getMetaData获取需要调用的类/** * 获取设置信息路径 * param context * return */ public static String getSettingInfo(Context context) { if (settingInfo null){ Object object AppMetaUtils.getMetaData(context,setting_info); if (object ! null object instanceof Integer) { return (String)object; } } return settingInfo; }然后还需要一个公共的方法调用可以使用接口的形式在Base module中声明一个接口在功能module中扩展使用。public interface SettingImp { void setData(String data); }在client和server中各自继承这个接口实现方法public class SettingInfo implements SettingImp{ Override public void setData(String data) { //进行数据处理 } } public class ServerSettingInfo implements SettingImp { Override public void setData(String data) { //进行数据处理 } }接下来就可以在Base module中再次封装并获取调用方法public static void SettingData(Context context,String data) { if (getSettingInfo(context) ! null){ Log.e(AppMetaUtils,setting_info is no found); } try{ Class? clazz Class.forName(getSettingInfo(context)); SettingImp imp (SettingImp)clazz.newInstance(); imp.setData(data); }catch (ClassNotFoundException e) { Log.e(AppMetaUtils,getSettingInfo error:e.toString()); }catch (InstantiationException e) { Log.e(AppMetaUtils,getSettingInfo error:e.toString()); } catch (IllegalAccessException e) { Log.e(AppMetaUtils,getSettingInfo error:e.toString()); } }利用反射的方式来初始化接口把接口做成共性调用的方式。更深层次的运用需要在实际的需求中调整。