插件化原理(small)
ClassLoader
DexClassLoader 和 PathClassLoader
android 中的calssloader,区别在于DexClassLoader多了一个optimize的优化目录,其可以加载外部的dex,zip,so等包,而pathclassloader只能加载内部的dex,apk等包
而两个都是继承自BaseDexClassLoader ,而BaseDexClassLoader的主要工作是交给DexPathList是做,接下来让我们看看这个DexPathList的构造方法
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
dexPath就是我们需要加载插件的路径,可以看到主要是由makeDexElements这个方法实现
private static Element[] makeDexElements(ArrayList files,
File optimizedDirectory) {
ArrayList elements = new ArrayList();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* IOException might get thrown "legitimately" by
* the DexFile constructor if the zip file turns
* out to be resource-only (that is, no
* classes.dex file in it). Safe to just ignore
* the exception here, and let dex == null.
*/
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
该方法返回的是一个element的数组。
看到这里,我们先不用深入理解makeDexElements内部的逻辑实现,先思考一个问题,何为插件化?
我们做插件的目的是有很多种,例如:减少包体积,热更新 。。。
插件化意味着宿主和插件之间能够进行通信,宿主可以调用插件里的对象,宿主可以访问插件里的资源等等。
所以每个BaseDexClassLoader构造完之后都会有一个dexElements,这就说明宿主的classloader有一个,我们插件内部自己的classloader也会有一个,说到这里已经说明插件化类访问的原理了。其核心就是分为以下步骤:
-
- 宿主的classloader通过反射拿到内部的dexPathList数组
-
- 构造一个我们插件的DexClassLoader(而不是PathClassLoader),然后通过反射拿到其中的dexPathList数组
-
- 将两个数组进行合并,然后通过反射设置会宿主的classloader中
事实上,Android官方的multidex就是这个原理。完成这些步骤以后,我们在宿主中就可以调用插件的类了,但是工作还没完,资源如何访问?
Resources
设想一个问题,我们将两个dexPathList进行了合并,此时宿主可以调用插件,但是假设插件内部根据一个id查找一个资源,会报ResourcesNotFind的异常,为什么呢?我们来看看源码,假设当前处在插件中的某个activity,根据id获取获取某个drawable并设置进
imageview中
imageview.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher_background))
最终回到ContextThemeWrapper中:
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
//mResouces为空
if (mResources == null) {
if (mOverrideConfiguration == null) {
//将会调用super.getResources()
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
而super.getReources最终实现是ContextImpl.getResources()中
而ContextImpl是在ActivityThread中由系统执行各个步骤时创建的,我们插件化的activity根本不会走这样一套流程(如果走这套流程的话,插件化就毫无意义啦~~)
所以,拿到的ContextImpl则是宿主的。而这个ComtextImpl在Application到Activity的各个阶段都会有所区别
具体在于,Application的mBase成员是通过ContextImpl.createAppContext经过attachBaseContext后创建的,而Activity的mBase成员是通过ContextImpl.createActivityContext创建的,两者的区别有兴趣可以阅读下源码
无论以哪种方式,最终都会来到ResourcesManager.getOrCreateResources()方法创建资源对象,
而经过层层判断之后,又会来到createResourcesImpl()方法,
而createResourcesImpl()内部
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
而createAssetManager()方法:
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (key.mResDir != null) {
//看到这里大概猜到为什么插件无法访问资源了
if (assets.addAssetPath(key.mResDir) == 0) {
...
return null;
}
}
...
...
return assets;
}
原来assets.addAssetPath()方法是把key.mResDir加进去assetmanager中,这样就可以访问到资源,mResDir就是res文件
至此我们终于知道为啥在插件访问不到资源了。
看到这里有两个实现方法
- 在插件的Activity重写getResources方法,然后根据重新创建一个AssetManager,这样插件内的资源可能通过自己的AssetManager进行资源
- 通过反射拿到宿主的AssetManager,然后调用内部addAssetPath()将当前插件的路径传进去,相当于进行资源的合并。
这两种方法都可行,第一种会导致资源爆炸,宿主一份,插件一份,而且这里面的资源无法公用。第二种则会导致资源id冲突,但是可以通过某些手段进行控制(比如控制分配id的段达到防止资源id冲突)
而small用的是第二种,并且配合gradle介入资源id段(PP)的分配情况
具体原理则是:在gradle执行到mergeAndroidResources这个task时,将R.java,R.txt替换为small extention中配置的packageId字段,并且替换完成后,重写整个resources.arsc文件,将原来的arsc文件里面的索引的id替换成配置后的id。如原来生成的id为0x7F010001 替换成自定义 0x21010001
替换资源id这个方法,除了上述这个之外,还可以手动修改AAPT的源码,然后重新编译一个aapt工具
至此,资源也可以访问了。
四大组件
类和资源都可以访问了,我们都知道四大组件要在宿主的AndroidManifest.xml中注册才可以使用,否则会提示找不到该component。以activity为例,如果将插件中的activity在宿主AndroidManifest中注册,那插件化将毫无意义,因为每次有新activity都需要更新宿主,插件的思想也就无从谈起。
有什么办法可以做到不在宿主中注册也可以调用呢?
Small使用hook,主要是hook住Instrumentation,ActivityThread和mH这几个类。
App创建过程 说过 Instrumentation最初的目的是为了给UI测试预留的接口,没想到可以被插件化玩出花样来,可能谷歌一开始也没想到。
步骤如下:
- Step 1
public static void hookInstrumentation() {
try {
Class at = Class.forName("android.app.ActivityThread");
Method atMethod = at.getDeclaredMethod("currentActivityThread",null);
atMethod.setAccessible(true);
Object activityThread = atMethod.invoke(null,null
);
Field instruFiled = at.getDeclaredField("mInstrumentation");
instruFiled.setAccessible(true);
Instrumentation instrumentation = (Instrumentation) instruFiled.get(activityThread);
TestInstrumentationWrapper wrapper = new TestInstrumentationWrapper(instrumentation);
instruFiled.set(activityThread, wrapper);
Log.d(TAG,"hook init success");
} catch (Throwable e) {
e.printStackTrace();
}
}
private static class TestInstrumentationWrapper extends Instrumentation {
private Instrumentation mBase;
public TestInstrumentationWrapper(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent
intent,
int requestCode, Bundle options) {
Log.d(TAG, "TestInstrumentationWrapper hook 1");
//step1
return realExecStartActivity1(who, contextThread, token, target, intent, requestCode, options);
}
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent,
int requestCode, Bundle options) {
Log.d(TAG, "TestInstrumentationWrapper hook 2");
return realExecStartActivity2(who, contextThread, token, target, intent, requestCode, options);
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String resultWho,
Intent intent, int requestCode, Bundle options, UserHandle user) {
Log.d(TAG, "TestInstrumentationWrapper hook 3");
return realExecStartActivity3(who,contextThread,token,resultWho,intent,requestCode,options,user);
}
@SuppressWarnings("NewApi")
private ActivityResult realExecStartActivity3(Context who, IBinder contextThread, IBinder token, String resultWho,
Intent intent, int requestCode, Bundle options, UserHandle user) {
ActivityResult activityResult = null;
try {
Class c = mBase.getClass();
Method execStartActivity = c.getDeclaredMethod("execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
String.class,
Intent.class,
int.class,
Bundle.class,
UserHandle.class
);
activityResult = (ActivityResult) execStartActivity.invoke(mBase,
who,
contextThread,
token,
resultWho,
intent,
requestCode,
options,
user
);
} catch (Exception e) {
e.printStackTrace();
}
return activityResult;
}
private ActivityResult realExecStartActivity2(Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
ActivityResult activityResult = null;
try {
Class c = mBase.getClass();
Method execStartActivity = c.getDeclaredMethod("execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
String.class,
Intent.class,
int.class,
Bundle.class
);
activityResult = (ActivityResult) execStartActivity.invoke(mBase,
who,
contextThread,
token,
target,
intent,
requestCode,
options
);
} catch (Exception e) {
e.printStackTrace();
}
return activityResult;
}
private ActivityResult realExecStartActivity1(Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
ActivityResult activityResult = null;
try {
Class c = mBase.getClass();
Method execStartActivity = c.getDeclaredMethod("execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
Activity.class,
Intent.class,
int.class,
Bundle.class
);
activityResult = (ActivityResult) execStartActivity.invoke(mBase,
who,
contextThread,
token,
target,
intent,
requestCode,
options
);
} catch (Exception e) {
e.printStackTrace();
}
return activityResult;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
//step2
//在这里实例化插件的activity
return super.newActivity(cl, className, intent);
}
// on Applicaiton
class App : Application(){
override fun onCreate() {
super.onCreate()
HookUtil.hookInstrumentation()
}
}
在MainActivity中通过intent启动一个TestActivity,运行结果:
2019-03-23 15:36:09.820 6537-6537/com.example.simpleapp D/Hook: hook init success
2019-03-23 15:36:13.068 6537-6537/com.example.simpleapp D/Hook: TestInstrumentationWrapper hook 1
此时我们已经hook住了startActivity过程,那么我可以在宿主中占坑一个ProxyActivity,在启动插件activity的过程中,重定向至ProxyActivity达到偷梁换柱的目的。
step2:
有去有回,经过上面已经可以做到将插件的activity换了个皮变成宿主中的ProxyActivity,但是怎么将这个ProxyActivity换回来呢?
这涉及app启动流程,主要是本地app进程(ActivityThread)和系统SystemServer进程(ActivityManagerService)进行binder通信的过程,有兴趣的看下之前写过的 一篇文章 分析app创建流程
现在我们只需要知道,Context.startActivity()最终会来到
ActivityStackSupervisor.realStartActivityLocked()
final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
boolean andResume, boolean checkConfig) throws RemoteException {
...
app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
System.identityHashCode(r), r.info,
// TODO: Have this take the merged configuration instead of separate global
// and override configs.
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
r.persistentState, results, newIntents, !andResume,
mService.isNextTransitionForward(), profilerInfo);
}
而app.thread是ApplicationThread,它是一个ActivityThread的内部类,可以理解为在ActivityManagerService这一侧的ActivityThread代理对象,主要是通过binder与远端(app进程)进行调用,接着我们分析
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List pendingResults, List pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident = ident;
r.intent = intent;
r.referrer = referrer;
r.voiceInteractor = voiceInteractor;
r.activityInfo = info;
r.compatInfo = compatInfo;
r.state = state;
r.persistentState = persistentState;
r.pendingResults = pendingResults;
r.pendingIntents = pendingNewIntents;
r.startsNotResumed = notResumed;
r.isForward = isForward;
r.profilerInfo = profilerInfo;
r.overrideConfig = overrideConfig;
updatePendingConfiguration(curConfig);
sendMessage(H.LAUNCH_ACTIVITY, r);
}
主要是通过mH这个handler发送消息然后进行处理,最终又会来到performLaunchActivity这个方法里面:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
...
...
return activity;
}
就是利用Instrumentation.newActivity()方法通过反射调用实例化我们插件中的activity,从而将插件中的activity交给系统托管。
而Small正是利用了这一点,核心原理大概讲完了.而其余组件的
总结
当然这里只是对核心原理进行了一下简略的描述,要想达到生产需求还要许多工作要做。
例如:正确区分宿主的activity和插件的activity,当某个activity处在宿主中且已注册时,直接跳过插件化的步骤,交给系统处理即可。
再例如,当我们的需求需要在start多个相同的launchMode的activity时,需要在宿主占坑多少个这样的proxy activity?
像上文提到的mH这个handler,我们其实可以hook住这个mH然后所有的分发事件。插件化的实现有很多种,但无非都是在App创建流程中在ActivityThread,Instrumentation,ActivityManagerNative(AMS的本地代理对象)做文章,所以理解App创建流程对于插件化思想至关重要,说不定可以找到某个新奇的突破点进行插件化。
值得一提的是Android9开始对反射进行限制,像反射调用ActivityThread里的currentActivityThread(),mH都被标为浅灰名单。
可能在日后的版本中插件化思想将不能使用了。。。