主目錄見:Android高級進階知識(這是總目錄索引)
框架地址:VirtualApk
在線源碼查看:AndroidXRef
上一篇文章插件化框架VirtualApk之初始化我們已經講了框架初始化的內容,主要就是hook Instrumentation
類和hook AMS
系統服務。今天這篇就是加載插件apk的內容,是插件化非常重要的一步,廢話不多說,直接開寫。。。
一.插件加載
在宿主工程初始化之后,我們就要加載插件工程了,還是從用法下手:
PluginManager pluginManager = PluginManager.getInstance(base);
File apk = new File(Environment.getExternalStorageDirectory(), "Test.apk");
if (apk.exists()) {
try {
pluginManager.loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
}
}
在前面我們已經實例化過PluginManager
實例了,因為單例模式,所以直接會返回PluginManager
實例,所以我們跟進PluginManager#loadPlugin()
方法:
public void loadPlugin(File apk) throws Exception {
if (null == apk) {
throw new IllegalArgumentException("error : apk is null.");
}
if (!apk.exists()) {
throw new FileNotFoundException(apk.getAbsolutePath());
}
LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
if (null != plugin) {
this.mPlugins.put(plugin.getPackageName(), plugin);
// try to invoke plugin's application
plugin.invokeApplication();
} else {
throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
}
}
這個方法主要做了兩件事,一個是根據apk構建LoadedPlugin
類實例,然后就是調用插件的Application
,我們一個一個來看,首先我們看LoadedPlugin#create()
方法做了啥:
public static LoadedPlugin create(PluginManager pluginManager, Context host, File apk) throws Exception {
return new LoadedPlugin(pluginManager, host, apk);
}
這個方法并沒有做什么復雜的操作,就是實例化了LoadedPlugin
類,參數分別是PluginManager
實例,宿主工程的上下文對象Context
,最后就是apk文件,接著我們看LoadedPlugin
類的構造函數寫了啥:
LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
this.mPluginManager = pluginManager;
this.mHostContext = context;
this.mLocation = apk.getAbsolutePath();
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
this.mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
this.mPackageInfo.signatures = this.mPackage.mSignatures;
this.mPackageInfo.packageName = this.mPackage.packageName;
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
}
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
this.mPackageManager = new PluginPackageManager();
this.mPluginContext = new PluginContext(this);
this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
this.mResources = createResources(context, apk);
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
tryToCopyNativeLib(apk);
// Cache instrumentations
Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
}
this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);
// Cache activities
Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity activity : this.mPackage.activities) {
activityInfos.put(activity.getComponentName(), activity.info);
}
this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
// Cache services
Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
for (PackageParser.Service service : this.mPackage.services) {
serviceInfos.put(service.getComponentName(), service.info);
}
this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);
// Cache providers
Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
for (PackageParser.Provider provider : this.mPackage.providers) {
providers.put(provider.info.authority, provider.info);
providerInfos.put(provider.getComponentName(), provider.info);
}
this.mProviders = Collections.unmodifiableMap(providers);
this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);
// Register broadcast receivers dynamically
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {
receivers.put(receiver.getComponentName(), receiver.info);
try {
BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
this.mHostContext.registerReceiver(br, aii);
}
} catch (Exception e) {
e.printStackTrace();
}
}
this.mReceiverInfos = Collections.unmodifiableMap(receivers);
this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
}
我們看到這個方法比較長,但是都很重要,所以我們這里不省略,一部分一部分來進行說明,首先我們看下面這部分:
this.mPluginManager = pluginManager;
this.mHostContext = context;
this.mLocation = apk.getAbsolutePath();
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
這部分首先是將pluginManager
和context
賦值給LoadedPlugin
類的相應屬性,接著獲取apk的絕對路徑進行保存,然后調用PackageParserCompat#parsePackage()
方法解析apk包中的AndroidManifest.xml
文件(如果對PackageParser解析APK不是很清楚的話那么可以參考文章PackageParser解析APK(上),PackageParser解析APK(下))將解析的結果保存在PackageParser.Package
類中,最后保存application meta-data
。代碼往下:
this.mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
this.mPackageInfo.signatures = this.mPackage.mSignatures;
this.mPackageInfo.packageName = this.mPackage.packageName;
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
}
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
這部分代碼比較簡單,主要是實例化一個PackageInfo
類,然后往里面屬性放進相應的值,大家應該一看就知道,不做贅述,我們直接來看下一部分代碼:
this.mPackageManager = new PluginPackageManager();
this.mPluginContext = new PluginContext(this);
this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
this.mResources = createResources(context, apk);
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
這里面有很多重要的方法,首先我們來說第一個實例化PluginPackageManager
類,這里主要要將插件中的PackageManager
替換成宿主程序的PackageManager
,這樣的話,就可以用宿主的PackageManager
來全權接管獲取插件中包apk的信息,因為解析插件apk用的是宿主的上下文對象。接著就是實例化PluginContext
類,這個類繼承ContextWrapper
,主要作用也是將插件中的上下文對象替換成這個PluginContext
類實例,然后里面的方法進行重寫,替換成我們自己構建的對象。然后就是獲取so文件的路徑。我們往下看這部分會看到createResources()
和createClassLoader()
方法,這兩個方法特別重要,我們這邊重點講解,首先來看createResources()
方法。
二.資源的加載
@WorkerThread
private static Resources createResources(Context context, File apk) {
if (Constants.COMBINE_RESOURCES) {
//如果插件資源合并到宿主里面去的情況,插件可以訪問宿主的資源
Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
ResourcesManager.hookResources(context, resources);
return resources;
} else {
//插件使用獨立的Resources,不與宿主有關系,無法訪問到宿主的資源
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
}
加載資源這塊,如果不清楚的話可以先看AssetManager加載資源過程,我們知道如果插件不調用宿主的資源的話,那么我們這里只要創建一個單獨的Resources
實例給插件即可,但是如果要想插件能訪問到宿主的資源的話,那么我們這里就得將資源添加到同一個AssetManager
中,我們跟進ResourcesManager#createResources()
方法:
public static synchronized Resources createResources(Context hostContext, String apk) {
Resources hostResources = hostContext.getResources();
Resources newResources = null;
AssetManager assetManager;
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
assetManager = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
} else {
assetManager = hostResources.getAssets();
}
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
}
if (isMiUi(hostResources)) {
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
} else if (isVivo(hostResources)) {
newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
} else if (isNubia(hostResources)) {
newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
} else if (isNotRawResources(hostResources)) {
newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
} else {
// is raw android resources
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
// lastly, sync all LoadedPlugin to newResources
for (LoadedPlugin plugin : pluginList) {
plugin.updateResources(newResources);
}
} catch (Exception e) {
e.printStackTrace();
}
return newResources;
}
這部分代碼也會長一點,而且涉及到了兼容性方面的問題,我們也拆開來看,首先我們來看下面這段代碼:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
assetManager = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
} else {
assetManager = hostResources.getAssets();
}
這里為啥要區別Android L之前的版本和之后的版本呢?這里主要是在Resources
對象最終是通過AssetManager
對象來獲取資源的,不過會先通過資源id查找到資源文件名。resources.arsc
包含了資源的id索引,但是在Android L之前,資源resources.arsc
的解析在前面已經解析完了,這樣的話,在addAssetPath
方法將插件資源加入到資源路徑列表里后,但是在resources.arsc
中并沒有插件資源的id索引,這樣會導致Resources
查找不到資源。所以滴滴這個插件化框架想出一個方法,就是構造一個新的 AssetManager
,將宿主和加載過的插件的所有 apk 全都添加一遍,然后再調用hookResources
方法將新的 Resources
替換回原來的(這個地方其實還有其他做法,但是這個地方不展開了)。
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
}
這段代碼就是將當前的插件已經加載過的插件的資源重新添加一遍。接著就是很蛋疼的代碼了,這應該也是寫這個框架當初疼了不少時間的問題:
if (isMiUi(hostResources)) {
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
} else if (isVivo(hostResources)) {
newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
} else if (isNubia(hostResources)) {
newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
} else if (isNotRawResources(hostResources)) {
newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
} else {
// is raw android resources
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
因為不同廠商都重寫了Resources
類,所以這個地方做一個適配。最后將各個插件的Resources
類進行更新即可。這樣我們的資源就加載完成了。
三.類加載
private static ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) {
File dexOutputDir = context.getDir(Constants.OPTIMIZE_DIR, Context.MODE_PRIVATE);
String dexOutputPath = dexOutputDir.getAbsolutePath();
DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
if (Constants.COMBINE_CLASSLOADER) {
try {
DexUtil.insertDex(loader);
} catch (Exception e) {
e.printStackTrace();
}
}
return loader;
}
如果對類加載不是非常清楚的,可以先看看ClassLoader及dex加載過程,這樣我們就知道為什么用DexClassLoader
這個類加載器來加載類了,這里也是一樣,如果要讓插件工程能調用宿主工程的類的話,那么就要將插件的dex和宿主的dex進行合并放在dexElements
中。我們來看看DexUtil#insertDex()
方法:
public static void insertDex(DexClassLoader dexClassLoader) throws Exception {
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(baseDexElements, newDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectUtil.setField(pathList.getClass(), pathList, "dexElements", allDexElements);
insertNativeLibrary(dexClassLoader);
}
這個類的作用就是將宿主的 DexPathList
中的dexElements
屬性值取出來,然后調用combineArray
將兩個數組進行合并,然后設置到宿主的dexElements
中去。最后我們看到又調用了insertNativeLibrary()
方法,這個方法是做什么的呢?
private static synchronized void insertNativeLibrary(DexClassLoader dexClassLoader) throws Exception {
if (sHasInsertedNativeLibrary) {
return;
}
sHasInsertedNativeLibrary = true;
Object basePathList = getPathList(getPathClassLoader());
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
List<File> nativeLibraryDirectories = (List<File>) ReflectUtil.getField(basePathList.getClass(),
basePathList, "nativeLibraryDirectories");
nativeLibraryDirectories.add(Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE));
Object baseNativeLibraryPathElements = ReflectUtil.getField(basePathList.getClass(), basePathList, "nativeLibraryPathElements");
final int baseArrayLength = Array.getLength(baseNativeLibraryPathElements);
Object newPathList = getPathList(dexClassLoader);
Object newNativeLibraryPathElements = ReflectUtil.getField(newPathList.getClass(), newPathList, "nativeLibraryPathElements");
Class<?> elementClass = newNativeLibraryPathElements.getClass().getComponentType();
Object allNativeLibraryPathElements = Array.newInstance(elementClass, baseArrayLength + 1);
System.arraycopy(baseNativeLibraryPathElements, 0, allNativeLibraryPathElements, 0, baseArrayLength);
Field soPathField;
if (Build.VERSION.SDK_INT >= 26) {
soPathField = elementClass.getDeclaredField("path");
} else {
soPathField = elementClass.getDeclaredField("dir");
}
soPathField.setAccessible(true);
final int newArrayLength = Array.getLength(newNativeLibraryPathElements);
for (int i = 0; i < newArrayLength; i++) {
Object element = Array.get(newNativeLibraryPathElements, i);
String dir = ((File)soPathField.get(element)).getAbsolutePath();
if (dir.contains(Constants.NATIVE_DIR)) {
Array.set(allNativeLibraryPathElements, baseArrayLength, element);
break;
}
}
ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryPathElements", allNativeLibraryPathElements);
} else {
File[] nativeLibraryDirectories = (File[]) ReflectUtil.getFieldNoException(basePathList.getClass(),
basePathList, "nativeLibraryDirectories");
final int N = nativeLibraryDirectories.length;
File[] newNativeLibraryDirectories = new File[N + 1];
System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
newNativeLibraryDirectories[N] = Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);
}
}
這個方法也比較長,我們看到這邊也有一個版本兼容的判斷,我們先來看看Android L之前的情況:
File[] nativeLibraryDirectories = (File[]) ReflectUtil.getFieldNoException(basePathList.getClass(),
basePathList, "nativeLibraryDirectories");
final int N = nativeLibraryDirectories.length;
File[] newNativeLibraryDirectories = new File[N + 1];
System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
newNativeLibraryDirectories[N] = Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);
這段代碼邏輯比較清晰,首先是獲取到DexPathList
類中的nativeLibraryDirectories屬性,這個屬性主要是存儲so文件的目錄,因為有可能會有多個,所以是個數組,這個方法主要是將插件的so文件路徑添加進nativeLibraryDirectories
,然后重新設置給DexPathList
中。同樣的如果Android L之后,前面是一樣的只不過有一段代碼不同:
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, 140 suppressedExceptions);
DexPathList
類中增加了一個屬性nativeLibraryPathElements
,具體的細節這里也就不做說明了,如果這塊有不明白的可以給私信。程序緊接著會調用 tryToCopyNativeLib(apk)
方法,就是將apk中的so文件拷貝到指定的位置。
四.動態注冊廣播
我們知道LoadedPlugin
構造函數會緊接著緩存instrumentations,activities,services,providers,receivers。這部分代碼比較簡單,我就直接跳過了。我們直接來講這段動態注冊廣播的代碼:
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {
receivers.put(receiver.getComponentName(), receiver.info);
try {
BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
this.mHostContext.registerReceiver(br, aii);
}
} catch (Exception e) {
e.printStackTrace();
}
}
我們知道,四大組件中的廣播有靜態注冊廣播和動態注冊廣播之分,我們這里可以采用動態注冊廣播的方法,將插件的廣播注冊到宿主的上下文Context中,這樣的話,就可以在插件中發送廣播,然后宿主可以接收得到了。所以相對于其他組件,廣播還是比較容易處理的。
五.創建插件Application
在PluginManager#loadPlugin()
方法的最后會調用LoadedPlugin#invokeApplication
方法來創建插件的Application,這部分代碼稍微簡單點,我們一起來瀏覽下:
public void invokeApplication() {
if (mApplication != null) {
return;
}
// make sure application's callback is run on ui thread.
RunUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
mApplication = makeApplication(false, mPluginManager.getInstrumentation());
}
}, true);
}
這個地方在ui線程又調用了makeApplication()
方法:
private Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
if (null != this.mApplication) {
return this.mApplication;
}
String appClass = this.mPackage.applicationInfo.className;
if (forceDefaultAppClass || null == appClass) {
appClass = "android.app.Application";
}
try {
this.mApplication = instrumentation.newApplication(this.mClassLoader, appClass, this.getPluginContext());
instrumentation.callApplicationOnCreate(this.mApplication);
return this.mApplication;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
這個方法是調用初始化時候hook掉的instrumentation
來創建插件工程的實現了Application
類的子類,然后調用它的onCreate()
方法。到這里,加載插件的代碼已經解析完畢了。
總結:加載插件的代碼我們已經分析完成了,如果有分析不完整的地方,大家可以提意見,我會補充或者進行解答,或者有錯誤大家可以提出來。下幾篇就會講關于四大組件啟動流程要做的一些處理了,希望大家一起進步。