插件化原理(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<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* 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<ResultInfo> pendingResults, List<ReferrerIntent> 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都被标为浅灰名单。
可能在日后的版本中插件化思想将不能使用了。。。