一.组件化的静态变量:
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组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)