Android插件化的实现(2)--Hook式插件化的实现

本文深入探讨了插桩式与Hook式插件化技术的实现原理,重点介绍了如何通过Hook方式绕过系统检查,实现未注册Activity的加载,并解决了类加载问题。通过动态代理和反射技术,详细讲解了插件化框架的设计与实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       插桩式的插件化实现,每次都必须使用宿主的Context ,去加载插件的layout.xml,activity,service,对于静态广播的注册使用到反射,需要对不同版本的sdk的进行兼容。

     Hook式插件化的实现,就是通过Hook的方式,用动态代理拦截系统的源代码实现,加上自己的处理逻辑。以一个简单的小例子,来说明动态代理的原理实现 以及 逻辑。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn = findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e("TAG","内容为:"+((Button)view).getText());
              //  Toast.makeText(MainActivity.this,((Button)view).getText(),Toast.LENGTH_LONG).show();
            }
        });

        try {
            hookClick(btn);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //InvocationHandler  Object proxy 的解析
    // https://blog.youkuaiyun.com/yaomingyang/article/details/81040390
    private void hookClick(Button button) throws Exception {
        // 得到 View 的 ListenerInfo 对象
        Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
        //修改getListenerInfo为可访问(View中的getListenerInfo不是public)
        getListenerInfo.setAccessible(true);
        Object listenerInfo = getListenerInfo.invoke(button);
        // 得到 原始的 OnClickListener 对象
        Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
        Field mOnClickListenerFiled = listenerInfoClz.getDeclaredField("mOnClickListener");
        mOnClickListenerFiled.setAccessible(true);
        final Object originOnClickListener = mOnClickListenerFiled.get(listenerInfo);
        Object OnClickListenerProxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                Button button1 = new Button(MainActivity.this);
                button1.setText("我爱你中国");
                Log.e("TAG",proxy.toString());
                return method.invoke(originOnClickListener,button1);
            }
        });
        mOnClickListenerFiled.set(listenerInfo,OnClickListenerProxy);
    }
}

用动态代理Proxy.newProxyInstance来拦截onClick事件:

       // Proxy.newProxyInstance 返回所要监听类 View.OnClickListener.class 的接口类的一个实例
        // 本例中就是View.OnClickListener 本身 大部分时候 可以使用 XXXX.class.getInterfaces() 作为第二个参数
        Object OnClickListenerProxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
            /**
             *
             * @param proxy   真实对象的一个代理对象  $Proxy3 也就是button 的 OnClickListener代理对象 
             * @param method  真实代理对象(OnClickListener) 的方法
             * @param objects  方法所需要的参数
             * @return 
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                Button button1 = new Button(MainActivity.this);
                button1.setText("我爱你中国");
                Log.e("TAG",proxy.getClass().getSimpleName());
                return method.invoke(originOnClickListener,button1);
            }
        });

将Button设置的onClickListener 设置为代理对象OnClickListenerProxy ,真实对象onClickListener对象执行某个方法的时候,需要回调

InvocationHandler的的invoke方法,意思就是 我们所代理的真实对象,执行对象中的所有方法,都会回调InvocationHandler的invoke方法,因此我们可以在真实对象的某些方法调用之前 或者 之后,甚至直接替换 某个方法,不让真实对象的方法不执行 

上面中hookClick前面代码的意思 就是利用反射 替换所传入的Button的 onClickListener对象。

代理对象中的InvocationHandler的回调方法invoke的第一个参数Object proxy的详细解释可以看这篇博客 

Hook实现插件化思路

    我们知道我们使用插件化的最终目的肯定是使用插件当中类似Activity,Service的组件....或者资源文件,那么要使用插件当中的Activity就绕不开ActiviytyManagerService的检查,当我们直接使用宿主APP,去加载插件当中的Activity的时候,需要解决2个难题:

    1.我们知道在App中要想启动一个activity,那么这个activity必须在AndroidManifest.xml中注册,否则是通过不了AMS的检查的,系统会抛出异常(类似下面的异常提示),那么怎么绕过系统检查,不让宿主APP加载插件的时候抛出异常,这就是需要解决的一道难题。

 Caused by: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.android.hook.hook.plugin/com.android.hook.hook.plugin.TestActivity}; 
have you declared this activity in your AndroidManifest.xml?

   2。让系统不抛出异常之后,我们需要用宿主App的ClassLoader(PathClassLoader)去加载插件当中的activity类,会抛出找不到系统该类的错误.

让未注册的Activity绕过AMS检查

      基本流程如下:

代码实现如下(我的SDK版本是28,不同版本有差异,后面会做Hook适配):

跟踪startActivity(Intent)这个源代码发现最终 启动Activity的是调用Instrumentation的  execStartActivity 。最终调用

try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }

ActivityManager.getService().startActivity   ......  (这里就是不同版本的可能不同的hook点)  需要去拦截IActivityManager的startActivity方法

 /**
     * @hide
     */
    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;
                }
            };

通过上面分析 我们需要有几个Hook点,要想达到最终Hook IActivityManager的startService方法,我们就需要Hook它的实现类,而实现类是由上面的Singleton来实现的。

/**   ======== 此代码在 Android 28 的sdk的情况下可用=======
     * 要在执行 AMS之前,替换可用的 Activity,替换在AndroidManifest里面配置的Activity
     */
    private void hookAmsAction() throws Exception {

        // 动态代理
        Class mActivityManagerClass = Class.forName("android.app.ActivityManager");
        //通过ActivityManager 的getService 静态方法 得到IActivityManager
        Object mIActivityManager = mActivityManagerClass.getMethod("getService").invoke(null);
        // @2 的获取    动态代理
        Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");

        //得到 ActivityManager  中的 IActivityManagerSingleton
        Field mIActivityManagerSingletonField =  mActivityManagerClass.getDeclaredField("IActivityManagerSingleton");
        mIActivityManagerSingletonField.setAccessible(true);
        Object mIActivityManagerSingletonObj = mIActivityManagerSingletonField.get(null);


        final Object finalMIActivityManager = mIActivityManager;

        // 本质是IActivityManager
        Object mIActivityManagerProxy = Proxy.newProxyInstance(

                getClassLoader(),

                new Class[]{mIActivityManagerClass}, // @2 要监听的接口

                new InvocationHandler() { // IActivityManager 接口的回调方法

                    /**
                     * @param proxy
                     * @param method IActivityManager里面的方法
                     * @param args IActivityManager里面的参数
                     * @return
                     * @throws
                     */

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        if ("startActivity".equals(method.getName())) {
                            // 做自己的业务逻辑
                            // 换成 可以 通过 AMS检查的 ProxyActivity

                            // 用ProxyActivity 绕过了 AMS检查
                            Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
                            intent.putExtra("actionIntent", ((Intent) args[2])); // 把之前TestActivity保存 携带过去
                            args[2] = intent;
                        }

                        Log.e("hook", "拦截到了IActivityManager里面的方法" + method.getName());

                        // 让系统继续正常往下执行
                        return method.invoke(finalMIActivityManager, args);
                    }
                });


        // 替换点
        Class mSingletonClass = Class.forName("android.util.Singleton");
        // 获取此字段 mInstance
        Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true); // 让虚拟机不要检测 权限修饰符
        // 替换
        mInstanceField.set(mIActivityManagerSingletonObj, mIActivityManagerProxy);
    }

上面的代码放在Application的onCreate方法中执行,就能Hook住AMS的检查,让未注册的TestActivity绕过AMS的检查。

加载未注册的Activity

     上面已经绕过AMS的检查,将我们要启动的TestActivity换成了我们在AndroidManifest.xml文件中注册的ProxyActivity。绕过AMS的检查,当通过AMS的检查之后,我们就需要将ProxyActivity替换成我们需要启动的TestActivity。这就需要查看Activity的启动流程,在SDK中的Activity的启动流程如下,所经过的主要类和方法为:

Android SDK 28的源代码 Activity的启动流程 以及Hook点分析

startActivity-->最终走到AMS startActivity-->mActivityStartController.obtainStarter().execute()

-->ActivityStarter的execute方法 -->ActivityStarter的startActivityUnchecked

-->ActivityStackSupervisor.resumeFocusedStackTopActivityLocked-->ActivityStack的resumeTopActivityUncheckedLocked

-->ActivityStackSupervisor的startSpecificActivityLocked-->ActivityStackSupervisor 的realStartActivityLocked

-->ActivityStackSupervisor 的 1523-1533行代码封装一个ClientTransaction 将要运行的Intent封装成LaunchActivityItem放在里面

-->mService.getLifecycleManager().scheduleTransaction(clientTransaction);
   ClientLifecycleManager 的 scheduleTransaction方法

-->ClientTransaction的schedule() -->IApplicationThread的scheduleTransaction方法

-->ActivityThread的内部类 ApplicationThread实现了IApplicationThread

-->ClientTransactionHandler 的 scheduleTransaction -->ActivityThread.H.EXECUTE_TRANSACTION 的Message

-->ActivityThread  H(就是一个Handler)的handlerMessage case EXECUTE_TRANSACTION

-->msg.obj == ClientTransaction  就是上面封装了LaunchActivityItem 的 ClientTransaction

Hook点 :
   Hook Hander mH 的handlerMessage 的case EXECUTE_TRANSACTION 分支
   替换ClientTransaction 中的 存放的LaunchActivityItem

下面的代码就是Hook ActivityThread的相关代码,将ProxyActivity替换成要跳转的未注册的Activity

private void hookLuanchActivity() throws Exception {

        Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
        mCallbackFiled.setAccessible(true); // 授权

        /**
         * handler对象怎么来
         * 1.寻找H,先寻找ActivityThread
         *
         * 执行此方法 public static ActivityThread currentActivityThread()
         *
         * 通过ActivityThread 找到 H
         *
         */
        Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
        // 获得ActivityThrea对象
        Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);

        Field mHField = mActivityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        // 获取真正对象
        Handler mH = (Handler) mHField.get(mActivityThread);

        mCallbackFiled.set(mH, new MyCallback()); // 替换 增加我们自己的实现代码
    }

    public static final int EXECUTE_TRANSACTION = 159;

    class MyCallback implements Handler.Callback {

        @Override
        public boolean handleMessage(Message msg) {
            // 做我们在自己的业务逻辑(把ProxyActivity 换成  TestActivity)
            //实际上是一个  ClientTransaction
            Object mClientTransaction = msg.obj;
            try {
                Class mLaunchActivityItemClass = Class.forName("android.app.servertransaction.LaunchActivityItem");
                //调用ClientTransaction 的 getCallbacks方法 得到集合mActivityCallbacks
                Method getCallbacksMethod = mClientTransaction.getClass().getDeclaredMethod("getCallbacks");
                getCallbacksMethod.setAccessible(true);
                // private List<ClientTransactionItem> mActivityCallbacks;
                List mActivityCallbacks = (List) getCallbacksMethod.invoke(mClientTransaction);

                if (mActivityCallbacks == null || mActivityCallbacks.isEmpty()) {
                    return false;
                }
                //从mActivityCallbacks 中取出 最后一个 也就是我们刚刚要启动的activity
                Object mLaunchAcitiveItemObj = mActivityCallbacks.get(0);
                //得到 LaunchActivityItem 中的 private Intent mIntent 对象 就是存放ProxyActivity的intent
                if (!mLaunchActivityItemClass.isInstance(mLaunchAcitiveItemObj)) {
                    return false;
                }
                Field mIntentFiled = mLaunchActivityItemClass.getDeclaredField("mIntent");
                mIntentFiled.setAccessible(true);
                Intent mProxyIntent = (Intent) mIntentFiled.get(mLaunchAcitiveItemObj);
                // 得到我们需要启动的TestActivivty的intent
                Intent actionIntent = mProxyIntent.getParcelableExtra("actionIntent");
                if (actionIntent != null) {
                    //替换intent
                    mIntentFiled.set(mLaunchAcitiveItemObj, actionIntent);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false; // 系统不会往下执行
        }
    }

下面一段代码 ,运行插件中的PluginActivity

public void jumpActivity(View view) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.android.pluginpackager", "com.android.pluginpackager.PluginActivity"));
        startActivity(intent);
    }

会报下面的错误

Caused by: java.lang.ClassNotFoundException: Didn't find class "com.android.pluginpackager.PluginActivity"

源码分析:最终这个异常由 BaseDexClassLoader 的findClass方法抛出,下面为源码分析:


第二个Hook点:
   关于Android中的ClassLoader的源代码地址可以在线查看,现在查看地址为:https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system
PathClassLoader.loadClass  ---》 BaseDexClassLoader --》ClassLoader.loadClass--findClass(空方法) 让覆盖的子类方法去完成 --》
BaseDexClassLoader.findClass() ---》pathList.findClass

BaseDexClassLoader.findClass() -- c 为什么为null,--》 DexPathList.findClass(className) ---》DexFile.loadClassBinaryName(系列步骤后 NDK)

for遍历 dexElements == Element[] ,分析 Element 是什么 ,为什么Element.dexFile==null?

Android虚拟机 dex文件的  dex == 对Dex表现形式的描述  Element  ---  dexFile拥有可执行

为什么 Element ==null?
答:就是因为类加载机制加载的是  ---》 宿主的 classes.dex--Elements,   【没有插件的Element】

解决方案:把插件的dexElements 和 宿主中的 dexElements 融为一体  PathClassLoader 就能加载到 插件/宿主  都可以加载到了

Hook的实现

    /**
     * 把插件的dexElements 和 宿主中的 dexElements 融为一体
     */
    private void pluginToAppAction() throws Exception {
        // 第一步:找到宿主 dexElements 得到此对象   PathClassLoader代表是宿主
        PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // 本质就是PathClassLoader
        Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        // private final DexPathList pathList;
        Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object mDexPathList = pathListField.get(pathClassLoader);

        Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        // 本质就是 Element[] dexElements
        Object dexElements = dexElementsField.get(mDexPathList);

        /*** ---------------------- ***/


        // 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件
        File file = new File(Environment.getExternalStorageDirectory() + File.separator + "pluginpackage-debug.apk");
        Log.e("hook","file 的真实路径:"+file.getAbsolutePath());
        if (!file.exists()) {
            throw new FileNotFoundException("没有找到插件包!!");
        }
        String pluginPath = file.getAbsolutePath();
        File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); // data/data/包名/pluginDir/
        DexClassLoader dexClassLoader = new
                DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());

        Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
        // private final DexPathList pathList;
        Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
        pathListFieldPlugin.setAccessible(true);
        Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);

        Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
        dexElementsFieldPlugin.setAccessible(true);
        // 本质就是 Element[] dexElements
        Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);


        // 第三步:创建出 新的 dexElements []
        int mainDexLeng =  Array.getLength(dexElements);
        int pluginDexLeng =  Array.getLength(dexElementsPlugin);
        int sumDexLeng = mainDexLeng + pluginDexLeng;

        Log.e("hook","dex 的list的size="+sumDexLeng);

        // 参数一:int[]  String[] ...  我们需要Element[]
        // 参数二:数组对象的长度
        // 本质就是 Element[] newDexElements
        Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // 创建数组对象


        // 第四步:宿主dexElements + 插件dexElements =----> 融合  新的 newDexElements
        for (int i = 0; i < sumDexLeng; i++) {
            // 先融合宿主
            if (i < mainDexLeng) {
                // 参数一:新要融合的容器 -- newDexElements
                Array.set(newDexElements, i, Array.get(dexElements, i));
            } else { // 再融合插件的
                Array.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng));
            }

        }

        // 第五步:把新的 newDexElements,设置到宿主中去
        // 宿主
        dexElementsField.set(mDexPathList, newDexElements);



        // 处理加载插件中的布局
        doPluginLayoutLoad();
    }

    private Resources resources;
    private AssetManager assetManager;

    /**
     * 处理加载插件中的布局
     * Resources
     */
    private void  doPluginLayoutLoad() throws Exception {
        assetManager = AssetManager.class.newInstance();

        // 把插件的路径 给 AssetManager
        File file = new File(Environment.getExternalStorageDirectory() + File.separator + "pluginpackage-debug.apk");
        if (!file.exists()) {
            throw new FileNotFoundException("没有找到插件包!!");
        }

        // 执行此 public final int addAssetPath(String path) 方法,才能把插件的路径添加进去
        Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); // 类类型
        method.setAccessible(true);
        method.invoke(assetManager, file.getAbsolutePath());

        Resources r = getResources(); // 拿到的是宿主的 配置信息

        // 特殊:专门加载插件资源
        resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
    }

Demo中,插件中的我们自己写的ProxyActivity 必须 继承BaseActivity,在BaseActivity必须重写 getResources 和 getAssets 方法,用自己在宿主中所创建的Resources和AssetsManager来加载资源文件。 

Hook式的插件化框架,在github上有360的开源项目,github地址为https://github.com/Qihoo360/DroidPlugin

注意:Hook式插件化框架,存在缺点,就是各个不同版本的SDK,需要去适配,并且我们加载Dex的时候,将插件的class加载到主App会增加内存,比如我们上面得都是以android28的源代码为例的,对于低版本的SDK,可能Hook的方法有些小小的改变。在这里有一个原则,也是Google开发工程师在sdk版本的迭代的时候遵循的一个原则,就是Hook,利用反射的时候,尽量去Hook,反射系统public修饰的方法,或者属性,因为就算Android SDK版本迭代的时候,google工程师不会随便修改public修饰的方法,或者属性,就算是修改也会考虑到适配的问题,但是对于私有属性或者方法,google工程师可以随便修改

PS:当我们的插件与主包的包名相同并且某个Class与主包中的class名称相同,我们融合 dexElements 的时候,将插件放在整个dexElements的前面的时候,系统找到插件中的Class的时候,就不会再在数组中去查找了。这样就可以达到替换主包中某个或者某些的Class的目的,达到替换主包中有bug的class或者方法的目的

Demo传送门

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值