對於App而言,所謂的插件化,我的的理解就是把一個完整的App拆分紅宿主和插件兩大部分,咱們在宿主app運行時能夠動態的載入或者替換插件的部分,插件不只是對宿主功能的擴展並且還能減少宿主的負擔,所謂的宿主就是運行的app,插件即宿主運行時加載的apk文件,這樣宿主和插件結合的方案技術大概就是插件化了吧。android
關於插件化主要解決的大概就是類加載、資源加載、組件的加載這些核心問題了吧,所謂的原理也就是圍繞這些問題進行的探討。git
android中的類加載系統的ClassLoader能夠大體劃分爲BaseDexClassLoader,SecureClassLoader。做爲插件化咱們只簡單分析一下PathClassLoader與DexClassLoader,畢竟類加載的內容也不少,要寫的東西也不少😝,先看下android類加載繼承關係圖:github
Provides a simple ClassLoader implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).windows
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application. Prior to API level 26, this class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir() to create such a directory:api
以上關於android的類加載只是輕描淡寫了一下,說了半天,關於插件化固然用到了DexClassLoader,咱們來看一下DexClassLoader的實現吧。數組
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
複製代碼
dexPath: 字符串變量,包含類和資源的jar/apk文件列表,用File.pathSeparator分隔,在Android上默認爲「:」。
optimizedDirectory:不推薦使用此參數,貌似是一個廢棄的參數,聽說是.dex文件的解壓路徑,自API級別26起再也不生效,那麼26以前是怎麼用的呢,查了一下是經過 context.getCodeCacheDir()。
librarySearchPath: 包含native庫的目錄列表,C/C++庫存放的路徑,用File.pathSeparator分隔;可能爲null。
parent: 父類加載器ClassLoader.瀏覽器
再看一下調用的父類BaseDexClassLoader的構造方法及核心方法緩存
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
複製代碼
顯然看出DexPathList這個成員對象的重要性,初始化構造方法的時候實例化DexPathList對象,同時,BaseDexClassLoader重寫了父類findClass()方法,經過該方法進行類查找的時候,會委託給pathList對象的findClass()方法進行相應的類查找,下面繼續查看DexPathList類的findClass方法:安全
final class DexPathList {
private Element[] dexElements;
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
...
}
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
return elements;
}
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}
複製代碼
DexPathList構造方法被調用的時候其實就是經過makeDexElements方法把dexPath進行遍歷,依次加載每一個dex文件,而後經過數組Element[]存放,而在DexPathList類的findClass調用的時候,經過遍歷Element[]的dex文件,在經過DexFile類的loadClassBinaryName()來加載類,若是不爲空那麼表明加載成功,而且返回class,不然返回null。
下面再來看一下基類的ClassLoader是如何實現的吧性能優化
public abstract class ClassLoader {
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
複製代碼
這明顯就是一個雙親委派模型,在類加載的時候,首先去查找這個類以前是否已經被加載過,若是加載過直接返回,不然委託父類加載器去查找,若是父類加載器找不到那麼就去系統的BootstrapClass去查找,到最後仍是找不到的話,那麼就本身親自上陣查找了。這樣就避免了重複加載,實現了更加安全。
好了總結一下DexClassLoader的加載過程:loadClass->findClass->BaseDexClassLoader.findClass->DexPathList.findClass->loadDexFile->DexFile.loadClassBinaryName->DexFile.defineClass,大致上就這樣麼個過程吧。
Android系統加載資源都是經過Resource資源對象來進行加載的,所以只須要添加資源(即apk文件)所在路徑到AssetManager中,便可實現對插件資源的訪問。
/**
* Create a new AssetManager containing only the basic system assets.
* Applications will not generally use this method, instead retrieving the
* appropriate asset manager with {@link Resources#getAssets}. Not for
* use by applications.
* @hide
*/
public AssetManager() {
final ApkAssets[] assets;
synchronized (sSync) {
createSystemAssetsInZygoteLocked();
assets = sSystemApkAssets;
}
mObject = nativeCreate();
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(hashCode());
}
// Always set the framework resources.
setApkAssets(assets, false /*invalidateCaches*/);
}
複製代碼
不難發現AssetManager的構造方法是@hide隱藏的api,因此不能直接使用,這裏確定是須要經過反射啦,不過有人說Android P不是對系統的隱藏Api作出了限制,所以插件化估計要涼涼,可是我想說如今一些主流的插件化技術基本都已經適配了Android9.0了,因此無需擔憂。下面先簡單貼出Android資源的加載流程。關於插件化的資源加載能夠參考下滴滴VirtualApk資源的加載思想 (傳送門)
class ContextImpl extends Context {
//...
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration) {
//....
Resources resources = packageInfo.getResources(mainThread);
//....
}
//...
}
複製代碼
這裏不去關注packageInfo是如何生成的,直接跟蹤到下面去.
public final class LoadedApk {
private final String mResDir;
public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
CompatibilityInfo compatInfo, ClassLoader baseLoader,
boolean securityViolation, boolean includeCode, boolean registerPackage) {
final int myUid = Process.myUid();
aInfo = adjustNativeLibraryPaths(aInfo);
mActivityThread = activityThread;
mApplicationInfo = aInfo;
mPackageName = aInfo.packageName;
mAppDir = aInfo.sourceDir;
mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
// 注意一下這個sourceDir,這個是咱們宿主的APK包在手機中的路徑,宿主的資源經過此地址加載。
// 該值的生成涉及到PMS,暫時不進行分析。
// Full path to the base APK for this application.
//....
}
//....
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
//....
}
複製代碼
進入到ActivityThread.getTopLevelResources()的邏輯中
public final class ActivityThread {
Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
//咱們暫時只關注下面這一段代碼
AssetManager assets = new AssetManager();
if (assets.addAssetPath(resDir) == 0) { //此處將上面的mResDir,也就是宿主的APK在手機中的路徑當作資源包添加到AssetManager裏,則Resources對象能夠經過AssetManager查找資源,此處見(老羅博客:Android應用程序資源的查找過程分析)
return null;
}
// 建立Resources對象,此處依賴AssetManager類來實現資源查找功能。
r = new Resources(assets, metrics, getConfiguration(), compInfo);
}
}
複製代碼
從上面的代碼中咱們知道了咱們經常使用的Resources是如何生成的了,那麼理論上插件也就按照如此方式生成一個Resources對象給本身用就能夠了。
這個其實不能一律而論,由於Android擁有四大組件,分別爲Activity、Service、ContentProvider、BoradCastRecevier,每一個組件的屬性及生命週期也不同,因此關於插件中加載的組件就須要分別研究每一個組件是如何加載的了。
簡單拿Activity組件來講,如今一些主流的方式基本上都是經過「坑位」的思想,這個詞最先聽說也是來源於360,總的來講,先佔坑,由於咱們宿主app的Manifest中是不會去申請插件中的Activity的,那我就先佔一個坑,欺騙系統,而後替換成插件中的Activity。這裏可能須要多個坑位,由於一些資源屬性都是能夠動態配置的。好比launchMode、process、configChanges、theme等等。
這裏還須要瞭解一下Activity的啓動流程,這裏咱們能夠簡單看一下。
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
複製代碼
能夠看出,咱們平時startActivity其實都是經過調用startActivityForResult(),咱們接下來繼續看
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
複製代碼
咱們能夠看到是經過系統的Instrumentation這個類execStartActivity()來執行啓動Activity的,咱們繼續能夠看到下面的這個方法:
public ActivityResult execStartActivity(
、、、、、
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
/**
* @hide
*/
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
複製代碼
ActivityManager.getService()拿到IActivityManager對象,而後就去調用startActivity()了,而IActivityManager只是一個抽象接口,下面看看它的實現類
public abstract class ActivityManagerNative extends Binder implements IActivityManager
public final class ActivityManagerService extends ActivityManagerNative
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback
class ActivityManagerProxy implements IActivityManager
複製代碼
能夠看到它的兩個實現類ActivityManagerProxy與ActivityManagerService,簡稱AMP與AMS,AMP只是AMS的本地代理對象,其startActivity方法會調用到AMS的startActivity方法。並且要注意,這個startActivity方法會把ApplicationThread對象傳遞到AMS所在進程,固然AMS拿到的其實是ApplicationThread的代理對象ApplicationThreadProxy,AMS就要經過這個代理對象與咱們的App進程進行通訊。
既然Activity是否存在的校驗是發生在AMS端,那麼咱們在與AMS交互前,提早將Activity的ComponentName進行替換爲佔坑的名字,選擇hook Instrumentation或者ActivityManagerProxy應該都是能夠的,而後Activity通過複雜的啓動流程後最終會執行Instrumentation的newActivity(),這裏咱們能夠進行還原成插件的Activity。
public Activity newActivity(Class<?> clazz, Context context,
IBinder token, Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
Object lastNonConfigurationInstance) throws InstantiationException,
IllegalAccessException {
Activity activity = (Activity)clazz.newInstance();
ActivityThread aThread = null;
// Activity.attach expects a non-null Application Object.
if (application == null) {
application = new Application();
}
activity.attach(context, aThread, this, token, 0 /* ident */, application, intent,
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new Configuration(), null /* referrer */, null /* voiceInteractor */,
null /* window */, null /* activityConfigCallback */);
return activity;
}
複製代碼
關於插件化四大組件的加載原理過於複雜,我只簡單的描述了一下插件化的思想,若是想看具體的思想流程,也能夠查看滴滴VirtualApk的組件加載原理,插件化思想都有共通之處(傳送門)
若是你在作插件化,或者想去研究插件化,上面看不懂沒有關係,反正市場上已經擁有很是多的成熟方案,下面是從萬千的方案中挑取較好的幾個方案,以避免走更多的彎路,畢竟我也是從茫茫的插件化方案中走了一遭。
VirtualAPK對插件沒有額外的約束,原生的apk便可做爲插件。插件工程編譯生成apk後,便可經過宿主App加載,每一個插件apk被加載後,都會在宿主中建立一個單獨的LoadedPlugin對象。以下圖所示,經過這些LoadedPlugin對象,VirtualAPK就能夠管理插件並賦予插件新的意義,使其能夠像手機中安裝過的App同樣運行。
第一步: 宿主Project的build.gradle添加
dependencies {
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}
複製代碼
第二步:宿主的Moudle的build.gradle添加
apply plugin: 'com.didi.virtualapk.host'
implementation 'com.didi.virtualapk:core:0.9.8'
複製代碼
第三步:宿主app的Applicaiton中添加初始化:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
複製代碼
第四步:增長混淆:
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
複製代碼
第五步:宿主的使用:
String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/Test.apk");
File plugin = new File(pluginPath);
PluginManager.getInstance(base).loadPlugin(plugin);
// Given "com.didi.virtualapk.demo" is the package name of plugin APK,
// and there is an activity called `MainActivity`.
Intent intent = new Intent();
intent.setClassName("com.didi.virtualapk.demo", "com.didi.virtualapk.demo.MainActivity");
startActivity(intent);
複製代碼
第六步:插件的Project的build.gradle配置:
dependencies {
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}
複製代碼
第七步: 插件app的build.gradle配置:
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk {
packageId = 0x6f // The package id of Resources.
targetHost='source/host/app' // The path of application module in host project.
applyHostMapping = true // [Optional] Default value is true.
}
複製代碼
第八步:關於編譯運行命令
宿主:gradlew clean assembleRelease
插件:gradlew clean assemblePlugin
複製代碼
關於android的插件化簡單研究大概就是醬樣子了,初次嚐鮮感受仍是蠻不錯的,可是最大的苦惱應該是業務插件該如何拆分了,基礎組件如何拆分,如何從複雜的荊棘業務中殺出一條血路,想要「萬花叢中過,片葉不沾身」,騷年,我相信你能夠的。