提到免安装应用,大家肯定第一想到的就是小程序,但是在Android中其实是有这么一项技术用于动态加载apk的,那就是插件化。今天一起来看看吧!
我觉得最主要的原因是可以动态扩展功能。把一些不常用的功能或者模块做成插件
,就能减少原本的安装包大小,让一些功能以插件的形式在被需要的时候被加载,也就是实现了动态加载
。
比如动态换肤、节日促销、见不得人
的一些功能,就可以在需要的时候去下载相应模式的apk,然后再动态加载功能。所以一般这个功能适用于一些平台类的项目,比如大众点评美团这种,功能很多,用户很大概率只会用其中的一些功能,而且这些模块单独拿出来都可以作为一个app运行。
但是现在用的缺很少了,具体情况见第三点。
要实现插件化,也就是实现从apk读取所有数据,要考虑三个问题:
读取插件代码
,完成插件中代码的加载和与主工程的互相调用读取插件资源
,完成插件中资源的加载和与主工程的互相访问四大组件管理
1)读取插件代码,其实也就是进行插件中的类加载。所以用到类加载器就可以了。Android中常用的有两种类加载器,DexClassLoader
和PathClassLoader
,它们都继承于BaseDexClassLoader
。区别在于DexClassLoader多传了一个optimizedDirectory
参数,表示缓存我们需要加载的dex文件的,并创建一个DexFile
对象,而且这个路径必须为内部存储路径。而PathClassLoader
这个参数为null,意思就是不会缓存到内部存储空间了,而是直接用原来的文件路径加载。所以DexClassLoader
功能更为强大,可以加载外部的dex文件。
同时由于双亲委派机制,在构造插件的ClassLoader
时会传入主工程的ClassLoader
作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。而主工程调用插件则需要通过DexClassLoader
去加载类,然后反射调用方法。
2)读取插件资源,主要是通过AssetManager
进行访问。具体代码如下:
/**
* 加载插件的资源:通过AssetManager添加插件的APK资源路径
*/
protected void loadPluginResources() {
//反射加载资源
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
通过addAssetPath方法把插件的路径穿进去,就可以访问到插件的资源了。
3)四大组件管理 为什么单独说下四大组件呢?因为四大组件不仅要把他们的类加载出来,还要去管理他们的生命周期,在AndroidManifest.xml
中注册。这也是插件化中比较重要的一部分。这里重点说下Activity。
主要实现方法是通过Hook技术,主要的方案就是先用一个在AndroidManifest.xml
中注册的Activity来进行占坑,用来通过AMS的校验,接着在合适的时机用插件Activity
替换占坑的Activity
。
❝Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。 ❞
这里的hook其实就是我们常说的下钩子,可以改变函数的内部行为。
这里加载插件Activity用到hook技术,有两个可以hook的点,分别是:
第一步
:使用占坑的这个Activity完成AMS验证。也就是让AMS知道我们要启动的Activity是在xml里面注册过的哦。具体代码如下: @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".contains(method.getName())) {
//换掉
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Intent) {
//说明找到了startActivity的Intent参数
intent = (Intent) args[i];
//这个意图是不能被启动的,因为Acitivity没有在清单文件中注册
index = i;
}
}
//伪造一个代理的Intent,代理Intent启动的是proxyActivity
Intent proxyIntent = new Intent();
ComponentName componentName = new ComponentName(context, proxyActivity);
proxyIntent.setComponent(componentName);
proxyIntent.putExtra("oldIntent", intent);
args[index] = proxyIntent;
}
return method.invoke(iActivityManagerObject, args);
}
第二步
:替换回我们的Activity。上面一步是把我们实际要启动的Activity换成了我们xml里面注册的activity来躲过验证,那么后续我们就需要把Activity换回来。Activity启动的最后一步其实是通过H(一个handler)中重写的handleMessage方法会对LAUNCH_ACTIVITY
类型的消息进行处理,最终会调用Activity的onCreate方法。最后会调用到Handler的dispatchMessage
方法用于处理消息,如果Handler的Callback类型的mCallback
不为null,就会执行mCallback的handleMessage
方法。所以我们能hook的点就是这个mCallback
。
public static void hookHandler() throws Exception {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");//1
Field mHField = FieldUtil.getField(activityThread,"mH");//2
Handler mH = (Handler) mHField.get(currentActivityThread);//3
FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
}
public class HCallback implements Handler.Callback{
//...
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
Object r = msg.obj;
try {
//得到消息中的Intent(启动SubActivity的Intent)
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
//得到此前保存起来的Intent(启动TargetActivity的Intent)
Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
//将启动SubActivity的Intent替换为启动TargetActivity的Intent
intent.setComponent(target.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
mHandler.handleMessage(msg);
return true;
}
}
用自定义的HCallback来替换mH中的mCallback
即可完成Activity的替换了。
这个方法是由于startActivityForResult
方法中调用了Instrumentation的execStartActivity
方法来激活Activity的生命周期,所以可以通过替换Instrumentation
来完成,然后在Instrumentation
的execStartActivity
方法中用占坑SubActivity
来通过AMS的验证,在Instrumentation
的newActivity
方法中还原TargetActivity。
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;
public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
mInstrumentation = instrumentation;
mPackageManager = packageManager;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
if (infos == null || infos.size() == 0) {
intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
intent.setClassName(who, "com.example.liuwangshu.pluginactivity.StubActivity");//2
}
try {
Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
public static void hookInstrumentation(Context context) throws Exception {
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Field mMainThreadField =FieldUtil.getField(contextImplClass,"mMainThread");//1
Object activityThread = mMainThreadField.get(context);//2
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
context.getPackageManager()));
}
前几年插件化还是很火的,比如Dynamic-Load-Apk(任玉刚),DroidPlugin,RePlugin(360),VirtualApk(滴滴)
,但是现在机会都没怎么在运营了,好多框架都只支持到Android9。
这是为什么呢?我觉得一个是维护成本太高,每更新一次源码,就要重新维护一次。二就是确实插件化技术现在用的不多了,以前用插件化干嘛?主要是更新代码,修复bug。那么现在又热更新技术了,为什么还还要考虑插件化呢?组件化+热更新就完全够用了。
虽然插件化用的不多了,但是我觉得技术还是可以了解的,而且热更新主要用的也是这些技术。方案可以被淘汰,但是技术不会。