Android插件化初体验
Posted zhuliyuan丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android插件化初体验相关的知识,希望对你有一定的参考价值。
*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
最近把Activity启动流程整体看了一遍,估摸着弄个啥来巩固下,发现插件化正好是这块技术的实践,而说道插件化其实有好几种实现方式,这里我用的是hook的方式实现,主要目的呢是为了对activity启动流程有个整体的认识,当然了本篇的插件化也只是一个demo版本并没有任何兼容适配,重在流程和原理的理解。
概述
插件化顾名思义,就是将一个APK拆成多个,当需要的时候下载对应插件APK加载的技术。本文demo中除了下载是通过adb命令,其他都是模拟真实环境的,这里先理下流程。
- 将插件工程打包为APK,然后通过adb push命令发送到宿主APK目录(模拟下载流程)。
- 利用ClassLoader加载插件APK中的类文件。
- hook Activity启动流程中部分类,利用占坑Activity帮助PluginActivity绕过AMS验证,在真正启动的时候又替换回PluginActivity。
- 创建插件Apk的Resources对象,完成插件资源的加载。
对整体流程有个大概认识后,下面将结合源码和Demo来详细讲解,本文贴出的源码基于API27。
初始化插件APK类文件
既然插件APK是通过网络下载下来的,那么APK中的类文件就需要我们自己加载了,这里我们要用到DexClassLoader去加载插件APK中的类文件,然后将DexClassLoader中的Element数组和宿主应用的PathClassLoader的Element数组合并再设置回PathClassLoader,完成插件APK中类的加载。对ClassLoader不太熟悉的可以看下我另篇Android ClassLoader浅析
public class InjectUtil
private static final String TAG = "InjectUtil";
private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
private static final String FIELD_PATH_LIST = "pathList";
private static final String FIELD_DEX_ELEMENTS = "dexElements";
public static void inject(Context context, ClassLoader origin) throws Exception
File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/android/data/$packageName/files/plugin
if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0)
Log.i(TAG, "插件文件不存在");
return;
pluginFile = pluginFile.listFiles()[0];//获取插件apk文件
File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
if (!optimizeFile.exists())
optimizeFile.mkdirs();
DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到插件Elements
Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements
Object array = combineArray(originElements, pluginElements);//合并数组
FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//设置回PathClassLoader
Log.i(TAG, "插件文件加载成功");
private static Object combineArray(Object pathElements, Object dexElements) //合并数组
Class<?> componentType = pathElements.getClass().getComponentType();
int i = Array.getLength(pathElements);
int j = Array.getLength(dexElements);
int k = i + j;
Object result = Array.newInstance(componentType, k);
System.arraycopy(dexElements, 0, result, 0, j);
System.arraycopy(pathElements, 0, result, j, i);
return result;
这里我们约定将插件APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目录,然后为了尽早加载所以在Application中执行加载逻辑。
public class MyApplication extends Application
@Override
protected void attachBaseContext(Context base)
super.attachBaseContext(base);
try
InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
catch (Exception e)
e.printStackTrace();
Hook启动流程
在说之前我们得先了解下Activity的启动流程。
上图抽象的给出了Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建Activity(步骤1),AMS会对这个Activty的生命周期栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动Activity。
那么在上一步我们已经将插件Apk的类文件加载进来了,但是我们并不能通过startActivity的方式去启动PluginActivity,因为PluginActivity并没有在AndroidManifest中注册过不了AMS的验证,既然这样我们换一个思路。
- 在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。
- 在AMS处理完相应验证通知我们ActivityThread创建Activty的时候在替换为PluginActivity。
占坑SubActivity非常简单
public class SubActivity extends Activity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
super.onCreate(savedInstanceState);
然后在AndroidManifest注册好即可
<activity android:name=".SubActivity"/>
对于startActivity()最终都会调到ActivityManagerService的startActivity()方法。
ActivityManager.getService()//获取AMS
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
那么我们可以通过动态代理hook ActivityManagerService,然后在startActivity()的时候将PluginActivity替换为SubActivity,不过对于ActivityManagerService的获取不同版本方式有所不同。
在Android7.0以下会调用ActivityManagerNative的getDefault方法获取,如下所示。
static public IActivityManager getDefault()
return gDefault.get();
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>()
protected IActivityManager create()
IBinder b = ServiceManager.getService("activity");//获取ams
if (false)
Log.v("ActivityManager", "default service binder = " + b);
IActivityManager am = asInterface(b);//拿到ams代理对象
if (false)
Log.v("ActivityManager", "default service = " + am);
return am;
;
getDefault()返回的是IActivityManager,而gDefault是一个单例对象Singleton并且是静态的是非常容易用反射获取。
Android8.0会调用ActivityManager的getService方法获取,如下所示。
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);//拿到ams
final IActivityManager am = IActivityManager.Stub.asInterface(b);//拿到ams代理对象
return am;
;
返回一个IActivityManager,而IActivityManagerSingleton是一个单例对象Singleton并且是静态非常容易获取。
在看下上面提到的Singleton等会hook会用到
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;
到这里会发现其实返回的都是AMS的接口IActivityManager,那么我们只要能通过反射拿到,然后通过动态代理去Hook这个接口在启动的时候把PluginActivity替换为SubActivity即可绕过AMS的验证。
public class IActivityManagerProxy implements InvocationHandler //动态代理
private final Object am;
public IActivityManagerProxy(Object am) //传入代理的AMS对象
this.am = am;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
if ("startActivity".equals(method.getName())) //startActivity方法
Intent oldIntent = null;
int i = 0;
for (; i < args.length - 1; i++) //获取startActivity Intent参数
if (args[i] instanceof Intent)
oldIntent = (Intent) args[i];
break;
Intent newIntent = new Intent();//创建新的Intent
newIntent.setClassName("rocketly.demo", "rocketly.demo.SubActivity");//启动目标SubActivity
newIntent.putExtra(HookHelper.TRANSFER_INTENT, oldIntent);//保留原始intent
args[i] = newIntent;//把插件Intent替换为占坑Intent
return method.invoke(am, args);
动态代理写好后,我们还需要通过反射去hook住原始AMS。因为会用到反射弄了一个简单的工具类
public class FieldUtil
public static Object getField(Class clazz, Object target, String name) throws Exception
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
public static Field getField(Class clazz, String name) throws Exception
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
public static void setField(Class clazz, Object target, String name, Object value) throws Exception
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
接下来是hook代码
public class HookHelper
public static final String TRANSFER_INTENT = "transfer_intent";
public static void hookAMS() throws Exception
Object singleton = null;
if (Build.VERSION.SDK_INT >= 26) //大于等于8.0
Class<?> clazz = Class.forName("android.app.ActivityManager");
singleton = FieldUtil.getField(clazz, null, "IActivityManagerSingleton");//拿到静态字段
else //8.0以下
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
singleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");//拿到静态字段
Class<?> singleClazz = Class.forName("android.util.Singleton");
Method getMethod = singleClazz.getMethod("get");
Object iActivityManager = getMethod.invoke(singleton);//拿到AMS
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]iActivityManagerClazz, new IActivityManagerProxy(iActivityManager));//生成动态代理
FieldUtil.setField(singleClazz, singleton, "mInstance", proxy);//将代理后的对象设置回去
接下来我们需要在Application去执行hook
public class MyApplication extends Application
@Override
protected void attachBaseContext(Context base)
super.attachBaseContext(base);
try
InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
HookHelper.hookAMS();//hookAMS
catch (Exception e)
e.printStackTrace();
那么这里我们已经实现了第一步
在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。
接下来我们在看如何在收到AMS创建Activity的通知时替换回PluginActivity。
AMS创建Activity的通知会先发送到ApplicationThread,然后ApplicationThread会通过Handler去执行对应逻辑。
private class ApplicationThread extends IApplicationThread.Stub
@Override
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) //收到AMS启动Activity事件
ActivityClientRecord r = new ActivityClientRecord();
r.intent = intent;//给r赋上要启动的intent
...//省略很多r属性初始化
sendMessage(H.LAUNCH_ACTIVITY, r);//发送r到Handler
private void sendMessage(int what, Object obj)
sendMessage(what, obj, 0, 0, false);
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async)
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async)
msg.setAsynchronous(true);
mH.sendMessage(msg);//发送到mH
private class H extends Handler
public static final int LAUNCH_ACTIVITY = 100;
public void handleMessage(Message msg)
switch (msg.what)
case LAUNCH_ACTIVITY:
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//执行启动activity
break;
既然是通过sendMessage()方式通知Handler去执行对应的方法,那么在调用handleMessage()之前会通过dispatchMessage()分发事件。
public class Handler
final Callback mCallback;
public void dispatchMessage(Message msg)
if (msg.callback != null)
handleCallback(msg);
else
if (mCallback != null)
if (mCallback.handleMessage(msg))
return;
handleMessage(msg);
public interface Callback
public boolean handleMessage(Message msg);
可以发现一个很好的hook点就是mCallback这个接口,可以让我们在handleMessage方法之前将ActivityClientRecord中的SubActivity Intent替换回PluginActivity Intent。
public class HCallback implements Handler.Callback //实现Callback接口
public static final int LAUNCH_ACTIVITY = 100;
@Override
public boolean handleMessage以上是关于Android插件化初体验的主要内容,如果未能解决你的问题,请参考以下文章
Android 插件化Hook 插件化框架 ( 合并 “插件包“ 与 “宿主“ 中的 Element[] dexElements | 设置合并后的 Element[] 数组 )