组件化开发就是将一个app分成多个模块,每个模块都是一个个组件,开发的过程中我们可以让这些组件相互依赖或者单独调试组件,但是最终发布的时候是将这些组件并成一个apk发布,而插件话 是分为一个宿主 和多个插件apk ,插件话成本高就是 适配 android版本,每个android版本的源码实现都不同,每个新版本出来,你就得去看源码然后 对这个源码做适配。
Bootstrap ClassLoader(启动类加载器):该类加载器由C++实现的。负责加载Java基础类,对应加载的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等。
Extension ClassLoader(标准扩展类加载器):继承URLClassLoader。对应加载的文件是%JRE_HOME/lib/ext 目录下的jar和class等。
App ClassLoader(系统类加载器):继承URLClassLoader。对应加载的应用程序classpath目录下的所有jar和class等。还有一种就是我们自定义的 ClassLoader,由Java实现。我们可以自定义类加载器,并可以指定这个类加载器要加载哪个路径下的class文件。
BootClassLoader:用来加载 sdk framewrok层系统层里面的类,注意,只是 framewrok层系统类。而项目依赖的库里面的类不算事系统类,所以就不是BootClassLoader加载,而是用PathClassLoader。比如,androidx 库里面的类就不是系统sdk类,而是依赖的库类,是用PathClassLoader加载的。
PathCLassLoader:BaseDexClassLoader的子类,是整个程序中的类加载器,相当于 java 中的 AppClassLoader,也就是说我们整个项目的除了系统的类是用BootClassLoader加载的 其他都是用 PathCLassLoader加载的 ,也就是说,根据 双亲委派 。 PathCLassLoader去加载类的时候 先判断 BootClassLoader 是否加载过,没有那就是 自己去加载了 也就是 PathCLassLoader自己去加载。
加载一个类就是靠的 classloader,那么如果我们想在宿主中加载插件的类,就有两种方案。
获取每个插件的classloader 然后利用插件的classloader 去加载插件的类,然后反射获取类的信息。
//获取每个插件的classloader 然后利用插件的classloader 去加载插件的类。
public void loadPluginClass(Context context, String pluginPath) {
pluginPath = "/sdcard/plugin";
if (TextUtils.isEmpty(pluginPath)) {
throw new IllegalArgumentException("插件路径不能拿为空!");
}
File pluginFile = new File(pluginPath);
if (!pluginFile.exists()) {
Log.e("zjs", "插件文件不存在!");
return ;
}
File optDir = context.getDir("optDir", Context.MODE_PRIVATE);
String optDirPath = optDir.getAbsolutePath();
Log.d("zjs", "optDirPath " + optDirPath);
try {
//获取到插件的DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, optDirPath, null, context.getClassLoader());
//就可以利用插件的DexClassLoader 去加载 插件的一个个类,然后反射获取类的信息。
Class<?> classType = dexClassLoader.loadClass("com.example.plugin.Book");
Constructor<?> constructor = classType.getConstructor(String.class, int.class);
Object book = constructor.newInstance("android开发艺术探索", 88);
Method getNameMethod = classType.getMethod("getName");
getNameMethod.setAccessible(true);
Object name = getNameMethod.invoke(book);
Log.d("zjs", "name " + name);
} catch (Exception e) {
Log.d("zjs", "e" , e);
e.printStackTrace();
}
}
把插件classloader的dexpathlist里的 Element【】element 和 宿主的 classLoader的dexpathlist里的 Element【】element 合并一个新的Element【】element然后用这个新的Element【】element 替换掉 宿主的 classLoader的的dexpathlist里 Element【】element这样在宿主中就可以直接用 宿主的 classLoader去加载 插件的任何一个类。当然你可以把 用这个新的Element【】element 替换掉 插件的 classLoader的的dexpathlist里 Element【】element这样在插件中就可以直接用 插件的 classLoader去加载 插件的任何一个类。
public void mergeHostAndPluginDex(Context context,String pluginPath){
if (TextUtils.isEmpty(pluginPath)) {
throw new IllegalArgumentException("插件路径不能拿为空!");
}
try {
Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clazz.getDeclaredField("pathList");
pathListField.setAccessible(true);
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElements = dexPathListClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
// 1.获取宿主的ClassLoader中的 dexPathList 在从 dexPathList 获取 dexElements
ClassLoader pathClassLoader = context.getClassLoader();
Object dexPathList = pathListField.get(pathClassLoader);
Object[] hostElements = (Object[]) dexElements.get(dexPathList);
// 2.获取插件的 dexElements
DexClassLoader dexClassLoader = new DexClassLoader(pluginPath,
context.getCacheDir().getAbsolutePath(), null, pathClassLoader);
Object pluginPathList = pathListField.get(dexClassLoader);
Object[] pluginElements = (Object[]) dexElements.get(pluginPathList);
// 3.先创建一个空的新数组
Object[] allElements = (Object[]) Array.newInstance(hostElements.getClass().getComponentType(),
hostElements.length + pluginElements.length);
//4把插件和宿主的Elements放进去
System.arraycopy(hostElements, 0, allElements, 0, hostElements.length);
System.arraycopy(pluginElements, 0, allElements, hostElements.length, pluginElements.length);
// 5.把宿主的classloader 的 dexPathList 中的dexElements 换成 allElements
dexElements.set(dexPathList, allElements);
} catch (Exception e) {
Log.d("zjs", "e" , e);
e.printStackTrace();
}
}
使用示例如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取sdcard 读写权限
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
// 高版本Android SDK时使用如下代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if(!Environment.isExternalStorageManager()){
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
return;
}
}
//把插件的dex和宿主的dex和在宿主的classloader中
mergeHostAndPluginDex(this, "/sdcard/plugin.apk");
//就可以直接在宿主的ClassLoader 去加载 插件的一个个类,然后反射获取类的信息。
try {
ClassLoader classLoader = this.getClassLoader();
Class<?> classType = classLoader.loadClass("com.example.plugin.Book");
Constructor<?> constructor = classType.getConstructor(String.class, int.class);
Object book = constructor.newInstance("android开发艺术探索", 88);
Method getNameMethod = classType.getMethod("getName");
getNameMethod.setAccessible(true);
Object name = getNameMethod.invoke(book);
Log.d("zjs", "name " + name);
} catch (Exception e) {
Log.d("zjs", "e " , e);
e.printStackTrace();
}
}
这种操作也有缺点,当插件和宿主 引用的 同一个库的不同版本时,可能会导致程序出错,需要进行特殊处理规避。此外,当插件数量过多时,会造成宿主的 dexElements 数组体积增大。
整体思路:
在宿主中的androidmainfest添加一个傀儡SubActivity。在宿主调用 startActivity(pluginActivity)的时候,通过Hook 告诉AMS 启动的是 SubActivity,因为 AMS 认识这个SubActivity,然后在AMS 通知启动 SubActivity,我们再次Hook,启动的是 我们的 pluginActivity。
而上面的说 一个 SubActivity 应对 插件千万个activity ,就是 我们 不管需要在宿主中开启插件哪个acticity,我们都是欺骗AMS 启动的是SubActivity,关键是把真正要开启的活动数据 放入SubActivity 中保存起来 ,那么再 AMS 通知启动 SubActivity回来的时候,我们再从SubActivity取出真正要开启realActivity。
<activity android:name="com.example.myapplication.SubActivity"/>
//在宿主内,开启任意想要的activity
Intent intent = new Intent();
ComponentName pluginActivity= new ComponentName("com.example.plugin", "com.example.plugin.MainActivity");
intent.setComponent(pluginActivity);
startActivity(intent);
前面我们学了 activity的工作流程,知道了 AMS的在应用进程的代理对象是AMP,也就是进程是通过AMP告诉AMS要做什么事情的,所以我们就可以Hook AMP,也就是根据前面学到的。
//android 8.0之前
public static void hookAMP(Context context){
try {
//先获取ActivityManagerNative类里面的静态变量 gDefault 它是个 Singleton 类型的
Class activityManagerClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = activityManagerClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
Object gDefault = gDefaultField.get(null);
//从这个 gDefault获取他的mInstance对象。这个mInstance 对象就是AMP
Class classSingleton = Class.forName("android.util.Singleton");
Field mInstanceField = classSingleton.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Object mInstance = mInstanceField.get(gDefault);
// 自定义一个 mInstance 的代理
Class<?> iActivityManagerInterface =Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(context.getClassLoader(),
new Class<?>[]{iActivityManagerInterface},
new InvocationHandlerBinder(mInstance));
//把gDefault的mInstance替换为 proxy
mInstanceField.set(gDefault,proxy);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
Log.d("zjs", "hookAMP: ",e);
e.printStackTrace();
}
}
static class InvocationHandlerBinder implements InvocationHandler{
private Object mBase;
public InvocationHandlerBinder(Object base) {
mBase= base;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
//动态代理了AMS在 应用进程的binder对象,这样 应用进程 调用 ams 的binder对象 通信给 ams的每个方法都会 经过这里
try {
//如果应用进程让AMS 开启活动
if ("startActivity".equals(method.getName())) {
int index = 0;
for (int i = 0; i < objects.length; i++) {
if (objects[i] instanceof Intent) {
index = i;
}
}
//从参数中获得实际上要启动activity
Intent realActivity = (Intent) objects[index];
//从这里判断这个activity 的包名是不是 不是 宿主的,不是才需要 创建个傀儡subActivity,然后把真正要启动到 realActivity放入subActivity中
//最好偷梁换柱,把原本的 变量realActivity 换成 subActivity
if (!("com.example.myapplication".equals(realActivity.getComponent().getPackageName()))) {
Intent subActivity = new Intent();
subActivity.setComponent(new ComponentName("com.example.myapplication", "com.example.myapplication.SubActivity"));
subActivity.putExtra("plugin", realActivity);
objects[index] = subActivity;
}
}
}catch (Exception e){
Log.d("zjs", "invoke: ",e);
}
//这里依旧是还源变量应该做的事情,
return method.invoke(mBase,objects);
}
}
当AMS 告诉应用进程启动SubActivity的时候会经过应用进程的ActivityThread 里面的Handle 类H ,这个H会发送一个消息叫做 LAUNCHER_ACTIVITY = 100的消息,然后由 Handle H 里面的 dipathchMessage()方法,通过这个方法的源码我们可以知道我们可以对这个 Handle H 设置一个我们的代理 CallbackProxy,让我们这个 CallbackProxy 去处理消息。
public static void hookHandleCallback(){
try {
//先获取ActivityThread类里面的静态变量 sCurrentActivityThread
Class activityThread = Class.forName("android.app.ActivityThread");
Field currentActivityThreadField = activityThread.getDeclaredField("sCurrentActivityThread");
currentActivityThreadField.setAccessible(true);
Object sCurrentActivityThread = currentActivityThreadField.get(null);
//在从sCurrentActivityThread内部 获取Handle类 mH对象 变量
Field mHField = activityThread.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(sCurrentActivityThread);
//把mh的mCallback字段替换成代理的
Class handle = Handler.class;;
Field mCallbackField = handle.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH,new CallbackProxy(mH));
} catch (Exception e) {
Log.d("zjs", "hookHandleCallback",e);
e.printStackTrace();
}
}
private static class CallbackProxy implements Handler.Callback{
Handler mH;
public CallbackProxy(Handler mH) {
this.mH = mH;
}
private void handleLauncherActivity(Message message) {
try {
//这里的message.obj 其实就是个ActivityClientRecord 对象
Object obj = message.obj;
//从这个ActivityClientRecord获取intent变量
Class object = obj.getClass();
Field intentField = object.getDeclaredField("intent");
intentField.setAccessible(true);
//这个raw Activity 就是AMS要去启动的Activity
Intent raw = (Intent) intentField.get(obj);
//我们对这个Activity进行判断 如果它里面存有插件活动,则证明这个Activity是个SubActivity
//那么我们就需要 把这个activity 设置 realActivity
Intent realActivity = raw.getParcelableExtra("plugin");
if(realActivity!=null){
raw.setComponent(realActivity.getComponent());
}
//到了这里不管是不是插件活动还是宿主活动 raw 都会是正确的值
Log.d("zjs", "handleLauncherActivity: "+ raw.getComponent().getClassName());
}catch (Exception e){
Log.d("zjs", "handleLauncherActivity: ",e);
}
}
@Override
public boolean handleMessage(@NonNull Message message) {
final int LAUNCH_ACTIVITY = 100;
switch (message.what){
case LAUNCH_ACTIVITY:
handleLauncherActivity(message);
}
//还原原本操作
mH.handleMessage(message);
return true;
}
}
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//将插件和宿主dex合并
LoadPluginDex.mergeHostAndPluginDex(this,"/sdcard/plugin.apk")
//告诉AMS启动的SubActivity
Hook.hookAMP(this);
//回来启动的realActivity
Hook.hookHandleCallback();
}
}
在宿主中开启插件的活动:
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d("zjs", "onClick:plugin");
Intent intent = new Intent();
ComponentName pluginActivity = new ComponentName("com.example.plugin", "com.example.plugin.MainActivity");
intent.setComponent(pluginActivity);
startActivity(intent);
}
});
前面通过我们hook我们已经可以把在宿主中开启插件的activity了,activity是启动了,但是activity的界面没有起来,资源的插件化我们是有两种方案:
方案一:
把插件的资源跟宿主的给合并一个allResource,但是这里有个缺点就是如果插件的某个资源id 跟宿主的 某个资源id 一样,allResource里面就不会有插件的这个资源id ,解决方案就是 利用appt去 修改插件的 资源id前缀。
方案二:
把插件的resource 跟宿主的资源分开出来,那么有几个关键注意点:
1、获取插件的resource是要放在宿主中获取呢,还是插件自己去获取自己的resouces,其实最好还是在插件中 自己利用反射去获取自己的resource,如果在宿主中 去反射获取插件的pluginResource,插件中的代码还得去引用宿主里的pluginResource对象 来获取资源。
2、前面我们说了我们把插件的dex 合并到宿主中来,再根据双亲委派机制,加载过的类不会再加载,并且宿主的dex 是优先于插件的dex,也就是一切都是以宿主为主,那么对于宿主来说,插件的MainActivity其实就是一个普通的类,它没有任何特殊的。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//获取的是宿主的Application,就算插件的自定义Application这个Application也不会被执行,
//因为整个项目里,Application,已经被宿主的Application加载过了
getApplication()
context.getResource()//获取的也是宿主的Resource
//context 的 setContentView(R.layout.activity_main)也是从宿主的Resource中找
setContentView(R.layout.activity_main);
}
}
3、宿主和插件双方的都有系统资源或者引用的第三库的资源是否会因为命名一致发生冲突。
比如说:如果你插件的 MainActivity 继承是的 AppCompatActivity,这个AppCompatActivity在 宿主跟插件都是引用了第三方android x 库。AppCompatActivity 里面的view 加载流程内 有个 id R.id.deco_content_parent 。 在 插件apk 编译的时候。假如它的id 0x7f07004d 。而在宿主的apk 编译的时候 它的id 0x7f07004e,那么由于 插件中的conetxt 是宿主的,那么当插件要去找 这个 id 为0x7f07004d 的时候,发现找不到 只有0x7f07004e所以就会报错。
所以解决思想就是,我们只要把思想转变为 ,对于宿主来说,插件的MainActivity类 其实就是宿主内部的一个普通的类,它没有任何特殊的,只是在宿主中单单的在执行一个MainActivity类的对应方法而已。那么我们在插件MainActivity类中自己反射自己获取自己的pluginResource。在MainActivity中,创建一个Context,通过反射把pluginResource 设置到 这个Context中,从这个Context获取view,后续MainActivity这个类 拿资源我们都是从这个context 中拿。
插件中代码示例:
package com.example.plugin;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.AppCompatButton;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
public Resources pluginResources;
private Context pluginContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//在这个类里先获取插件的pluginResources
preloadResource(getApplication(), "/sdcard/plugin.apk");
//创建一个pluginContext,把pluginResources设置到pluginContext中去
//我们这创造的这个context ,是个 ContextThemeWrapper ,导入的包是android x 下的 ContextThemeWrapper
pluginContext = new ContextThemeWrapper(getBaseContext(),0);
Class classname = pluginContext.getClass();
try {
Field field = classname.getDeclaredField("mResources");
field.setAccessible(true);
field.set(pluginContext, pluginResources);
}catch (Exception e){
Log.d("zjs", "plugin onCreate: ",e);
}
//通过pluginContext创造view
View view = LayoutInflater.from(pluginContext).inflate(R.layout.activity_main, null);
setContentView(view);
AppCompatButton button = view.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d("zjs", "plugin MainActivity button: ");
}
});
Log.d("zjs", "plugin MainActivity onCreate: ");
}
public void preloadResource(Context context, String apkFilePath) {
try {
//反射调用AssetManager的addAssetPath方法把插件路径传入
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager,apkFilePath);
//以插件的 AssetManager 创作属于插件的 resources。
//这里的resource的后面两个参数,一般跟宿主的配置一样就可以了,根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建
pluginResources = new Resources(assetManager,context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
}
AMS启动的是SubActiviy,被我们换成RealActivity,那么生命周期会不会有影响,其实是不会的,通过上面的activity源码流程我们知道AMS跟应用进程内部都会维护一个mActivitys,它是个ArrayMap<IBinder,ActivityClientRecord>。
AMS就是根据这个key 对应的,来给key对应的那个ActivityClientRecord发消息。应用进程就是根据这个key,告诉AMS 要对key对应的ActivityClientRecord做什么事情。
最开始我们hook 欺骗AMS,启动subactivity ,那么 AMS的 mActivitys,它的其中一个 token key 对应的是 subactivity 。
那么,当AMS 根据这个key 告诉应用进程说要启动 subactivity 的时候 ,应用进程的收到这个消息的 流程如下:
LAUNCH ACTIVITY = 100》handleLaunchActivity》performlaunchActivity》mInstrumentation.newActiviry>mInstrumentation.callonActivityonCreate>onCreate
PAUSE_ACTIVITY = 101》handlePauseActivity》performPauseActivity>》mInstrumentation.callActivityonPause>onPause
在 performlaunchActivity 方法中,mActivitys.put(r.token,r) 把对 应用进程的当前的ActivityClientRescord跟token存起来。
而我们hook的是 LAUNCH ACTIVITY = 100》handleLaunchActivity 这个步骤,我们 把 ActivityClientRescord的Intent 的componet 从原本的 subactivity 替换为我们要的 RealActivity。到了 performlaunchActivity 这一步的时候,mActivitys.put(r.token,r)对这个token,存放的是 RealActivity。
所以 应用进程 的 mActivitys,它跟 AMS 的 mActivitys 中的 相同 token key 对应的是 不同的。应用进程对应的 RealActivity,AMS对应的subactivity。
流程如下:
当应用进程的 需要调用某个activity 的onpause 方法,流程就会跑到Instrumentation 利用binder 告诉 AMS 要 对这个token 对应的 activity 调用onpause ,这个时候这个 activity 是 RealActivity ,而在AMS 收到消息后,它根据这个token 认为 我调用的是 subactivity的onpause,应用进程收到消息后,根据这个token 认为 要调用的是 RealActivity的onpause。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。