Android插件化基礎(4),動態啟動插件中的Activity

jopen 8年前發布 | 24K 次閱讀 Android開發 移動開發

Android插件化基礎(4),動態啟動插件中的Activity


Author:鄭海波-莫川


簡介

如何動態啟動插件中的Activity呢?我們首先分析,啟動插件中的Activity需要做那些準備?

  • 1.插件中Activity類的加載
    也就是ClassLoader的問題。由第一節課中的MultiDex可以知道,我們可以動態的加載apk,然后將插件中的class加載到當前的ClassLoader當中。這樣,插件中的class和宿主中的class同屬一個ClassLoader,它們之間的相互調用問題也就解決了。
  • 2.插件中Activity在AndroidManifest.xml中的注冊問題
    由于插件apk有自己的AndroidManifest文件,為了能夠在運行時,動態啟動插件中的Activity,需要在打包時,將插件apk的Activity注冊移動到宿主apk的AndroidManifest文件中。
  • 3.插件中Activity的資源加載問題
    處理插件中的資源加載問題,是插件化最難的問題之一!我們需要考慮很多問題:
    1.插件Activity運行時如何實時獲取Resources對象,并且能夠根據插件包名對應下的R文件的id,查找到Resources中的資源。
    2.插件apk中的資源文件與其他插件及宿主之間的資源名稱沖突如何解決?
    3.宿主及各插件的資源如何統一并且方便的管理?

問題的解決:

1.類的加載

我們使用之前我們改造的AssetsMultiDexLoader,來加載assets目錄下的apk。由于之前的博客已經說明了問題,再次不在贅述。

2.插件中的Activity在宿主AndroidManifest中注冊

這件事情需要在打包時處理,也就是說,我們需要改造我們的打包工具,在打包時,將各個插件的AndroidManifest文件合并到宿主AndroidManifest文件中。

3.插件資源的加載問題

我們需要在編譯過程和運行過程分別做處理:

3.1 編譯過程

我們首先回顧一下Android打包的過程:
1.生成R.java文件
比如:

aapt package -f -m -J ./gen -S res -M AndroidManifest.xml -I D:\android_sdk_for_studio\platforms\android-22\android.jar

2.清空bin目錄
清空上次生成的文件
3.編譯java文件和jar包

javac -encoding GBK -target 1.5 -bootclasspath D:\android_sdk_for_studio\platforms\android-22\android.jar -d bin src\net\mobctrl\normal\apk\*.java gen\net\mobctrl\normal\apk\R.java -classpath libs\*.jar

4.使用dx工具打包成classes.dex

dx --dex --output=C:\Users\mochuan.zhb\newworkspace\BundleApk5\bin\classes.dex C:\Users\mochuan.zhb\newworkspace\BundleApk5\bin\

5.編譯成資源文件

aapt package -f -M AndroidManifest.xml -S res -I D:\android_sdk_for_studio\platforms\android-22\android.jar -F bin\resources.ap_ --non-constant-id

6.使用sdklib.jar工具生成未簽名的apk

java -cp D:\android_sdk_for_studio\tools\lib\sdklib.jar com.android.sdklib.build.ApkBuilderMain bin\MyCommond.apk -v -u -z bin\resources.ap_ -f bin\classes.dex -rf C:\Users\mochuan.zhb\newworkspace\BundleApk5\src

7.使用jarsigner對apk進行簽名
jarsigner -verbose -keystore C:\test.keystore -storepass 123456 -keypass 123456 -signedjar C:\projectdemo-signed.apk C:\test.apk test

3.2編譯過程的修改

為了解決插件與插件之間以及宿主之間的資源沖突問題,我們需要對插件進行編號,修改R文件的生成過程。我們知道,R文件中的ID是一個int類型,總共32位。那么這32位分別代表什么含義呢?
1.前8位代表插件的packageId,其中兩個特殊的Id:Host是0x7f,android系統自帶的是以0x01開頭.
2.緊跟著的8位是區分資源類型的,比如layout,id,string,dimen等
3.后面16位是資源的編號

為了解決資源的命名沖突,一般由以下2中方法:

1.約定

團隊在開發時,對資源的命名進行約定,比如各業務線按照一定的規則命名,大家準守規則,避免重復。
然后在打包時,我們對各個插件的資源進行合并,統一生成R文件,所有插件和宿主的R文件內容都是完全一樣的,資源都保存在宿主項目的資源中。
說明:Google使用的Android打包過程,就是這樣的。比如主project依賴lib_project1,lib_project2等,在編譯主project的時候,它就將各個lib項目的資源都復制到主project中,然后使用aapt進行統一生成R文件,生成多個不同包名的R文件,但是R文件的內容是完全一樣的。

2.修改aapt

為了從機制上避免資源名稱重復的問題,我們可以通過修改aapt的源碼,讓其可以根據不同的packageId生成不同的id。也就是說,R文件中的id由以下32位組成:
[packageId(8)][resourceType(8)][resourcesSeq(16)]
我們為每一個插件分配一個packageId,范圍是(1,127).
修改aapt的源碼之后,我們需要改動打包過程中的第一步和第五步,在生成R文件和編譯資源的時候,使用我們改造后的aapt。這樣,最終生成的apk,其R文件就是按照我們分配的方式。
我們可以通過反編譯,查看test.apk中的R文件,如下圖所示,我們設定的packageId是5:
這里寫圖片描述
【test.apk的反編譯圖】
反編譯之后的id需要轉化為16進制顯示。

反編譯步驟:
1.解壓apk文件
2.使用d2j-dex2jar工具,將dex轉化為jar
3.使用Java-Decompiler反編譯jar

3.3運行過程中資源的加載

1.將插件apk加載到當前的AssetsManager中

核心代碼:

    /** * 修改AssetManager * * @param assetManager * @param apkPaths * @return */
    private static AssetManager modifyAssetManager(AssetManager assetManager,
            List<String> apkPaths) {
        if (apkPaths == null || apkPaths.size() == 0) {
            return null;
        }
        try {
            for (String apkPath : apkPaths) {
                try {
                    AssetManager.class.getDeclaredMethod("addAssetPath",
                            String.class).invoke(assetManager, apkPath);
                } catch (Throwable th) {
                    System.out.println("debug:createAssetManager :"
                            + th.getMessage());
                    th.printStackTrace();
                }
            }
            return assetManager;
        } catch (Throwable th) {
            System.out.println("debug:createAssetManager :" + th.getMessage());
            th.printStackTrace();
        }
        return null;
    }

    /** * 獲取整個App的資源管理器中的資源 * * @param context * @param apkPath * @return */
    public static Resources getAppResource(Context context) {
        System.out.println("debug:getAppResource ...");
        AssetsManager.copyAllAssetsApk(context);
        // 獲取dex文件列表
        File dexDir = context.getDir(AssetsManager.APK_DIR,
                Context.MODE_PRIVATE);
        File[] szFiles = dexDir.listFiles(new FilenameFilter() {

            @Override
            public boolean accept(File dir, String filename) {
                return filename.endsWith(AssetsManager.FILE_FILTER);
            }
        });
        if (szFiles == null || szFiles.length == 0) {
            return context.getResources();
        }
        System.out.println("debug:getAppResource szFiles = "+szFiles.length);
        List<String> apkPaths = new ArrayList<String>();
        for (File f : szFiles) {
            Log.i(TAG, "load file:" + f.getName());
            apkPaths.add(f.getAbsolutePath());
            System.out.println("debug:apkPath = " + f.getAbsolutePath());
        }
        AssetManager assetManager = modifyAssetManager(context.getAssets(),
                apkPaths);
        AppResource resources = new AppResource(
                assetManager, context.getResources().getDisplayMetrics(),
                context.getResources().getConfiguration());
        return resources;
    }

2.Application中Resources的加載

核心代碼

public class HostApplication extends Application {

    private Resources mAppResources = null;
    private Resources mOldResources = null;

    @Override
    public void onCreate() {
        super.onCreate();
        mOldResources = super.getResources();
        AssetsMultiDexLoader.install(this);// 加載assets中的apk
        installResource();
    }

    @Override
    public Resources getResources() {
        if(mAppResources == null){
            return mOldResources;
        }
        return this.mAppResources;
    }

    private void installResource() {
        if (mAppResources == null) {
            mAppResources = BundlerResourceLoader.getAppResource(this);// 加載assets中的資源對象
        }
    }

    @Override
    public AssetManager getAssets() {
        if (this.mAppResources == null) {
            return super.getAssets();
        }
        return this.mAppResources.getAssets();
    }

}

3.插件Activity替換Resource對象

以BundleActivity為例:

public class BundleActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.bundle_layout);
        findViewById(R.id.text_view).setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_LONG).show();
            }
        });
    }

    @Override
    public Resources getResources() {
        return getApplication().getResources();
    }

}

最關鍵的是,重寫getResources()方法,使用Application的Resource替換當前的Resource方法。

源碼地址:

https://github.com/nuptboyzhb/AndroidPluginFramework

參考

1.關于Android如何動態加載res
2.Android應用程序資源的編譯和打包過程分析
3.Android應用程序資源管理器(Asset Manager)的創建過程分析
4.Android 自動編譯、打包生成apk文件 1 - 命令行方式
5.使用ANT打包Android應用
6.如何修改android aapt源碼實現自定義package ID

來自: http://blog.csdn.net/nupt123456789/article/details/50531722

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!