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 gDefault = new Singleton() {
        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 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 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

你会发现插件中的使用布局怎么是宿主的布局:

Android插件化之Activity篇_第1张图片
宿主的界面截图
Android插件化之Activity篇_第2张图片
实际的布局截图

资源加载又成了一个问题
找到ActivityThread

final ArrayMap> mPackages
            = new ArrayMap>();

替换掉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);

笔记本没有电量了,有点不详细,请见谅!

你可能感兴趣的:(Android插件化之Activity篇)