8.最簡單的插件化解決方案
- 插件化技術
1.合併所有插件的dex,解決插件類加載問題
2.預先在宿主中聲明所有插件中得四大組件
3.把插件中的所有資源一次合併到宿主的資源中。
8.1 在宿主清單文件裏聲明插件中的組件
8.2hostapp 加載插件中的類
/**
* * 由於應用程序使用的ClassLoader爲PathClassLoader
* 最終繼承自 BaseDexClassLoader
* 查看源碼得知,這個BaseDexClassLoader加載代碼根據一個叫做
* dexElements的數組進行, 因此我們把包含代碼的dex文件插入這個數組
* 系統的classLoader就能幫助我們找到這個類
*/
public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 獲取 BaseDexClassLoader : pathList
Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");
// 獲取 PathList: Element[] dexElements
Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");
// Element 類型
Class<?> elementClass = dexElements.getClass().getComponentType();
// 創建一個數組, 用來替換原始的數組
Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);
// 構造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 這個構造函數
Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Object o = RefInvoke.createObject(elementClass, p1, v1);
Object[] toAddElementArray = new Object[] { o };
// 把原始的elements複製進去
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 插件的那個element複製進去
System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);
// 替換
RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
}
8.4加載插件中的資源
private static void reloadInstalledPluginResources(ArrayList<String> pluginPaths) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());
for(String pluginPath: pluginPaths) {
addAssetPath.invoke(assetManager, pluginPath);
}
Resources newResources = new Resources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration());
// RefInvoke.setFieldObject(mBaseContext, "mResources", newResources);
//這是最主要的需要替換的,如果不支持插件運行時更新,只留這一個就可以了
RefInvoke.setFieldObject(mPackageInfo, "mResources", newResources);
//清除一下之前的resource的數據,釋放一些內存
//因爲這個resource有可能還被系統持有着,內存都沒被釋放
//clearResoucesDrawableCache(mNowResources);
mNowResources = newResources;
//需要清理mtheme對象,否則通過inflate方式加載資源會報錯
//如果是activity動態加載插件,則需要把activity的mTheme對象也設置爲null
// RefInvoke.setFieldObject(mBaseContext, "mTheme", null);
} catch (Throwable e) {
e.printStackTrace();
}
}
9.Activity的插件化解決方案
9.1啓動沒有在清單文件中聲明的插件activity
hook1:ActivityManagerNative 的gDefault變量 是個單例對象。獲取泛型對象
hook2:ActivityThread類中mH,handler中callback回調,如果是startActivity方法,把包裝的Intent轉換成真正要啓動的Activity.
hook3:啓動activity之前 AMS 會檢查當前啓動頁面的信息,也就是LoadedApk對象
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
9.2 故意命中緩存
public class LoadedApkClassLoaderHookHelper {
public static Map<String, Object> sLoadedApk = new HashMap<String, Object>();
public static void hookLoadedApkInActivityThread(File apkFile) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {
// 先獲取到當前的ActivityThread對象
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread");
// 獲取到 mPackages 這個靜態成員變量, 這裏緩存了dex包的信息
Map mPackages = (Map) RefInvoke.getFieldObject(currentActivityThread, "mPackages");
//準備兩個參數
// android.content.res.CompatibilityInfo
Object defaultCompatibilityInfo = RefInvoke.getStaticFieldObject("android.content.res.CompatibilityInfo", "DEFAULT_COMPATIBILITY_INFO");
//從apk中取得ApplicationInfo信息
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);
//調用ActivityThread的getPackageInfoNoCheck方法loadedApk,得到,上面兩個數據都是用來做參數的
Class[] p1 = {ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo")};
Object[] v1 = {applicationInfo, defaultCompatibilityInfo};
Object loadedApk = RefInvoke.invokeInstanceMethod(currentActivityThread, "getPackageInfoNoCheck", p1, v1);
//爲插件造一個新的ClassLoader
String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
RefInvoke.setFieldObject(loadedApk, "mClassLoader", classLoader);
//把插件LoadedApk對象放入緩存
WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference);
// 由於是弱引用, 因此我們必須在某個地方存一份, 不然容易被GC; 那麼就前功盡棄了.
sLoadedApk.put(applicationInfo.packageName, loadedApk);
}
/**
* 這個方法的最終目的是調用
* android.content.pm.PackageParser#generateActivityInfo(android.content.pm.PackageParser.Activity, int, android.content.pm.PackageUserState, int)
*/
public static ApplicationInfo generateApplicationInfo(File apkFile)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
// 找出需要反射的核心類: android.content.pm.PackageParser
Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
// 我們的終極目標: android.content.pm.PackageParser#generateApplicationInfo(android.content.pm.PackageParser.Package,
// int, android.content.pm.PackageUserState)
// 要調用這個方法, 需要做很多準備工作; 考驗反射技術的時候到了 - -!
// 下面, 我們開始這場Hack之旅吧!
// 首先拿到我們得終極目標: generateApplicationInfo方法
// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// public static ApplicationInfo generateApplicationInfo(Package p, int flags,
// PackageUserState state) {
// 其他Android版本不保證也是如此.
// 首先, 我們得創建出一個Package對象出來供這個方法調用
// 而這個需要得對象可以通過 android.content.pm.PackageParser#parsePackage 這個方法返回得 Package對象得字段獲取得到
// 創建出一個PackageParser對象供使用
Object packageParser = packageParserClass.newInstance();
// 調用 PackageParser.parsePackage 解析apk的信息
// 實際上是一個 android.content.pm.PackageParser.Package 對象
Class[] p1 = {File.class, int.class};
Object[] v1 = {apkFile, 0};
Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1);
// 第三個參數 mDefaultPackageUserState 我們直接使用默認構造函數構造一個出來即可
Object defaultPackageUserState = packageUserStateClass.newInstance();
// 萬事具備!!!!!!!!!!!!!!
Class[] p2 = {packageParser$PackageClass, int.class, packageUserStateClass};
Object[] v2 = {packageObj, 0, defaultPackageUserState};
ApplicationInfo applicationInfo = (ApplicationInfo)RefInvoke.invokeInstanceMethod(packageParser, "generateApplicationInfo", p2, v2);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;
return applicationInfo;
}
}