一.组件化的静态变量:
- 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 *
-keepclassmen=mbers 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:name="channel" 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 build
3.在apk文件后添加zip Comment
apk文件本质上是一个带签名信息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 version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.demo1">
<application
android:name=".basemodule.BaseApplication"
android:allowBackup="true"
android:extractNativeLibs="true"
<!--app名引用-->
android:label="${app_name}"
tools:replace="label"
android:supportsRtl="true"/>
<!--版本号声明-->
<meta-data android:name="verNum" android:value="${verNum}"/>
<!--渠道名声明-->
<meta-data android:name="channel" android:value="${channel}"/>
</manifest>
android:label属性用于更改签名,${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:name="setting_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());
}
}
利用反射的方式来初始化接口,把接口做成共性调用的方式。更深层次的运用需要在实际的需求中调整。