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