Android 插件化基礎

Android 原生語言爲java,java文件通過java虛擬機後會變爲.class文件,java文件雖然能在JVM中運行,但是想在Android運行環境中執行還需要特殊處理,那就是dx處理,它會對.class文件翻譯、重構、解釋、壓縮等操作。

dx 處理會使用到一個工具 dx.jar,這個文件位於 SDK 中,具體的目錄大致爲 你的sdk根目錄/build-tools/任意版本里面。使用 dx 工具處理.class 文件,執行完之後就會生成一個.dex文件,這個 .dex 文件就可以直接在 Android 運行時環境執行,一般可以通過 PathClassLoader 去加載 dex 文件。

PathClassLoader 和 DexClassLoader的區別:前者只能加載內存中已經安裝的apk中的dex,而後者可以加載sd卡中的apk/jar ,因此 DexClassLoader 是熱修復和插件化的基礎。

Android 中,apk 安裝時,系統會使用 PathClassLoader 來加載apk文件中的dex,PathClassLoader的構造方法中,調用父類的構造方法,實例化出一個 DexPathList ,DexPathList 通過 makePathElements 在所有傳入的dexPath 路徑中,找到DexFile,存入 Element 數組,在應用啓動後,所有的類都在 Element 數組中尋找,不會再次加載。

在熱更新時,實現 DexClassLoader 子類,傳入要更新的dex/apk/jar補丁文件路徑(如sd卡路徑中存放的patch.jar),通過反射拿到 DexPathList,得到補丁 Element 數組,再從Apk原本安裝時使用的 PathClassLoader 中拿到舊版本的 Element 數組,合併新舊數組,將補丁放在數組最前面,這樣一個類一旦在補丁 Element 中找到,就不會再次加載,這樣就能替換舊 Element 中的舊類,實現熱更新。

插件化主要的兩個問題,一個是資源,一個是Activity,Activity也會加載很多資源文件,本篇文章重點從加載資源文件說起,進一步說說 AssetManager 的 addAssetPath 方法來實現資源的插件化。

Android資源文件分爲兩類,一種是res目錄下存放的可編譯性文件,編譯時,系統會自動在R.java中生成資源文件的十六進制

第二種是assets目錄下存放的資源文件,因爲apk在編譯的時候不會編譯assets下的資源文件,所以不能通過R.XX的方式訪問,因爲apk下載後不會解壓到本地,所以我們無法直接獲取到assets的絕對路徑,這時候就要藉助AssetManager類的open方法來獲取assets目錄下的文件資源了。

AssetManager中有個addAssetPath(String path)方法,在App啓動的時候,會把當前apk的路徑傳遞進去,接下來AssetManager和Resources 就能訪問apk的所有資源了,由於addAssetPath方法是不對外的,我們只能通過反射的方式獲取,然後把插件apk的路徑傳遞過去,就把插件的資源添加到資源池中了。

package com.demo.dynamicdemo11;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
import opencv.wanmao.com.mylibrary.IDynamic;

public class MainActivity extends AppCompatActivity {

    private TextView tv;
    private Button button;
    private String apkName = "plugin1.apk";    //apk名稱

    private AssetManager mAssetManager;
    private Resources mResources;
    private Resources.Theme mTheme;
    private String dexpath = null;    //apk文件地址
    private File fileRelease = null;  //釋放目錄
    private DexClassLoader classLoader = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        button = findViewById(R.id.btn_6);


        File extractFile = this.getFileStreamPath(apkName);
        dexpath = extractFile.getPath();
        fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE
        classLoader = new DexClassLoader(dexpath,
                fileRelease.getAbsolutePath(), null, getClassLoader());

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                loadResources();
                Class mLoadClassDynamic = null;
//                Dynamic
                try {
                    mLoadClassDynamic = classLoader.loadClass("opencv.wanmao.com.plug1.Dynamic");
                    Object dynamicObject = mLoadClassDynamic.newInstance();

                    IDynamic dynamic = (IDynamic) dynamicObject;
                    String content = dynamic.getStringForResId(MainActivity.this);
                    Log.e("----demo-->>", "msg:>>>>>>>" + content);
                    tv.setText(content);
                    Toast.makeText(getApplicationContext(), content + "", Toast.LENGTH_LONG).show();
                } catch (Exception e) {
                    Log.e("----demo-->>", "msg:" + e.getMessage());
                }
            }
        });
    }

    //反射獲取AssetManager並執行addAssetPath方法
    //反射獲取AssetManager 並間接獲取得到Resources
    protected void loadResources() {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexpath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }

        mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        if (mAssetManager == null) {
            Log.e("----demo-->>", "mAssetManager is null");
            return super.getAssets();
        }
        Log.e("----demo-->>", "mAssetManager is not null");
        return mAssetManager;
    }

    @Override
    public Resources getResources() {
        if (mResources == null) {
            Log.e("----demo-->>", "mResources is null");
            return super.getResources();
        }
        Log.e("----demo-->>", "mResources is not null");
        return mResources;
    }

    @Override
    public Resources.Theme getTheme() {
        if (mTheme == null) {
            Log.e("----demo-->>", "Theme is null");
            return super.getTheme();
        }
        Log.e("----demo-->>", "Theme is not null");
        return mTheme;
    }
}

在此記錄下Android中的路徑問題:

Context.getFilesDir

獲取路徑:/data/user/0/應用包名/files
該目錄是應用的文件存儲目錄,應用被卸載時,該目錄一同被系統刪除。默認存在,默認具備讀寫權限(6.0系統可以不用向用戶申請)

Context.getCacheDir

獲取路徑:/data/user/0/應用包名/cache
該目錄是應用的文件緩存目錄,應用被卸載時,該目錄一同被系統刪除。默認存在,默認具備讀寫權限。不同於getFileDir,該目錄下的文件在系統內存緊張時,會被清空文件,來騰出空間供系統使用,著名的圖片加載庫ImageLoader就是在沒有外置存儲讀寫權限時使用此文件夾。getFileDir,不會因爲系統內存不足而被清空。(6.0系統可以不用向用戶申請)

Context.getObbDir

獲取路徑:/storage/emulated/0/Android/obb/應用包名 
該目錄是應用的數據存放目錄,一般被用來存放遊戲數據包obb文件。默認存在,可讀寫(6.0系統可以不用向用戶申請)

Context.CodeCacheDir

獲取路徑:/data/user/0/應用包名/code_cache
默認存在,可讀寫。(6.0系統可以不用向用戶申請)

Context.getExternalFilesDir

獲取路徑:(以下載目錄爲準) /storage/emulated/0/Android/data/應用包名/files/Download
默認存在,可讀寫。(6.0系統可以不用向用戶申請)

Context.getExternalCacheDir

獲取路徑:/storage/emulated/0/Android/data/應用包名/cache
默認存在,可讀寫。(6.0系統可以不用向用戶申請)

Context.getDatabasePath

獲取路徑:/data/user/0/應用包名/databases/參數名
默認不存在,可讀寫。(6.0系統可以不用向用戶申請)

Context.getDir

獲取路徑:/data/user/0/應用包名/app_參數名
默認存在,可讀寫。分爲Private等三個權限,private代表僅能自己訪問。(6.0系統可以不用向用戶申請)

Context.getPackageCodePath

獲取路徑:/data/app/應用包名-1/base.apk
默認存在,獲取apk包路徑

Context.getRootDirectory

獲取路徑:/system
默認存在,不可讀寫(除非具備root權限)

Environment.getExternalStorageDirectory

獲取路徑:/storage/emulated/0
默認存在,聲明權限則可讀寫(6.0和以後系統還需要向用戶申請同意纔可以)

Environment.getExternalStoragePublicDirectory

獲取路徑:/storage/emulated/0/Download(以下載目錄爲例)
默認存在,聲明權限則可讀寫(6.0和以後系統還需要向用戶申請同意纔可以)

Context.getFileStreamPath

獲取路徑:/data/data/應用包名/files/download(示例download)
該目錄是應用的文件存儲目錄,應用被卸載時,該目錄一同被系統刪除。默認存在,默認具備讀寫權限(6.0系統可以不用向用戶申請

 

File f1=Environment.getDataDirectory();                                 //  /data
File f2=Environment.getDownloadCacheDirectory();                        //  /cache
File f3=Environment.getRootDirectory();                                 //  /system
File f4= context.getCacheDir();                                         //  /data/data/com.example.fileexiststest/cache
File f5= context.getDatabasePath("abc.db");                             //  /data/data/com.example.fileexiststest/databases/abc.db
File f6= context.getFilesDir();                                         //  /data/data/com.example.fileexiststest/files
File f7= context.getFileStreamPath("test2.txt");                        //  /data/data/com.example.fileexiststest/files/test2.txt

 

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