1. 程式人生 > >解決資源id衝突 資源的外掛化

解決資源id衝突 資源的外掛化

--摘自《android外掛化開發指南》

1.一套完整的Android App打包流程(Gradle方案)

第一步:aapt。為res目錄下的資源生成R.java檔案,同時為AndroidManifest.xml生成Manifest.java檔案

第二步:aidl。把專案中自定義的aidl檔案生成相應的Java程式碼檔案

第三步:javac。把專案中所有的Java程式碼編譯成class檔案。包括三部分Java程式碼,自己寫的的業務邏輯程式碼,aapt生成的Java檔案,aidl生成的Java檔案

第四步:proguard。混淆同時生成proguardMapping.txt。這一步是可選的

第五步:dex。把所有的class檔案(包括第三方庫的class檔案)轉換成dex檔案

第六步:aapt。把res目錄下的資源、assets目錄下的檔案,打包成一個.ap_檔案

第七步:apkbuilder。將所有的dex檔案、ap_檔案、AndroidManifest.xml打包為.apk檔案,這是一個未簽名的apk包

第八步:jarsigner。對apk進行簽名

第九步:zipaligin。對要釋出的apk檔案進行對齊操作,以便在執行時節省記憶體

2.res目錄下的所有資源會生成一個R.java檔案,每個資源都對應一個R中的十六進位制整數變數,由三部分組成,即PackageId+TypeId+EntryId(一般是預設0x7f+兩位+四位)

***外掛化中資源id衝突的解決方案***

方案1:

把宿主和外掛的資源都合併到一起

方案1.1:重寫AAPT命令,在外掛apk打包過程中,通過指定資源id的字首,比如0x71,來保證宿主和外掛的資源id永遠不會衝突

  1)在AAPT的命令列引數中傳遞apk打包時的字首值

  2)把這個值設定給Bundle實體的mApkModule欄位,作為ResourceTable建構函式的引數傳遞進去

  3)在ResourceTable的建構函式,讀取Bundle引數中的mApkModule值,也就是字首值,設定給packageId

  都是c程式碼,就不貼了

  4)把新的AAPT工具命名為aapt_mac,放到專案根目錄下

apply plugin: 'com.android.application'

import com.android.sdklib.BuildToolInfo
import java.lang.reflect.Method

Task modifyAaptPathTask = task('modifyAaptPath') << {
    android.applicationVariants.all { variant ->
        BuildToolInfo buildToolInfo = variant.androidBuilder.getTargetInfo().getBuildTools()
        Method addMethod = BuildToolInfo.class.getDeclaredMethod("add", BuildToolInfo.PathId.class, File.class)
        addMethod.setAccessible(true)
        addMethod.invoke(buildToolInfo, BuildToolInfo.PathId.AAPT, new File(rootDir, "aapt_mac"))
        println "[LOG] new aapt path = " + buildToolInfo.getPath(BuildToolInfo.PathId.AAPT)
    }
}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"

    defaultConfig {
        applicationId "jianqiang.com.testreflection"
        minSdkVersion 21
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    preBuild.doFirst {
        modifyAaptPathTask.execute()
    }

    aaptOptions {
        aaptOptions.additionalParameters '--PLUG-resoure-id', '0x71'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.2.0'
}

通過反射,把AAPT路徑臨時修改為指向當前app根路徑下的mac_aapt

方案1.2:在外掛apk打包後,修改R,java和resources.arsc中儲存的資源id值,比如預設的0x7f字首,修改為0x71,這樣就保證了宿主和外掛的資源id永遠不會衝突

方案1.3:在public.xml中指定apk中所有資源的id值。但每增加一個資源,都要維護public.xml。所以只能用於固定幾個特定的值

afterEvaluate {
    for (variant in android.applicationVariants) {
        def scope = variant.getVariantData().getScope()
        String mergeTaskName = scope.getMergeResourcesTask().name

        def mergeTask = tasks.getByName(mergeTaskName)

        mergeTask.doLast {
            copy {
                int i = 0
                println android.sourceSets.main.res.srcDirs
                from(android.sourceSets.main.res.srcDirs) {
                    include 'values/public.xml'
                    rename 'public.xml', (i++ == 0 ? "public.xml" : "public_${i}.xml")
                }

                into(mergeTask.outputDir)
            }
        }
    }
}

 string.xml

<resources>
    <string name="app_name">ActivityHook1</string>

    <string name="string1">Test String</string>
</resources>

public.xml

<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <public type = "string" name="string1" id = "0x7f050024"/>
</resources>

宿主的資源值固定了以後,外掛如果想使用宿主的資源,只要把宿主打包成jar,然後複製到外掛專案的某個位置,使用gradle指令碼provided就可以了,這樣打出的外掛不會包含宿主的程式碼

方案2:如果不事先合併資源,那就為每個外掛建立一個AssetManager,每個AssetManager都是通過反射呼叫addAssetPath方法,把外掛自己的資源新增進去。詳細的程式碼見資源的外掛化

方案1的缺點是資源id的字首是有限的,就256個值,當一個app中有多於256個外掛時,就要考慮方案2了