一.組件化的靜態變量:
- 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());
}
}
利用反射的方式來初始化接口,把接口做成共性調用的方式。更深層次的運用需要在實際的需求中調整。