Android組件化架構學習筆記——組件化編程之靜態變量/資源/混淆/多渠道打包

一.組件化的靜態變量:

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

利用反射的方式來初始化接口,把接口做成共性調用的方式。更深層次的運用需要在實際的需求中調整。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章