Qigsaw源碼之Gradle插件解析

hi,2021

Android App BundleQigsaw的前置依賴知識點目养。

Android App BundleAndroid新推出的一種官方發(fā)布格式.aab序六,可讓您以更高效的方式開發(fā)和發(fā)布應(yīng)用。借助 Android App Bundle戈擒,您可以更輕松地以更小的應(yīng)用提供優(yōu)質(zhì)的使用體驗蟹演,從而提升安裝成功率并減少卸載量阴颖。轉(zhuǎn)換過程輕松便捷。您無需重構(gòu)代碼即可開始獲享較小應(yīng)用的優(yōu)勢云头。改用這種格式后捐友,您可以體驗?zāi)K化應(yīng)用開發(fā)和可自定義功能交付,并從中受益(PS:必須依賴于GooglePlay)溃槐。

qigsaw基于AAB實現(xiàn)匣砖,同時完全仿照AAB提供的play core library接口加載插件,開發(fā)查閱官方文檔即可開始開發(fā)昏滴。如果有國際化需求的公司可以在國內(nèi)版和國際版上無縫切換猴鲫。同時Qigsaw實現(xiàn)0 hook,僅有少量私有 API 訪問谣殊,保證其兼容性和穩(wěn)定性拂共。

Github:Qigsaw

本篇文章主要講述Qigsaw相關(guān)的plugin

Qigsaw插件

主工程進(jìn)行進(jìn)行apply plugin: 'com.iqiyi.qigsaw.application'插件的依賴姻几;

feature工程進(jìn)行以下依賴:

apply plugin: 'com.android.dynamic-feature'
apply plugin: 'com.iqiyi.qigsaw.dynamicfeature'

gradle.properties文件中配置QIGSAW_BUILD=true宜狐,才會有feature包的一些信息生成。

com.iqiyi.qigsaw.application

com.iqiyi.qigsaw.application.properties文件內(nèi)容為:

implementation-class=com.iqiyi.qigsaw.buildtool.gradle.QigsawAppBasePlugin

QigsawAppBasePlugin默認(rèn)會注冊一個 SplitComponentTransform蛇捌,在開啟QIGSAW_BUILD=true之后還會注冊SplitResourcesLoaderTransform抚恒。通過 Transform實現(xiàn)對插件內(nèi)容的AOP

QigsawAppBasePlugin除過注冊兩個Transform之外络拌,為主要的是處理插件和基礎(chǔ)包信息生成Qigsaw產(chǎn)物柑爸。

com.iqiyi.qigsaw.dynamicfeature

com.iqiyi.qigsaw.dynamicfeature.properties文件內(nèi)容為:

implementation-class=com.iqiyi.qigsaw.buildtool.gradle.QigsawDynamicFeaturePlugin

QigsawDynamicFeaturePlugin在開啟QIGSAW_BUILD=true之后會注冊SplitResourcesLoaderTransform以及SplitLibraryLoaderTransform實現(xiàn)對插件內(nèi)容的AOP

SplitResourcesLoaderTransform

主要是向Activity盒音、ServiceReceiver類中的getResources注入SplitInstallHelper.loadResources(this, super.getResources())表鳍。

interface SplitComponentWeaver {
    /**
     * 鏈接目標(biāo)
     */
    String CLASS_WOVEN = "com/google/android/play/core/splitinstall/SplitInstallHelper"
    /**
     * 鏈接方法
     */
    String METHOD_WOVEN = "loadResources"
    byte[] weave(InputStream inputStream)
}

相關(guān)注入類為:

class SplitResourcesLoaderInjector {
    WaitableExecutor waitableExecutor
    /**
     * 預(yù)埋的 Activity
     */
    Set<String> activities
    Set<String> services
    Set<String> receivers
    SplitActivityWeaver activityWeaver
    SplitServiceWeaver serviceWeaver
    SplitReceiverWeaver receiverWeaver
    /**部分代碼省略**/
}

其中基礎(chǔ)包和插件的區(qū)別主要是注冊的目標(biāo)不同:

基礎(chǔ)包只是讀取build.gradle文件中的qigsawSplit.baseContainerActivities配置的Activity

而插件需要讀取AndroidManifest.xml文件中的Activity祥诽、ServiceReceiver譬圣。

SplitInstallHelper.loadResources(this, super.getResources());的作用是將所有插件資源路徑添加到AssetManager中,這樣各個插件就可以訪問所有的資源雄坪,關(guān)鍵實現(xiàn)代碼如下:

static Method getAddAssetPathMethod() throws NoSuchMethodException {
    if (addAssetPathMethod == null) {
        addAssetPathMethod = HiddenApiReflection.findMethod(AssetManager.class, "addAssetPath", String.class);
    }
    return addAssetPathMethod;
}

SplitComponentTransform

Transform主要進(jìn)行了兩個操作 :

  • 讀取各個插件apkManifest文件厘熟,創(chuàng)建ComponentInfo類并將將各個插件apkApplication,Activity,Service,Recevier記錄在該類的字段中,字段名稱以工程名+組件類型命名维哈,值為各個插件apk包含的組件绳姨,如過包含多個用逗號隔開。
//com.iqiyi.android.qigsaw.core.extension.ComponentInfo
public class ComponentInfo {
   public static final String native_ACTIVITIES = "com.iqiyi.qigsaw.sample.ccode.NativeSampleActivity";
   public static final String java_ACTIVITIES = "com.iqiyi.qigsaw.sample.java.JavaSampleActivity";
   public static final String java_APPLICATION = "com.iqiyi.qigsaw.sample.java.JavaSampleApplication";
}
  • 為每個provider創(chuàng)建代理類 類名為String providerClassName=providerName+"Decorated"+splitName阔挠,其中providerName為原始provider類名飘庄,splitName為插件apk對應(yīng)的名稱,并且該類繼承SplitContentProvider购撼。
public class JavaContentProvider_Decorated_java extends SplitContentProvider {}
provider_deccorated.png

為啥這么做呢?

因為在app啟動時provider的執(zhí)行時機(jī)是比較靠前的跪削,
Application->attachBaseContext ==>ContentProvider->onCreate ==>Application->onCreate ==>Activity->onCreate在這個過程中我們的插件apk并沒有加載進(jìn)來,一定會報ClassNotFound迂求。所以我們將插件apkprovider生成一個代理類碾盐,然后替換掉,如果插件沒有加載進(jìn)來揩局,代理類什么也不執(zhí)行就可以了毫玖。很好的解決了我們的問題。

SplitLibraryLoaderTransform

SplitLibraryLoaderTransform類進(jìn)行的操作是向dynamic-feature構(gòu)建apk的過程中凌盯,創(chuàng)建以 "com.iqiyi.android.qigsaw.core.splitlib." + project.name + "SplitLibraryLoader"的類付枫。

// com.iqiyi.android.qigsaw.core.splitlib.assetsSplitLibraryLoader
// com.iqiyi.android.qigsaw.core.splitlib.javaSplitLibraryLoader
// com.iqiyi.android.qigsaw.core.splitlib.nativeSplitLibraryLoader
package com.iqiyi.android.qigsaw.core.splitlib;
public class javaSplitLibraryLoader {
    public void loadSplitLibrary(String str) {
        System.loadLibrary(str);
    }
}

這個類的作用是啥呢?

下面我們來解釋一下十气,你會發(fā)現(xiàn)很有趣的励背。

  • Qigsaw是基于對于com.google.android.play.core對外暴露的方法,進(jìn)行了自定義實現(xiàn)砸西。因為aab目前只能對google play上發(fā)布應(yīng)用起作用叶眉,所以開發(fā)者重新實現(xiàn)了一套com.google.android.play.core包名的第三方庫,這樣就可以做到在國內(nèi)市場芹枷,與國外應(yīng)用市場無縫遷移衅疙。
  • Qigsaw提供兩種加載方式加載插件apk,單Classloader和多Classloader模式鸳慈,單Classloader涉及私有api訪問饱溢,而多Classloader不涉及私有api訪問。

該類的存在就是為了解決多Classloader模式下的so加載問題
System.loadLibrary(str);該方法會使用調(diào)用方的classloader從中獲取so信息并加載走芋。

//java.lang.System.java
@CallerSensitive
public static void loadLibrary(String libname) {
  Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

由于多Classloader模式下绩郎,每個插件都要各自的Classloader,sodex都在各自的Classloader中記錄潘鲫,所以在多Classloader模式下, System.loadLibrary應(yīng)由插件apk各自的Classloader調(diào)用肋杖。具體實現(xiàn)可參考SplitLibraryLoaderHelper類溉仑。

//com.iqiyi.android.qigsaw.core.splitload.SplitLibraryLoaderHelper.java
private static boolean loadSplitLibrary0(ClassLoader classLoader, String splitName, String name) {
    try {
        Class<?> splitLoaderCl = classLoader.loadClass("com.iqiyi.android.qigsaw.core.splitlib." + splitName + "SplitLibraryLoader");
        Object splitLoader = splitLoaderCl.newInstance();
        Method method = HiddenApiReflection.findMethod(splitLoaderCl, "loadSplitLibrary", String.class);
        method.invoke(splitLoader, name);
        return true;
    } catch (Throwable ignored) {

    }
    return false;
}

Qigsaw編譯解析

Qigsaw打包流程

qigsaw_plugin_flow_chart.png

copySplitManifestDebug

實現(xiàn)feature包下生成的AndroidManifest.xml文件的拷貝。

目標(biāo)文件和地址:featureName/build/intermediates/merged_manifests/debug/AndroidManifest.xml状植。

拷貝后的地址:app/build/intermediates/qigsaw/split-outputs/manifests/debug浊竟。

拷貝后的文件名:$featureName.xml

ProcessTaskDependenciesBetweenBaseAndSplitsWithQigsaw

觸發(fā)copySplitManifestDebug任務(wù),將feature包生成的產(chǎn)物和數(shù)據(jù)輸出到qigsawProcessDebugManifest任務(wù)中津畸。

extractTargetFilesFromOldApk

app_debug.apk解壓 將assets/目錄下所有內(nèi)容釋放到app/build/intermediates/qigsaw/old-apk/target-files/xxx

qigsawProcessDebugManifest

SplitComponentTransform創(chuàng)建的$ContentProviderName_Decorated_$featureName繼承SplitContentProvider代替原有的Provider振定。

因為Provider 在應(yīng)用啟動的時候就需要加載,避免這個時候feature包沒有下載下來肉拓,先加載一個代理的Provider后频。

provider.png

generateDebugQigsawConfig

生成以下文件:

@Keep
public final class QigsawConfig {
    public static final String DEFAULT_SPLIT_INFO_VERSION = "1.0.0_1.0.0";
    public static final String[] DYNAMIC_FEATURES = {"java", "assets", "native"};
    public static final String QIGSAW_ID = "1.0.0_c40ab5d";
    public static final boolean QIGSAW_MODE = Boolean.parseBoolean("true");
    public static final String VERSION_NAME = "1.0.0";
}

QIGSAW_ID回先獲取基礎(chǔ)包的id,如果沒有那么為當(dāng)前的QigsawId帝簇。

processSplitApkDebug

每個feature都需要執(zhí)行的任務(wù)徘郭,分別處理自己的的apk并生成對應(yīng)的json文件。

  • feature包的apk文件解壓到app/build/intermediates/qigsaw/split-outputs/unzip/debug/$featureName文件丧肴;

  • 遍歷解壓apk中的lib文件目錄残揉,找到支持的ABI;

  • 如果有lib文件有so文件,那么在該目錄生成一個AndroidManifest.xml文件芋浮;

    • lib文件和生成的AndroidManifest.xml壓縮為protoAbiApk;

    • 利用aapt2工具將 protoAbiApkbinaryAbiApk中;

    • binaryAbiApk進(jìn)行簽名生成app/build/intermediates/qigsaw/split-outputs/apks/debug/$feature-$abi.apk;

    • 生成SplitInfo.SplitApkData數(shù)據(jù)抱环;

      {
        "abi": "x86",
        "url": "assets://qigsaw/native-x86.zip",
        "md5": "03a29962b87c6ed2a7961b6dbe45f532",
        "size": 8539
      }
      
  • 遍歷解壓apk除過lib之前的文件目錄,壓縮為$fearure-master-unsigned.apk纸巷。簽名生成app/build/intermediates/qigsaw/split-outputs/apks/debug/$feature-master.apk;

  • 生成SplitInfo.SplitApkData數(shù)據(jù)镇草;

    {
      "abi": "master",
      "url": "assets://qigsaw/native-master.zip",
      "md5": "3b89066aeaf7d2c2a59b4f3a10fef345",
      "size": 12824
    }
    
  • 更具lib文件下的數(shù)據(jù)生成SplitInfo.SplitLibData數(shù)據(jù);

    {
      "abi": "arm64-v8a",
      "jniLibs": [
        {
          "name": "libhello-jni.so",
          "md5": "2938d8b40825e82715422dbdba479e4f",
          "size": 5896
        }
      ]
    }
    
  • 最后生成每個featureSplitInfo數(shù)據(jù)瘤旨,寫入/app/build/intermediates/qigsaw/split-outputs/split-info/debug/$featureName.json文件梯啤;

    public class SplitInfo implements Cloneable, GroovyObject {
        private String splitName;//feature包名稱
        private boolean builtIn;//!onDemand||!releaseSplitApk(releaseSplitApk是gradle中配置項)
        private boolean onDemand;//取自AndroidManifest.xml中的onDemand
        private String applicationName;//feature應(yīng)用名
        private String version;//feature包中的versionname@versioncode
        private int minSdkVersion;//feature最低版本
        private int dexNumber;//feature包中的dex數(shù)量
        private Set<String> dependencies;//feature包的依賴;
        private Set<String> workProcesses;//feature包AndroidManifest.xml中的Activity存哲、Service因宇、Receiver、provider配置的進(jìn)程祟偷;
        private List<SplitInfo.SplitApkData> apkData;//SplitInfo.SplitApkData數(shù)據(jù)
        private List<SplitInfo.SplitLibData> libData;//SplitInfo.SplitLibData數(shù)據(jù)
    }
    

qigsawAssembleDebug

  • build/intermediates/qigsaw/split-outputs/split-info/debug中的每個feature包生成的json合并察滑;
  • 合并之后的文件與基礎(chǔ)包中的Qigsaw配置文件進(jìn)行對比,生成新的增量Qigsaw配置文件修肠;
    • 對比規(guī)則是verisonName相等的時候?qū)Ρ?code>split.version贺辰,有一個不同就表示有更新;
    • 如果有更新,那么QigsawId為基礎(chǔ)包的QigsawId,并分析和修改split信息饲化;
      • 修改split信息的時候莽鸭,相同的splitName對比split.version。如果相同那么split使用基礎(chǔ)包的split信息吃靠,如果不同那么該splitbuiltIn=false蒋川,onDemand=true。并將有更新的split做記錄(updatesplits字段值)撩笆。此時updateMode值為VERSION_CHANGED=1
      • 沒有任何修改缸浦,那么updateMode值為VERSION_NO_CHANGED=2夕冲;
      • 如果沒有基礎(chǔ)包,那么updateMode值為DEFAULT=0裂逐;
  • 分別判斷如果feature包的builtInfalse歹鱼;
    • 判斷是否有上傳服務(wù),如有有那么上傳feature包卜高。上傳成功后將對應(yīng)的url地址修改為可下載的http地址弥姻。如果地址為空,或者不是http開頭會跑異常掺涛。
    • 如果沒有實現(xiàn)上傳服務(wù)那么builtIn置為true庭敦;
  • 格式化split內(nèi)容,寫到build/intermediates/qigsaw/split-details/debug文件目錄下薪缆。
  • updateMode值寫到build/intermediates/qigsaw/split-details/debug/_update_record_.json文件秧廉。
  • 如果updateMode值為VERSION_NO_CHANGED,那么將intermediates/qigsaw/old-apk/target-files/debug/assets/qigsaw/qigsaw_*.json文件拷貝到app/build/intermediates/merged_assets/debug/out/qigsaw/qigsaw_*.json;
    • 否則將app/build/intermediates/qigsaw/split-details/debug/qigsaw_*.json文件拷貝到app/build/intermediates/merged_assets/debug/out/qigsaw/qigsaw_*.json;
  • app/build/intermediates/qigsaw/split-details/debug/base.app.cpu.abilist.properties寫入支持的abi拣帽,并將其拷貝到app/build/intermediates/merged_assets/debug/out/下面疼电;
  • 遍歷feature生成的splitinfo信息,如果builtIntrue;
    • 如果updateMode值為DEFAULT=0减拭,將將app/build/intermediates/qigsaw/split-outputs/apks/debug/*.apk拷貝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip蔽豺;
    • 如果updateMode值為DEFAULT!=0,判斷該feature是否是在updateSplits中;
      • 如果是那么將app/build/intermediates/qigsaw/split-outputs/apks/debug/*.apk拷貝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip拧粪;
      • 如果不是將app/build/intermediates/qigsaw/old-apk/target-files/debug/assets/qigsaw/*.zip拷貝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip修陡;

產(chǎn)物

Qigsaw配置文件

{
  "qigsawId": "1.0.0_c40ab5d",
  "appVersionName": "1.0.0",
  "updateSplits": [
    "java"
  ],
  "splits": [
    {
      "splitName": "java",
      "builtIn": true,
      "onDemand": false,
      "applicationName": "com.iqiyi.qigsaw.sample.java.JavaSampleApplication",
      "version": "1.1@1",
      "minSdkVersion": 14,
      "dexNumber": 2,
      "workProcesses": [
        ""
      ],
      "apkData": [
        {
          "abi": "master",
          "url": "assets://qigsaw/java-master.zip",
          "md5": "658bc419a9d3c7812a36e61f6c5be4c4",
          "size": 12822
        }
      ]
    }
    {
      "splitName": "native",
      "builtIn": true,
      "onDemand": true,
      "version": "1.0@1",
      "minSdkVersion": 14,
      "dexNumber": 2,
      "apkData": [
        {
          "abi": "arm64-v8a",
          "url": "assets://qigsaw/native-arm64-v8a.zip",
          "md5": "b01ad63db38a4ec5fad3284c573a02d3",
          "size": 8545
        },
        {
          "abi": "master",
          "url": "assets://qigsaw/native-master.zip",
          "md5": "3c41745a16a31e967cde8247009463f1",
          "size": 12824
        }
      ],
      "libData": [
        {
          "abi": "arm64-v8a",
          "jniLibs": [
            {
              "name": "libhello-jni.so",
              "md5": "2938d8b40825e82715422dbdba479e4f",
              "size": 5896
            }
          ]
        }
      ]
    }
  ]
}

Qigsaw加載的壓縮包

app-debug.png

下期研究知識點

  • 混淆相關(guān)使用操作;
  • Tinker熱修改相關(guān)使用操作既们;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末厘灼,一起剝皮案震驚了整個濱河市座掘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖修档,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件感猛,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)主经,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庭惜,“玉大人罩驻,你說我怎么就攤上這事』ど蓿” “怎么了惠遏?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長骏啰。 經(jīng)常有香客問我节吮,道長,這世上最難降的妖魔是什么判耕? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任透绩,我火速辦了婚禮,結(jié)果婚禮上壁熄,老公的妹妹穿的比我還像新娘帚豪。我一直安慰自己,他們只是感情好草丧,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布狸臣。 她就那樣靜靜地躺著,像睡著了一般方仿。 火紅的嫁衣襯著肌膚如雪固棚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天仙蚜,我揣著相機(jī)與錄音此洲,去河邊找鬼。 笑死委粉,一個胖子當(dāng)著我的面吹牛呜师,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贾节,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼汁汗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了栗涂?” 一聲冷哼從身側(cè)響起知牌,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斤程,沒想到半個月后角寸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年扁藕,在試婚紗的時候發(fā)現(xiàn)自己被綠了沮峡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡亿柑,死狀恐怖邢疙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情望薄,我是刑警寧澤疟游,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站痕支,受9級特大地震影響乡摹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜采转,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞬痘。 院中可真熱鬧故慈,春花似錦、人聲如沸框全。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽津辩。三九已至拆撼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喘沿,已是汗流浹背闸度。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留蚜印,地道東北人莺禁。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像窄赋,于是被迫代替她去往敵國和親哟冬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容