Android组件化架构学习笔记——组件化编程之静态变量资源混淆多渠道打包

Android组件化架构学习笔记——组件化编程之静态变量资源混淆多渠道打包,第1张

概述一.组件化的静态变量:R.java的生成:各个module会生成aar文件,并且被引用到Applicationmodule中,最终合并为apk文件。当各个次级module在Applicationmodule中被解压后,在编译时资源R.java会被重新解压到build/generated/source/debug(release)/包名/R.java中。当每个组件中的

一.组件化的静态变量:

R.java的生成:@H_404_7@

各个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:@H_404_7@

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中注册申明,而混淆后类名会发生更改,这样不符合四大组件的注册机制;@H_404_7@
-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
注解不能混淆,很多场景下注解被用于在运行时反射一些元素;@H_404_7@
-keepattributes *Annotation
不能混淆枚举中的value和valueOf方法,因为这两个方法时静态添加到代码中运行,也会被反射使用,所以无法混淆这两种方法。应用使用枚举将添加很多方法,增加了包中的方法数,将增加dex的大小;@H_404_7@
-keepclassmembers enum * {    public static **[] values();    public static ** vauleOf(java.lang.String);}
JNI调用Java方法,需要通过类名和方法名构成的地址形成;Java使用Native方法,Native是C/C++编写的,方法是无法一同混淆的;@H_404_7@
-keepclasswithmembername class * {    native <methods>;}
Js调用Java方法;@H_404_7@
-keepattributes *JavaScriptInterface*

 

WebVIEw中JavaScript调用方法不能混淆;@H_404_7@
-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;@H_404_7@
-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的序列号和反序列化,其实质上是使用反射获取类解析的;@H_404_7@
-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”哪里,先建立注解类;@H_404_7@
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文件,修改后重新打包。@H_404_7@

微信的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中,库混淆时会自动使用此混淆配置文件。@H_404_7@

当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中的渠道信息传输到后台。@H_404_7@

打包必须经过签名这个步骤,而AndroID的签名有两种不同的方法:

Android7.0以前,使用v1签名方式,是jar signature,源于JDK;Android7.0以后,引入v2签名方式,是AndroID独有的apk signature,只对Android7.0以上有效,Android7.0以下无效。@H_404_7@
 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文件夹,里面就是打包的渠道包了。@H_404_7@

2.使用官方提供的方式实现多渠道打包:

在AndroIDManifest.xml中加入渠道区分标识,写入一个Meta标签;@H_404_7@
<Meta-data androID:name="channel" androID:value="${channel}"/>
在app目录的build.gradle中配置productFlavors:@H_404_7@
   productFlavors {        qihu360{}        yingyongbao{}        productFlavors.all {            flavor -> flavor.manifestPlaceholders = [channel : name]        }    }
在AndroID Studio Build ->Generate signed apk中选择设置渠道。@H_404_7@

这样就可以打包不同渠道的包了,在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());        }    }

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

总结

以上是内存溢出为你收集整理的Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包全部内容,希望文章能够帮你解决Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1104360.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-28
下一篇 2022-05-28

发表评论

登录后才能评论

评论列表(0条)

保存