一.组件化的静态变量:

  • 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资源。

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());
        }
    }

利用反射的方式来初始化接口,把接口做成共性调用的方式。更深层次的运用需要在实际的需求中调整。