Android插件化之Activity篇
Android客户端的业务越来越多,客户端代码量也越来越显得臃肿,一般都采用组件化,将应用进行多个模块开发,但是同样不会让apk瘦起来,采用插件化则可以进行热拔插的方式进行功能模块使用起来,现在就为你讲解如何启动一个插件的Activity。
首先我们得了解ClassLoader,Android在API中给出可动态加载的有:DexClassLoader 和 PathClassLoader。
DexClassLoader:可加载jar、apk和dex,可以从SD卡中加载(本文使用这种方式)
PathClassLoader:只能加载已经安装搭配Android系统中的apk文件
我们先假设插件MuPlug.apk,是我们的一个插件apk,存放到/sdcard/目录下。
首先在需要加载插件之间合并Dex文件到BaseDexClassLoader.dexElements中(我们的代码都放到了这里)。
/**
* Dex代码注入类
* Created by Hickey on 2017/6/4 on MuDynamicLoadingHost.
*/
public class DexInject {
public static void inject(DexClassLoader dexClassLoader) {
/** 拿到本应用的PathClassLoader */
PathClassLoader pathClassLoader = (PathClassLoader) AppContext.getAppContext().getClassLoader();
try {
/** 获取宿主和插件pathList */
Object mainObj = getPathList(pathClassLoader);
Object plugObj = getPathList(dexClassLoader);
/** 获取组合之后的dexElements */
Object dexElements = combineArray(getDexElements(mainObj), getDexElements(plugObj));
/** 重新设置字段值 */
setField(mainObj, mainObj.getClass(), dexElements, "dexElements");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
/**
* 设置对象参数值
*
* @param dexPathList 此类对象
* @param cls 此类类名
* @param dexElementsValus 字段值
* @param field 字段名称
* @throws NoSuchMethodException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void setField(Object dexPathList, Class<?> cls, Object dexElementsValus, String field) throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
Field set = cls.getDeclaredField(field);
set.setAccessible(true);
set.set(dexPathList, dexElementsValus);
}
/**
* 重新组合数组
*
* @param main
* @param plug
* @return
*/
private static Object combineArray(Object main, Object plug) {
/** 获取原数组类型 */
Class<?> loadClass = main.getClass().getComponentType();
/** 获取宿主DexElements的长度 */
int mainLen = Array.getLength(main);
MuL.e("Host dex length:"+mainLen);
/** 现在的长度 */
int curLen = Array.getLength(plug) + mainLen;
Object result = Array.newInstance(loadClass, curLen);
for (int i = 0; i < curLen; ++i) {
if (i < mainLen) {
Array.set(result, i, Array.get(main, i));
} else {
Array.set(result, i, Array.get(plug, i - mainLen));
}
}
MuL.e("After adding the plugin Dex,length:" + curLen);
return result;
}
/**
* 反射获取到DexPathList对象
*
* @param pathClassLoader 类加载
* @return
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getPathList(Object pathClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(pathClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 获取某个类的全局变量
*
* @param classLoader 对象
* @param cls 类
* @param field 字段名称
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getField(Object classLoader, Class<?> cls, String field) throws NoSuchFieldException, IllegalAccessException {
Field mField = cls.getDeclaredField(field);
mField.setAccessible(true);
return mField.get(classLoader);
}
/**
* 获取dexElements
*
* @param mPathList
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object mPathList) throws NoSuchFieldException, IllegalAccessException {
return getField(mPathList, mPathList.getClass(), "dexElements");
}
}
我们知道启动Activity一般都需要在清单文件中声明才可以正常使用,否则就出现找不到Activity的异常。
在这里我们需要代理 ActivityManagerNative中的IActivityManager对象
/**
* Retrieve the system's default/global activity manager.
*/
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
public static void onProxyActivityManagerNative(){
try {
Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
/** 获取gDefault的值 */
Field gDefault = activityManagerNativeClass.getDeclaredField("gDefault");
gDefault.setAccessible(true);
Object objSingleton = gDefault.get(null);
/** 获取Singleton对象 */
Class<?> clsSingleton = Class.forName("android.util.Singleton");
/** 获取Singleton T 对象 */
Field field = clsSingleton.getDeclaredField("mInstance");
field.setAccessible(true);
Object objIActivityManager = field.get(objSingleton);
Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{
iActivityManagerInterface
}, new IActivityManagerHandler(objIActivityManager));
field.set(objSingleton, proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
这个IActivityManagerHandler:
/**
* Created by Hickey on 2017/6/4 on MuDynamicLoadingHost.
*/
public class IActivityManagerHandler implements InvocationHandler {
public static final String EXTRA_INTENT = "EXTRA_INTENT";
private Object objIActivityManager;
public IActivityManagerHandler(Object objIActivityManager) {
this.objIActivityManager = objIActivityManager;
}
/**
* 代理某些ActivityManager的某些方法
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//当接收到应用使用startActivity方法的时候
if ("startActivity".equals(method.getName())){
Pair<Integer,Intent> mPair = onFoundFirstIntentOfArgs(args);
/** Create proxy component name */
String pkgName = AppContext.getAppContext().getPackageName();
String clzName = ProxyActivity.class.getName();
ComponentName mComponentName = new ComponentName(pkgName,clzName);
Intent pIntent = new Intent();
pIntent.setComponent(mComponentName);
/** Will save the real intention object */
pIntent.putExtra(EXTRA_INTENT,mPair.second);
/** Replace intention */
args[mPair.first] = pIntent;
}
return method.invoke(objIActivityManager, args);
}
/**
* 获取对象和参数下标
* @param args
* @return
*/
private Pair<Integer, Intent> onFoundFirstIntentOfArgs(Object... args) {
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
return Pair.create(index, (Intent) args[index]);
}
}
当IActivityManager的startActivity方法被执行的时候
android.app.IActivityManager ;
public interface IActivityManager extends IInterface {
public int startActivity(IApplicationThread caller, String callingPackage, Intent intent,
String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags,
ProfilerInfo profilerInfo, Bundle options) throws RemoteException;
}
我们替换掉intent参数对象,换成我们的ProxyActivity,从而绕过AMS的检测。那我们什么时候换回来呢,我们继续hook...
我们应用都是被ActivityThread的控制来调度的,通过内部类H来进行分发消息的
final H mH = new H();
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public static final int PAUSE_ACTIVITY = 101;
public static final int PAUSE_ACTIVITY_FINISHING= 102;
public static final int STOP_ACTIVITY_SHOW = 103;
public static final int STOP_ACTIVITY_HIDE = 104;
public static final int SHOW_WINDOW = 105;
.....
}
所以我们需要创建自己的Handler.CallBack对象来处理这些消息
public static void onProxyActivityThreadmH(){
try {
Class<?> cls = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = cls.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
/** 执行方法得到ActivityThread对象 */
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
/** 由于ActivityThread一个进程只有一个,我们获取这个对象的mH */
Field mHField = cls.getDeclaredField("mH");
mHField.setAccessible(true);
/**得到H这个Handler*/
Handler mH = (Handler) mHField.get(currentActivityThread);
Field mCallBackField = Handler.class.getDeclaredField("mCallback");
mCallBackField.setAccessible(true);
mCallBackField.set(mH, new ActivityThreadHanderCallBack(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
public class ActivityThreadHanderCallBack implements Handler.Callback{
private Handler mH;
public static final int LAUNCH_ACTIVITY = 100;
public ActivityThreadHanderCallBack(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case LAUNCH_ACTIVITY:
launcherActivity(message);
break;
}
mH.handleMessage(message);
return true;
}
private void launcherActivity(Message message) {
Object obj = message.obj;//ActivityClientRecord
try {
//ActivityClientRecord取出里面的Intent对象
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxyInent = (Intent) intentField.get(obj);
//得到真实要启动的Activity的Inetnt
Intent realIntent = proxyInent.getParcelableExtra(IActivityManagerHandler.EXTRA_INTENT);
proxyInent.setComponent(realIntent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
static final class ActivityClientRecord {
IBinder token;
int ident;
Intent intent;//这个实际就是我们的ProxyActivity对象对应Intent
String referrer;
IVoiceInteractor voiceInteractor;
Bundle state;
PersistableBundle persistentState;
Activity activity;
Window window;
Activity parent;
String embeddedID;
Activity.NonConfigurationInstances lastNonConfigurationInstances;
boolean paused;
boolean stopped;
boolean hideForNow;
Configuration newConfig;
Configuration createdConfig;
Configuration overrideConfig;
// Used for consolidating configs before sending on to Activity.
private Configuration tmpConfig = new Configuration();
ActivityClientRecord nextIdle;
ProfilerInfo profilerInfo;
.......
}
这样我们就绕过了AMS去验证清单文件是否注册的问题了。
我们这样就大功告成了?没有运行项目:
Caused by: java.lang.IllegalArgumentException: android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{com.android.mudl/com.android.mudl.plug.PlugMainActivity}
at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:284)
at android.support.v7.app.AppCompatDelegateImplV7.onCreate(AppCompatDelegateImplV7.java:152)
at android.support.v7.app.AppCompatDelegateImplV14.onCreate(AppCompatDelegateImplV14.java:46)
at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:73)
at com.android.mudl.plug.PlugMainActivity.onCreate(PlugMainActivity.java:14)
at android.app.Activity.performCreate(Activity.java:6910)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2746)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
at com.hickey.mudl.ActivityThreadHanderCallBack.handleMessage(ActivityThreadHanderCallBack.java:31)
at android.os.Handler.dispatchMessage(Handler.java:101)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6524)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:941)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831)
从错误日志看到,我们实际已经偷梁换柱成功,可是在使用AppCompatActivity时,它又去向PackageManger去检测父类Activity,没找到。那怎么办,我们继续hook!
//同样的从ActivityThread入手,找到sPackageManager,代理它
static IPackageManager sPackageManager;
//具体反射代码
public static void onHookIPackageManager() {
try {
// 兼容AppCompatActivity报错问题
Class<?> forName = Class.forName("android.app.ActivityThread");
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThread = field.get(null);
Method getPackageManager = activityThread.getClass().getDeclaredMethod("getPackageManager");
Object iPackageManager = getPackageManager.invoke(activityThread);
PackageManagerHandler handler = new PackageManagerHandler(iPackageManager);
Class<?> iPackageManagerIntercept = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{iPackageManagerIntercept}, handler);
// 获取 sPackageManager 属性
Field iPackageManagerField = activityThread.getClass().getDeclaredField("sPackageManager");
iPackageManagerField.setAccessible(true);
iPackageManagerField.set(activityThread, proxy);
}catch (Exception e){
MuL.e("onHookIPackageManager:"+e.toString());
}
这里是找到上面验证失败的方法getActivityInfo,将里面的ComponentName对象换成ProxyActivity的。
public static class PackageManagerHandler implements InvocationHandler {
public Object iPackageManager;
public PackageManagerHandler(Object iPackageManager) {
this.iPackageManager = iPackageManager;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getActivityInfo".equals(method.getName())){
for (int i=0;i<args.length;i++){
if (args[i] instanceof ComponentName){
ComponentName componentName = new ComponentName(AppContext.getAppContext().getPackageName(), ProxyActivity.class.getName());
args[i] = componentName;
}
}
}
return method.invoke(iPackageManager,args);
}
}
你会发现插件中的使用布局怎么是宿主的布局:
宿主的界面截图 实际的布局截图资源加载又成了一个问题
找到ActivityThread
final ArrayMap<String, WeakReference<LoadedApk>> mPackages
= new ArrayMap<String, WeakReference<LoadedApk>>();
替换掉LoadedApk中的mResDir参数:变成我们插件的路径:
public static void switchToPlugResources(String resPath) {
try {
String packageName = AppContext.getAppContext().getPackageName();
//获取LoadedApk的Class
Class<?> loadApkCls = Class.forName("android.app.LoadedApk");
//获取ActivityThread的Class
Class<?> activityThreadCls = Class.forName("android.app.ActivityThread");
//获取ActivityThread对象
Method currentActivityThreadMethod = activityThreadCls.getMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
//反射获取mPackages中的LoadedApk
Field filed = activityThreadCls.getDeclaredField("mPackages");
filed.setAccessible(true);
Map mPackages = (Map) filed.get(currentActivityThread);
WeakReference wr = (WeakReference) mPackages.get(packageName);
Field filed2 = loadApkCls.getDeclaredField("mResDir");
filed2.setAccessible(true);
filed2.set(wr.get(), resPath);
}catch (Exception e){
MuL.e("changeResDir:"+e.toString());
}
}
这样就成功启动插件中的Activity且支持AppCompatActivity.
由于当前应用的资源路径变换了,我们需要在适当的是将资源路径变回来。
我们通过如上方式重新将资源路径变换回来,所有方法的调用顺序如下
public class InitRunable implements Runnable {
@Override
public void run() {
MuL.e("Step1:Merge plugins and host Dex.");
String cacheDir = MainActivity.this.getCacheDir().getAbsolutePath();
String apkPath = Environment.getExternalStorageDirectory() + File.separator + "MuPlug.apk";
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, cacheDir, cacheDir, getClassLoader());
DexInject.inject(dexClassLoader);
MuL.e("Step2:Agent IActivityManager Object.");
ActivityManagerHook.onProxyActivityManagerNative();
MuL.e("Step3:Agent ActivityThread mH object.");
ActivityThreadHandlerHook.onProxyActivityThreadmH();
/*MuL.e("Step4:Get ActivityThread sInstrumentation Object.");
ActivityThreadHandlerHook.onProxyActivityInstrumentation(MainActivity.this);*/
/** Switch to the plug-in resource directory */
MuL.e("Step4:Switch to the plug-in resource directory");
LoadApkResDir.switchToPlugResources(apkPath);
IPackageManagerHook.onHookIPackageManager();
runOnUiThread(new Runnable() {
@Override
public void run() {
compatButton.setEnabled(true);
}
});
}
}
//将资源路径变成我们的apk路径
LoadApkResDir.switchToPlugResources(getApplicationInfo().sourceDir);
笔记本没有电量了,有点不详细,请见谅!