为什么插件化
- 将 app 中一些不常用的功能模块做成插件(免安装apk):
- 一方面减小了安装包的大小,启动更快,
- 另一方面可以实现 app 功能的动态插拔。
- 附带好处
升级``ABTest``换肤``多开
- 插件化和组件化的区别:
- 组件化是拆成业务组件和功能组件,虽然可以单独调试,但发布的时候依然依赖进来统一编译一个apk;
- 插件化在组件化基础上更进一步, 把独立出来的模块编译成单独apk,打包时宿主和插件分开打包;
插件化的开源框架发展
特性 | DroidPlugin | RePlugin | VirtualAPK |
---|---|---|---|
作者 | 360 | 360 | 滴滴 |
支持四大组件 | 全支持 | 全支持 | 全支持 |
组件无需在宿主manifest中预注册 | √ | √ | √ |
插件可以依赖宿主 | × | √ | √ |
支持PendingIntent | √ | √ | √ |
Android特性支持 | 几乎全部 | 几乎全部 | 几乎全部 |
兼容性适配 | 高 | 高 | 高 |
插件构建 | 无 | Gradle插 件 | Gradle插件 |
需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进
行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk。
插件化的难点
1. 如何加载插件的类?
虚拟机类加载三件事
- (Art)读取该类的二进制字节流
- 将静态的字节流转为方法区的数据结构
- 在堆生成该类的Class对象,作为访问方法区数据入口
类的Class对象
:当前类的抽象类的对象,比当前类实例化拿到更多属性和方法- 怎么获取类的对象?
- getClass()
- Class.forName()
- getClassLoader().loadClass()
java Android 类加载机制 区别
- java里面加载class 文件 ,Android 中 DVM 和 ART 加载的是 dex 文件
了解APP实际使用的ClassLoader
// 在 onCreate 中执行下面代码
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.e("tag", "classLoader:" + classLoader);
classLoader = classLoader.getParent();
}
Log.e("tag", "classLoader:" + Activity.class.getClassLoader());
Log.e("tag", "classLoader:" + AppCompatActivity.class.getClassLoader());
结果:
classLoader:dalvik.system.PathClassLoader //默认由pathClassLoader加载当前应用 和依赖库
classLoader:java.lang.BootClassLoader@a26e88d //父类
classLoader:java.lang.BootClassLoader@a26e88d//源码SDK默认由BootClassLoade加载,也就是部分Framework去掉依赖进来的系统源码;
classLoader:dalvik.system.PathClassLoader //依赖进来的系统源码 由pathClassLoader加载
如何使用类加载器去加载一个类
//用法
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,context.getCacheDir().getAbsolutePath()//路径是 apkPath
, null, context.getClassLoader());
Class<?> clazz = dexClassLoader.loadClass("com.xxx.xxx.Test");//类名
Method method = clazz.getMethod("xxx");
method.invoke(null);
-----------------------------------------------
//源码:机制
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(parent);
// 初始化 pathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
//接着再来看 DexPathList 类中的 findClass 方法。
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
//通过 Element 获取 Class 对象
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
return null;
}
//首先检测这个类是否已经当前类加载器加载了,如果找到了,直接获取返回;
//如果没有找到,调用父类的方法loadclass,检测是否被父类加载器加载了,依次递归
//到最顶层父类,判断是否已经加载了,
//如果没有抛异常依次返回到当前类加器加载;
--------/**-双亲委托*/--------
插件化类加载实现原理:
- 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
- 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
- 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。 4. 最后通过反射将新的 Element[] 赋值给宿主的 dexElements 。
// 获取 pathList 的字段
public static void loadClass(Context context) {
Class.forName("dalvik.system.BaseDexClassLoader");
getDeclaredField("pathList");
// 获取 dexElements 字段
Class.forName("dalvik.system.DexPathList");
getDeclaredField("dexElements");
//注意上面获取的是类相关与对象无关,不同对象可以获得不同的值
//所以下面宿主和插件都可以用
//获取宿主的 dexElements[]
(PathClassLoader) context.getClassLoader();
Object hostPathList = pathListField.get(pathClassLoader);
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
//获取插件的 dexElements[]
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(), null, context.getClassLoader());
Object pluginPathList = pathListField.get(dexClassLoader);
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
2. 如何加载插件的资源?
//Resources去访问 res中的资源,使用 AssetManager 访问 assets 里面的资源
String appName = getResources().getString(R.string.app_name);
InputStream is = getAssets().open("icon_1.png");
创建新的Resources对象
如何创建新的Resources对象返回给插件使用assert.addAssertPath(插件资源),传入宿主的application;
LoadUtil.class
public static Resources loadResource(Context context) {
try {
Class<?> assetManagerClass = AssetManager.class;
AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
Method addAssetPathMethod = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, apkPath);
Resources resources = context.getResources();
//用来加载插件包中的资源
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
如何调用loadResource方法
- 实现方式
-
宿主资源和插件资源合并,冲突的话aapt解决
aapt流程
:生成R.java和srec
apk打包流程
:待补充
ziplign
四字节对齐:1.运行快 — mmap -
资源不合并,插件单独加载,在插件中基类重写getResource();也会产生冲突,id不同
// 插件中代码
//然后让插件的 Activity 都继承自 BaseActivity;
resources 对象,也就可以拿到资源了。
public abstract class BaseActivity extends Activity {
@Override
public Resources getResources() {
//思考为什么传入getApplication ?
Resources resources = LoadUtil.loadResource(getApplication);
return resources == null ? super.getResources() : resources;
}
}
在插件getApplication
获取到的宿主还是插件的?宿主的,双亲机制,是同一个,因为插件的application不会执行- 假如继承AppCompatActivity,空指针–
- 解决方法:创建插件自己的Context,绑定启动插件资源的Resource;
public class BaseActivity extends AppCompatActivity {
protected Context mContext;
// 将创建的插件reource,绑定到插件的context里面,创建插件的context
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources resources = LoadUtil.getResources(getApplication());
mContext = new ContextThemeWrapper(getBaseContext(), 0);
Class<? extends Context> clazz = mContext.getClass();
try {
Field mResourcesField = clazz.getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(mContext, resources);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//然后在MainActivity 使用插件的context
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 暂时注释,没涉及到资源
// setContentView(R.layout.activity_main);
View view = LayoutInflater.from(mContext).inflate(R.layout.activity_main, null);
setContentView(view);
}
}
3. 如何调用插件类?解决注册的问题
HOOK原则
:最好静态变量或单例对象,其次public对象和方法(goole对外的);
看源码疑问?凭啥hook只改那一点点就可以,其他方法不需要重写?只能代理接口?
1. method.invoke
2. 只能代理接口,但是invoke传入被代理对象的实例
Object mInstanceProxy = Proxy.newProxyInstance(, A类, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//invoke 回调几次 取决去A类有几个方法;
//这里我们开心在在invoke前后随意操作 -----
//mInstance 原来对象
return method.invoke(mInstance, args);
---这一步保证A类的所有方法全部保留,想当于super.xxx------
Activity的启动流程
参考app启动流程
找出hook点 ----替换Intent
通过这张图我们可以确定 Hook 点的大致位置。
在进入 AMS 之前,找到一个 Hook 点,用来将插件 Activity 替换为 ProxyActivity。
// android/app/Instrumentation.java
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// 这儿就是我们的 Hook 点,替换传入 startActivity 方法中的 intent 参数
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
}
生成一个代理对象,代理ActivityManager.getService()
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
下面我们就生成代理对象,并且当执行的方法是startActivity 的时候,替换它的参数 intent。
Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{iActivityManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 当执行的方法是 startActivity 时作处理
if ("startActivity".equals(method.getName())) {
int index = 0;
// 获取 Intent 参数在 args 数组中的index值
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
// 得到原始的 Intent 对象 -- 唐僧(插件)的Intent
Intent intent = (Intent) args[index];
// 生成代理proxyIntent -- 孙悟空(代理)的Intent
Intent proxyIntent = new Intent();
proxyIntent.setClassName("com.enjoy.pluginactivity", ProxyActivity.class.getName());
// 保存原始的Intent对象
proxyIntent.putExtra(TARGET_INTENT, intent);
// 使用proxyIntent替换数组中的Intent
args[index] = proxyIntent;
}
return method.invoke(mInstance, args);
}
});
那如何替换
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;
}
};
下面我们再看下Singleton 是怎么样的。
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
可以看出,IActivityManagerSingleton.get() 返回的实际上就是 mInstance 对象。所以接下来我们要替换的就是这
个对象。
接着我们来实现第二步,从 AMS 出来后,再找一个 Hook 点,用来将 ProxyActivity 替换为插件 Activity。
在AMS检查之前将插件activity用ProxyActivity替换,因为它是宿主的可以注册,AMS检测没问题,回到Handler的时候替换回来,handleMessage();
public class HookUtil {
private static final String TARGET_INTENT = "target_intent";
public static void hookAMS() {
try {
// 获取 singleton 对象
Field singletonField = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // 小于8.0
Class<?> clazz = Class.forName("android.app.ActivityManagerNative");
singletonField = clazz.getDeclaredField("gDefault");
} else {
Class<?> clazz = Class.forName("android.app.ActivityManager");
singletonField = clazz.getDeclaredField("IActivityManagerSingleton");
}
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);
// 获取 系统的 IActivityManager 对象
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
final Object mInstance = mInstanceField.get(singleton);
Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
// 创建动态代理对象
Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{iActivityManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// do something
// Intent的修改 -- 过滤
/**
* IActivityManager类的方法
* startActivity(whoThread, who.getBasePackageName(), intent,
* intent.resolveTypeIfNeeded(who.getContentResolver()),
* token, target != null ? target.mEmbeddedID : null,
* requestCode, 0, null, options)
*/
// 过滤
if ("startActivity".equals(method.getName())) {
int index = -1;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
// 启动插件的
Intent intent = (Intent) args[index];
Intent proxyIntent = new Intent();
proxyIntent.setClassName("com.enjoy.leo_plugin",
"com.enjoy.leo_plugin.ProxyActivity");
proxyIntent.putExtra(TARGET_INTENT, intent);
args[index] = proxyIntent;
}
// args method需要的参数 --- 不改变原有的执行流程
// mInstance 系统的 IActivityManager 对象
return method.invoke(mInstance, args);
}
});
// ActivityManager.getService() 替换成 proxyInstance
mInstanceField.set(singleton, proxyInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void hookHandler() {
try {
// 获取 ActivityThread 类的 Class 对象
Class<?> clazz = Class.forName("android.app.ActivityThread");
// 获取 ActivityThread 对象
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);
// 获取 mH 对象
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
// 创建的 callback
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
// 通过msg 可以拿到 Intent,可以换回执行插件的Intent
// 找到 Intent的方便替换的地方 --- 在这个类里面 ActivityClientRecord --- Intent intent 非静态
// msg.obj == ActivityClientRecord
switch (msg.what) {
case 100:
try {
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// 启动代理Intent
Intent proxyIntent = (Intent) intentField.get(msg.obj);
// 启动插件的 Intent
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
if (intent != null) {
intentField.set(msg.obj, intent);
}
} catch (Exception e) {
e.printStackTrace();
}
break;
case 159:
try {
// 获取 mActivityCallbacks 对象
Field mActivityCallbacksField = msg.obj.getClass()
.getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);
for (int i = 0; i < mActivityCallbacks.size(); i++) {
if (mActivityCallbacks.get(i).getClass().getName()
.equals("android.app.servertransaction.LaunchActivityItem")) {
Object launchActivityItem = mActivityCallbacks.get(i);
// 获取启动代理的 Intent
Field mIntentField = launchActivityItem.getClass()
.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);
// 目标 intent 替换 proxyIntent
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
if (intent != null) {
mIntentField.set(launchActivityItem, intent);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
// 必须 return false
return false;
}
};
// 替换系统的 callBack
mCallbackField.set(mH, callback);
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结
插件化涉及的技术其实是非常多的,比如
应用程序启动流程、四大组件启动流程、AMS原理、PMS原理、ClassLoader原理、Binder机制,动态代理等等;
插件化的框架使用
//待补充