幾個月前,騰訊開源了一款新的插件化框架Shadow。它的出現對於Android插件化的進程是十分重要的,由於隨着google對系統API限制愈來愈嚴格,市面上大多數插件框架終將被被淘汰。而Shadow重新的角度解決了這一難題。java
與市面上其餘插件框架相比,Shadow最明顯的兩個優勢:android
在分析以前,咱們先看一下官方對全動態插件框架的解釋。數據庫
這是一個在咱們長期接觸插件框架技術後就意識到的問題。也是咱們發現全部已知的插件框架沒有解決的問題。咱們將它稱爲全動態插件框架。全動態指的就是除了插件代碼以外,插件框架自己的全部邏輯代碼也都是動態的。實際上插件框架的代碼咱們是和插件打包在一塊兒發佈的。json
這個特性有多重要呢?實際上它比無Hack、零反射實現還要重要!由於有了這個特性以後,就算是咱們用了Hack的方案,須要兼容各類手機廠商的系統。咱們也不須要等宿主App更新才能解決問題。緩存
這個特性對於新研發的Shadow來講也尤其重要,由於新研發的東西確定有不少不完善的地方。若是是要打包在宿主裏發佈的話,那必然要測試的很是當心,還要寫大量過設計以知足將來的插件需求。可是,如今Shadow是不須要這樣的,Shadow只實現業務眼前所需的功能就能夠發佈了。app
上面這段話總結起來就是:全動態指的就是除了插件代碼以外,插件框架自己的全部邏輯代碼也都是動態的。Shadow將框架分爲四部分:Host、Manager、Loader和Runtime。其中除Host外,Manager、Loader、Runtime都是動態的。框架
Host:Host打包在宿主中,它負責兩件事情:1. 爲Manger、Loader、Runtime運行提供接口。2.加載Manager、Loader、Runtime等插件。它有兩個重要的類DynamicPluginManager和PluginProcessService。DynamicPluginManager的enter()方法是程序的入口,該方法實現了加載Manager的邏輯。PluginProcessService加載Loader和Runtime。ide
Manager:管理插件,包括插件的下載邏輯、入口邏輯、預加載邏輯等。反正就是一切尚未進入到Loader以前的全部事情。開源的部分主要包括插件的安裝(apk存放指定路徑、odex優化、解壓so庫等)。post
Loader:Loader是框架的核心部分。主要負責加載插、管理四大組件的生命週期、Application的生命週期等功能。不少插件框架只有Loader這部分功能。通常來講Loader是宿主和插件之間的橋樑。好比在以Hook系統API方式實現的插件框架中,只有在宿主中執行Loader中代碼才能Hook一些系統類,從而能夠成功加載插件。或者在以代理方式實現的插件框架中,也必須經過Loader加載插件才能完成四大組件的轉調。測試
Runtime:Runtime這一部分主要是註冊在AndroidManifest.xml中的一些殼子類。Shadow做者對這部分的描述是被迫動態化。緣由是宿主對合入代碼的增量要求極其嚴格,而殼子類會引入大量的方法增量,所以被迫把這部分作成動態化。這也迫使Shadow引入了整套方案中惟一一處Hook系統的API。這咱們下面再細說。
關於Manager、Loader、Runtime更多的介紹能夠看原做者的博客Shadow的全動態設計原理解析。
接下來咱們具體分析一下Manager、Loader、Runtime的動態化是如何實現的。直接上圖。
這張圖描述了Host、Manger、Loader、Runtime和Plugin的主要交互邏輯。整個過程我作了簡化。
在介紹Host時,我提到過整個框架的入口是DynamicPluginManager的enter()方法。咱們就從enter()開始分析Manager的動態化。enter()主要邏輯以下:
咱們先看一下前兩步的處理邏輯
final class ManagerImplLoader extends ImplLoader {
private static final String MANAGER_FACTORY_CLASS_NAME = "com.tencent.shadow.dynamic.impl.ManagerFactoryImpl";
PluginManagerImpl load() {
//1. 加載Manager插件。這個ClassLoader很神奇,沒徹底遵循雙親委派。只能經過parent加載白名單中的類。
ApkClassLoader apkClassLoader = new ApkClassLoader(
installedApk, //Manager插件APK。
getClass().getClassLoader(),
loadWhiteList(installedApk),//ClassLoader白名單,後面再西說
1
);
//支持Resource相關和ClassLoader,容許Manager插件使用資源。
Context pluginManagerContext = new ChangeApkContextWrapper(
applicationContext, //Applicaton
installedApk.apkFilePath,
apkClassLoader
);
try {
//2.反射獲得Manager中的類com.tencent.shadow.dynamic.impl.ManagerFactoryImpl的實例,調用buildManager生成PluginManagerImpl。
ManagerFactory managerFactory = apkClassLoader.getInterface(
ManagerFactory.class,
MANAGER_FACTORY_CLASS_NAME
);
return managerFactory.buildManager(pluginManagerContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
複製代碼
隨後調用PluginManagerImpl的enter()方法。這樣看來DynamicPluginManager只是一個代理類,真正的處理類是Manager插件中的PluginManagerImpl。
Runtime的加載過程是在PluginProcessService的loadRuntime中完成的。咱們看一下僞代碼。
public class PluginProcessService extends Service {
//與宿主通訊的代理對象
private UuidManager mUuidManager;
void loadRuntime(String uuid) throws FailedException {
//...省略代碼
//獲取Runtime的安裝信息
InstalledApk installedRuntimeApk = mUuidManager.getRuntime(uuid);
//加載Runtime.
boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
//...省略代碼
}
}
複製代碼
loadRuntime主要完成了兩件事情:
public class DynamicRuntime{
public static boolean loadRuntime(InstalledApk installedRuntimeApk) {
ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();
RuntimeClassLoader runtimeClassLoader = getRuntimeClassLoader();
if (runtimeClassLoader != null) {
String apkPath = runtimeClassLoader.apkPath;
if (TextUtils.equals(apkPath, installedRuntimeApk.apkFilePath)) {
//已經加載相同版本的runtime了,不須要加載
return false;
} else {
//版本不同,說明要更新runtime,先恢復正常的classLoader結構
recoveryClassLoader();
}
}
try {
//正常處理,將runtime 掛到pathclassLoader之上
hackParentToRuntime(installedRuntimeApk, contextClassLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
return true;
}
private static void hackParentToRuntime(InstalledApk installedRuntimeApk, ClassLoader contextClassLoader) throws Exception {
//RuntimeClassLoader加載Runtime插件。
RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(installedRuntimeApk.apkFilePath, installedRuntimeApk.oDexPath,
installedRuntimeApk.libraryPath, contextClassLoader.getParent());
//這是全框架中惟一一處反射系統API的地方。但這是Java層提供的API,相對來講風險較小。
Field field = getParentField();
if (field == null) {
throw new RuntimeException("在ClassLoader.class中沒找到類型爲ClassLoader的parent域");
}
field.setAccessible(true);
field.set(contextClassLoader, runtimeClassLoader);
}
}
複製代碼
loadRuntime用到了全框架中惟一一處反射調用系統的私有API,它的做用是將加載Runtime的RuntimeClassLoader掛載到系統的PathClassLoader之上。也就是將RuntimeClassLoader做爲PathClassLoader的父加載器。爲何這樣處理呢?由於Runtime主要是一些殼子類,例如殼子Activity。在系統啓動插件中的Activity時,實際上是啓動這些殼子Activity。這就要保證系統的PathClassLoader必須能找到殼子Activity。簡單的方式就是利用雙親委派模型,把PathClassLoader的父加載器設置成RuntimeClassLoader。
Loader的加載過程與Runtime同樣,也是在PluginProcessService中完成的。主要流程以下:
public class PluginProcessService extends Service {
private PluginLoaderImpl mPluginLoader;
private UuidManager mUuidManager;
void loadPluginLoader(String uuid) throws FailedException {
//獲取Loader的安裝信息
InstalledApk installedApk = mUuidManager.getPluginLoader(uuid);
//交給LoaderImplLoader.load()加載loader。
PluginLoaderImpl pluginLoader = new LoaderImplLoader().load(installedApk, uuid, getApplicationContext());
pluginLoader.setUuidManager(mUuidManager);
mPluginLoader = pluginLoader;
}
IBinder getPluginLoader() {
return mPluginLoader;
}
}
複製代碼
public class PpsController {
public IBinder getPluginLoader() throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
IBinder _result;
try {
_data.writeInterfaceToken(PpsBinder.DESCRIPTOR);
mRemote.transact(PpsBinder.TRANSACTION_getPluginLoader, _data, _reply, 0);
_reply.readException();
_result = _reply.readStrongBinder();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
複製代碼
LoaderImplLoader.load()中的邏輯與前面的ManagerImplLoader邏輯類似,我就不分析了。不過這裏再強調一下,LoaderImplLoader.load()返回的PluginLoaderImpl是一個Binder對象。咱們的Manager能夠經過PpsController.getPluginLoader()獲得其代理。
因爲Loader和Runtime的動態化,在發佈插件時,咱們還要發佈Loader和Runtime。Shadow的作法是將插件和Loader、Runtime打包到一個zip文件中。目錄結構是這樣子的。
除了以上文件外,咱們發現還有一個config.json文件,這個文件是在編譯期shadow自動生成的。它是對整個安裝包的描述。
{
"compact_version": [
1,
2,
3
],
"pluginLoader": { //Loader的描述
"apkName": "sample-loader-debug.apk", //指定哪一個APK是Loader。
"hash": "FF05A9CC0A80D9D0950B0EA9CB431B35" //文件的hash值,shadow根據這個值和已經安裝/加載的Loader插件對比,是否須要從新安裝/加載。
},
"plugins": [ //插件描述
{
"partKey": "sample-plugin-app",//
"apkName": "sample-plugin-app-debug.apk",
"businessName": "sample-plugin-app",
"hash": "FD6FF462B95540C1184A64370FCA090E"
}
],
"runtime": { //Runtime的描述
"apkName": "sample-runtime-debug.apk",
"hash": "AC2E39021DDFCBED5471B8739DDB3643"
},
"UUID": "E822E493-2E29-41BB-B1E6-28D58BEBB8AB",
"version": 4,
"UUID_NickName": "1.1.5"
}
複製代碼
我把整個安裝過程分爲已下幾步:
整個過程比較簡單,就不分析源碼了。oDex優化是把apk交給DexClassLoader處理就行了。so庫提取就是利用zip文件流,找到裏面的so文件,複製到相應的目錄。
宿主在與插件交互時,例如啓動插件Activity,要先加載插件。插件的加載是Manager中的PluginLoader經過Binder通訊向Loader中的PluginLoaderBinder發送消息。PluginLoadersBinder收到消息後,委託給PluginLoaderImpl處理。
插件加載其實就是爲插件四大組件準備運行環境。整個過程分爲:
咱們直接看Loader中的實現,具體的實現邏輯在LoadPluginBloc中。
object LoadPluginBloc {
fun loadPlugin( executorService: ExecutorService, abi: String, commonPluginPackageManager: CommonPluginPackageManager, componentManager: ComponentManager, lock: ReentrantLock, pluginPartsMap: MutableMap<String, PluginParts>, hostAppContext: Context, installedApk: InstalledApk, loadParameters: LoadParameters, remoteViewCreatorProvider: ShadowRemoteViewCreatorProvider? ): Future<*> {
if (installedApk.apkFilePath == null) {
throw LoadPluginException("apkFilePath==null")
} else {
//利用ClassLoader加載APk。
val buildClassLoader = executorService.submit(Callable {
lock.withLock {
LoadApkBloc.loadPlugin(installedApk, loadParameters, pluginPartsMap)
}
})
//利用packageManager獲取插件信息。
val getPackageInfo = executorService.submit(Callable {
val archiveFilePath = installedApk.apkFilePath
val packageManager = hostAppContext.packageManager
val packageArchiveInfo = packageManager.getPackageArchiveInfo(
archiveFilePath,
PackageManager.GET_ACTIVITIES
or PackageManager.GET_META_DATA
or PackageManager.GET_SERVICES
or PackageManager.GET_PROVIDERS
or PackageManager.GET_SIGNATURES
)
?: throw NullPointerException("getPackageArchiveInfo return null.archiveFilePath==$archiveFilePath")
packageArchiveInfo
})
//建立存儲插件信息的PluginPackageManager。
val buildPackageManager = executorService.submit(Callable {
val packageInfo = getPackageInfo.get()
val pluginInfo = ParsePluginApkBloc.parse(packageInfo, loadParameters, hostAppContext)
PluginPackageManager(commonPluginPackageManager, pluginInfo)
})
//建立查找插件資源的Resources。
val buildResources = executorService.submit(Callable {
val packageInfo = getPackageInfo.get()
CreateResourceBloc.create(packageInfo, installedApk.apkFilePath, hostAppContext)
})
//爲當前插件建立Application。
val buildApplication = executorService.submit(Callable {
val pluginClassLoader = buildClassLoader.get()
val pluginPackageManager = buildPackageManager.get()
val resources = buildResources.get()
val pluginInfo = pluginPackageManager.pluginInfo
CreateApplicationBloc.createShadowApplication(
pluginClassLoader,
pluginInfo.applicationClassName,
pluginPackageManager,
resources,
hostAppContext,
componentManager,
remoteViewCreatorProvider
)
})
//將以上內容保存在緩存中。
val buildRunningPlugin = executorService.submit {
if (File(installedApk.apkFilePath).exists().not()) {
throw LoadPluginException("插件文件不存在.pluginFile==" + installedApk.apkFilePath)
}
val pluginPackageManager = buildPackageManager.get()
val pluginClassLoader = buildClassLoader.get()
val resources = buildResources.get()
val pluginInfo = pluginPackageManager.pluginInfo
val shadowApplication = buildApplication.get()
lock.withLock {
componentManager.addPluginApkInfo(pluginInfo)
pluginPartsMap[pluginInfo.partKey] = PluginParts(
shadowApplication,
pluginClassLoader,
resources,
pluginInfo.businessName
)
PluginPartInfoManager.addPluginInfo(pluginClassLoader, PluginPartInfo(shadowApplication, resources,
pluginClassLoader, pluginPackageManager))
}
}
return buildRunningPlugin
}
}
}
複製代碼
插件中的ClassLoader必須能夠加載宿主和其它插件中的類,這樣插件才能與宿主或其餘插件交互。
在Shadow中,插件只能加載白名單中配置了的宿主類。咱們看一下具體的實現方式。
class PluginClassLoader(
private val dexPath: String,
optimizedDirectory: File?,
private val librarySearchPath: String?,
parent: ClassLoader,
private val specialClassLoader: ClassLoader?,
hostWhiteList: Array<String>?
) : BaseDexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) {
}
複製代碼
咱們須要關注三個參數:parent、specialClassLoader、hostWhiteList。在只考慮宿主和插件交互的狀況下,specialClassLoader是宿主PathClassLoader的父加載器。parent是宿主PathClassLoader。hostWhiteList表示插件能夠加載宿主類的白名單。具體的加載邏輯以下:
@Throws(ClassNotFoundException::class)
override fun loadClass(className: String, resolve: Boolean): Class<*> {
//在白名單中類直接走雙親委派
if (specialClassLoader == null || className.startWith(allHostWhiteList)) {
return super.loadClass(className, resolve)
} else {
var clazz: Class<*>? = findLoadedClass(className)
if (clazz == null) {
clazz = findClass(className)!!
if (clazz == null) {
clazz = specialClassLoader.loadClass(className)!!
}
}
return clazz
}
}
複製代碼
在介紹安裝過程時,我說過shadow的安裝包中有一個config.json文件。該文件對插件的描述其實還有一個dependsOn字段,用來表示依賴的其餘插件的。shadow在加載插件時,會先判斷它所依賴的插件是否已經加載。若是依賴的插件已經所有加載,則把加載這些插件的PathClassLoader組裝到一個CombineClassLoader中。這個CombineClassLoader就是當前插件PathClassLoader的父加載器。若是有依賴的插件沒有加載,則拋出異常。所以這就要求咱們熟悉插件間的調用關係,在加載插件時,先加載其依賴插件。
@Throws(LoadApkException::class)
fun loadPlugin(installedApk: InstalledApk, loadParameters: LoadParameters, pluginPartsMap: MutableMap<String, PluginParts>): PluginClassLoader {
val apk = File(installedApk.apkFilePath)
val odexDir = if (installedApk.oDexPath == null) null else File(installedApk.oDexPath)
val dependsOn = loadParameters.dependsOn
//Logger類必定打包在宿主中,所在的classLoader即爲加載宿主的classLoader
val hostClassLoader: ClassLoader = Logger::class.java.classLoader!!
val hostParentClassLoader = hostClassLoader.parent
if (dependsOn == null || dependsOn.isEmpty()) {
return PluginClassLoader(
apk.absolutePath,
odexDir,
installedApk.libraryPath,
hostClassLoader,
hostParentClassLoader,
loadParameters.hostWhiteList
)
} else if (dependsOn.size == 1) {
val partKey = dependsOn[0]
val pluginParts = pluginPartsMap[partKey]
if (pluginParts == null) {
throw LoadApkException("加載" + loadParameters.partKey + "時它的依賴" + partKey + "尚未加載")
} else {
return PluginClassLoader(
apk.absolutePath,
odexDir,
installedApk.libraryPath,
pluginParts.classLoader,
null,
loadParameters.hostWhiteList
)
}
} else {
val dependsOnClassLoaders = dependsOn.map {
val pluginParts = pluginPartsMap[it]
if (pluginParts == null) {
throw LoadApkException("加載" + loadParameters.partKey + "時它的依賴" + it + "尚未加載")
} else {
pluginParts.classLoader
}
}.toTypedArray()
val combineClassLoader = CombineClassLoader(dependsOnClassLoaders, hostParentClassLoader)
return PluginClassLoader(
apk.absolutePath,
odexDir,
installedApk.libraryPath,
combineClassLoader,
null,
loadParameters.hostWhiteList
)
}
}
複製代碼
val getPackageInfo = executorService.submit(Callable {
val archiveFilePath = installedApk.apkFilePath
val packageManager = hostAppContext.packageManager
val packageArchiveInfo = packageManager.getPackageArchiveInfo(
archiveFilePath,
PackageManager.GET_ACTIVITIES
or PackageManager.GET_META_DATA
or PackageManager.GET_SERVICES
or PackageManager.GET_PROVIDERS
or PackageManager.GET_SIGNATURES
)
?: throw NullPointerException("getPackageArchiveInfo return null.archiveFilePath==$archiveFilePath")
packageArchiveInfo
})
複製代碼
這裏就是調用了PackageManager.getPackageArchiveInfo(),將插件APK保存的路徑傳遞過去。
val buildPackageManager = executorService.submit(Callable {
//獲取插件信息
val packageInfo = getPackageInfo.get()
//解析成咱們本身的PluginInfo。
val pluginInfo = ParsePluginApkBloc.parse(packageInfo, loadParameters, hostAppContext)
//構建PluginPackManager
PluginPackageManager(commonPluginPackageManager, pluginInfo)
})
複製代碼
整個過程很簡單,直接看PluginPackageManager。
class PluginPackageManager(val commonPluginPackageManager: CommonPluginPackageManager,
val pluginInfo: PluginInfo) : PackageManager() {
override fun getApplicationInfo(packageName: String?, flags: Int): ApplicationInfo {
val applicationInfo = ApplicationInfo()
applicationInfo.metaData = pluginInfo.metaData
applicationInfo.className = pluginInfo.applicationClassName
return applicationInfo
}
override fun getPackageInfo(packageName: String?, flags: Int): PackageInfo? {
if (pluginInfo.packageName == packageName) {
val info = PackageInfo()
info.versionCode = pluginInfo.versionCode
info.versionName = pluginInfo.versionName
info.signatures = pluginInfo.signatures
return info;
}
return null;
}
override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo {
val find = pluginInfo.mActivities.find {
it.className == component.className
}
if (find == null) {
return commonPluginPackageManager.getActivityInfo(component, flags)
} else {
return find.activityInfo
}
}
override fun getProviderInfo(component: ComponentName?, flags: Int): ProviderInfo {
ImplementLater()
}
override fun getReceiverInfo(component: ComponentName?, flags: Int): ActivityInfo {
ImplementLater()
}
}
複製代碼
PluginPackageManager繼承自PackageManager,返回當前插件的相關信息。關於這部分請看做者的博客Shadow對PackageManager的處理方法。
object CreateResourceBloc {
fun create(packageArchiveInfo: PackageInfo, archiveFilePath: String, hostAppContext: Context): Resources {
val packageManager = hostAppContext.packageManager
packageArchiveInfo.applicationInfo.publicSourceDir = archiveFilePath
packageArchiveInfo.applicationInfo.sourceDir = archiveFilePath
try {
return packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo)
} catch (e: PackageManager.NameNotFoundException) {
throw RuntimeException(e)
}
}
}
複製代碼
將插件apk的路徑賦值給publicSourceDir和sourceDir,利用PackageManager.getResourcesForApplication()建立一個新的Resources。
Shadow在處理插件Application時,採用了字節碼技術,在編譯期將繼承的系統Application替換成了ShadowApplication。ShadowApplication就是一個繼承了Context的普通類。
替換邏輯在ApplicationTransform中,經過下面代碼咱們能夠看到Shadow插件將android.app.Application替換成了com.tencent.shadow.core.runtime.ShadowApplication。class ApplicationTransform : SimpleRenameTransform(
mapOf(
"android.app.Application"
to "com.tencent.shadow.core.runtime.ShadowApplication"
,
"android.app.Application\$ActivityLifecycleCallbacks"
to "com.tencent.shadow.core.runtime.ShadowActivityLifecycleCallbacks"
)
)
複製代碼
object CreateApplicationBloc {
@Throws(CreateApplicationException::class)
fun createShadowApplication( pluginClassLoader: PluginClassLoader, appClassName: String?, pluginPackageManager: PluginPackageManager, resources: Resources, hostAppContext: Context, componentManager: ComponentManager, remoteViewCreatorProvider: ShadowRemoteViewCreatorProvider? ): ShadowApplication {
try {
val shadowApplication : ShadowApplication;
shadowApplication = if (appClassName != null) {//若是appClassName存在,表示插件聲明瞭Application,反射生成ShadowApplication。
val appClass = pluginClassLoader.loadClass(appClassName)
ShadowApplication::class.java.cast(appClass.newInstance())
} else {
object : ShadowApplication(){}//插件沒有聲明Application,直接new一個ShadowApplication。
}
val partKey = pluginPackageManager.pluginInfo.partKey
shadowApplication.setPluginResources(resources) //添加Resources
shadowApplication.setPluginClassLoader(pluginClassLoader) //添加ClassLoader
shadowApplication.setPluginComponentLauncher(componentManager) //添加ComponentManager,管理四大組件用的。
shadowApplication.setHostApplicationContextAsBase(hostAppContext) //添加宿主的Application。
shadowApplication.setBroadcasts(componentManager.getBroadcastsByPartKey(partKey)) //添加BroadcastReceiver。
shadowApplication.setLibrarySearchPath(pluginClassLoader.getLibrarySearchPath())
shadowApplication.setDexPath(pluginClassLoader.getDexPath())//插件APK路徑。
shadowApplication.setBusinessName(pluginPackageManager.pluginInfo.businessName)
shadowApplication.setPluginPartKey(partKey)
shadowApplication.remoteViewCreatorProvider = remoteViewCreatorProvider
return shadowApplication
} catch (e: Exception) {
throw CreateApplicationException(e)
}
}
}
複製代碼
這裏就直接看註釋吧。