Android插件化技术之加载未安装APK

news2024/11/10 7:08:43

目录

  • 1、概述
  • 2、HOOK技术
    • 2.1、根据Android9.0系统源码,查看Activity.startActivity(intent,...)的调用流程
    • 2.2、根据Android9.0系统源码,查看Context.startActivity(intent,...)的调用流程
  • 3、最终解决方案
    • 3.1、实现思路
    • 3.2、演示效果
  • 4、最终代码实现
    • 4.1、创建代理类:InstrumentationProxy.java
    • 4.2、创建插件上下文类:PluginContext.java
    • 4.3、宿主中TestActivity调用
    • 4.4、宿主与插件的主题
    • 4.5、插件依赖库保持简单一点
    • 4.6、反射工具类Reflect.java
    • 4.7、反射异常类ReflectException.java
  • 5、实践过程中的各种报错及原因
    • 5.1、Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x101005a a=-1}
    • 5.2、'android.view.Window$Callback android.view.Window.getCallback()' on a null object reference
    • 5.3、Exception while getting ActivityInfo
    • 5.4、You need to use a Theme.AppCompat theme (or descendant) with this activity.
  • 6、参考资料

1、概述

Android插件化是一种解决方案,当一个应用发展成一个平台级应用时,就更需要针对各个子业务模块按需动态加载,要做到按需动态加载一种是可以通过H5的方案,另一种就是
针对各个子业务模块单独开发成一个APK,这时候这个平台级应用我们称为宿主,子业务模块APK称为插件,宿主通过反射点击去学习、代理点击去学习等实现hook技术来完成插件APK的免安装加载。

所以必须要先了解这个HOOK技术。

2、HOOK技术

HOOK翻译成钩子,钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。在Android系统中,通俗来讲,就是去阅读源码,然后通过反射或代理的方式去替换/绕过系统原有的调用逻辑,嵌入自己的逻辑代码,从而实现业务功能的扩展。

Android的插件化过程中最重要的步骤就是在宿主应用中去打开未安装插件APK的Activity,要想实现这个过程,通过常规的API方式去调用是无法做到这一点的,startActivity只可以实现打开一个已安装应用的且android:exported="true"的Activity,所以我们必须要想办法去解决这个问题,也就是做到不安装APK也能打开其Activity。

所以,就需要通过源码去了解startActivity的调用过程,找到可以hook的地方,startActivty有三种常用的调用方式,Context.startActivity(intent,…),Activity.startActivity(intent,…),隐式调用。

2.1、根据Android9.0系统源码,查看Activity.startActivity(intent,…)的调用流程

[开始] --> [startActivity] --> [Activity.startActivityForResult]
–> [mInstrumentation.execStartActivity] --> [ActivityManager.getService().startActivity]
–> [ActivityManagerService.startActivity] --> [ActivityManagerService.startActivityAsUser]
–> [ActivityStarter.execute()] --> [startActivity] --> [startActivityUnchecked]
–> [ActivityStackSupervisor.resumeFocusedStackTopActivityLocked] --> [ActivityStack.resumeTopActivityUncheckedLocked]
–> [resumeTopActivityInnerLocked] --> [ClientLifecycleManager.scheduleTransaction]
–> [ClientTransaction.schedule] --> [IApplicationThread.scheduleTransaction]
–> [ActivityThread.this.scheduleTransaction] --> [ClientTransactionHandler.sendMessage]
–> [TransactionExecutor.execute] --> [executeCallbacks] --> [ClientTransactionItem.execute]
–> [LaunchActivityItem.execute] --> [ClientTransactionHandler.handleLaunchActivity]
–> [ActivityThread.handleLaunchActivity] --> [performLaunchActivity]
–> [mInstrumentation.newActivity] --> [mInstrumentation.callActivityOnCreate]
–> [Activity.performCreate] --> [Activity.onCreate] --> [结束]

Activity Instrumentation ActivityManagerService ActivityStarter ActivityStackSupervisor ActivityStack ClientLifecycleManager ClientTransaction IApplicationThread ClientTransactionHandler TransactionExecutor ClientTransactionItem LaunchActivityItem ActivityThread startActivityForResult execStartActivity startActivityAsUser startActivityUnchecked resumeFocusedStackTopActivityLocked resumeTopActivityInnerLocked scheduleTransaction schedule sendMessage execute execute execute handleLaunchActivity handleLaunchActivity performLaunchActivity newActivity、callActivityOnCreate、performCreate onCreate Activity Instrumentation ActivityManagerService ActivityStarter ActivityStackSupervisor ActivityStack ClientLifecycleManager ClientTransaction IApplicationThread ClientTransactionHandler TransactionExecutor ClientTransactionItem LaunchActivityItem ActivityThread

2.2、根据Android9.0系统源码,查看Context.startActivity(intent,…)的调用流程

[开始] --> [getBaseContext.startActivity] --> [ContextImpl.startActivity] --> [mMainThread.getInstrumentation().execStartActivity] --> [ActivityManager.getService().startActivity] --> …之后同上Activity.startActivity… --> [结束]

看源码的网站,随便推荐一个: 点击查看Android系统源码 。

3、最终解决方案

3.1、实现思路

现在开始来选取Hook的注入点,主要是针对Instrumentation这个类进行代理扩展系统业务功能,execStartActivity执行前,先把传入的插件APK目标Intent替换成我们在宿主项目中创建的已在manifest.xml中注册的替身类ShaowActivity,绕过Manifest注册检查(对应的checkStartActivityResult函数就是做这个检查的),然后我们选择newActivity作为关键点,创建新的Activity时使用插件APK的Activity,创建好插件APK的Activity之后,还需要替换插件的资源(因为现在的资源还是宿主的),故我们选择在callActivityOnCreate这个地方作为替换插件APK的上下文、Resources、Theme等等资源的关键点,具体操作就是需要构建插件的上下文然后通过反射进行替换。(多多参考学习)

3.2、演示效果

在这里插入图片描述

4、最终代码实现

4.1、创建代理类:InstrumentationProxy.java



import android.app.Activity;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PersistableBundle;
import android.util.Log;

import com.xx.escape.plan.ProxyActivity;
import com.xx.escape.plan.plugin.PluginContext;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @author james
 * @date 2024-08-09 ~ 2024-08-23 调试完毕
 * @brief description
 * 标准版
 */
public class InstrumentationProxy extends Instrumentation {
    public static final String TAG = InstrumentationProxy.class.getSimpleName();
    Instrumentation mInstrumentation;

    static final String KEY_TARGET_INTENT = "key_target_intent";

    public InstrumentationProxy(Instrumentation instrumentation) {
        mInstrumentation = instrumentation;
    }

    private PluginContext mPluginContext;

    public void inject(PluginContext pluginContext) {
        this.mPluginContext = pluginContext;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        Log.d(TAG, "\n执行了execStartActivity, 参数如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");

        try {
            //List<ResolveInfo> infoList = who.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);//这种写法不兼容6.0以下的设备
            boolean isPluginActivity = isUnregisteredManifestActivity((Activity) who, intent);
            //替换系统传递的intent(这个意图里面就是我们要访问的插件APK的Activity界面,直接访问插件APK里面的Activity是会被系统拦截的,因为没有注册manifest.xml,所以我们需要伪装)为我们指定的目标targetIntent,通过伪装成访问宿主的ProxyActivity,跳过manifest.xml的注册检测
            Intent targetIntent;
            if (isPluginActivity) {
                targetIntent = new Intent(who, ProxyActivity.class);
                //塞入到Extra中,执行到后面的newActivity再进行还原
                targetIntent.putExtra(KEY_TARGET_INTENT, intent);
            } else {
                targetIntent = intent;
            }
            Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class,
                    Activity.class, Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, targetIntent, requestCode, options);
        } catch (Exception e) {
            throw new RuntimeException("don't support context start!");
        }
    }

    /**
     * 隐式调用
     *
     * @param intent
     * @return
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     */
    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, String target,
            Intent intent, int requestCode, Bundle options) {
        Log.d(TAG, "\n执行了execStartActivity, 参数如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");

        try {
            boolean isPluginActivity = isUnregisteredManifestActivity((Activity) who, intent);
            //替换系统传递的intent(这个意图里面就是我们要访问的插件APK的Activity界面,直接访问插件APK里面的Activity是会被系统拦截的,因为没有注册manifest.xml,所以我们需要伪装)为我们指定的目标targetIntent,通过伪装成访问宿主的ProxyActivity,跳过manifest.xml的注册检测
            Intent targetIntent;
            if (isPluginActivity) {
                targetIntent = new Intent(who, ProxyActivity.class);
                //塞入到Extra中,执行到后面的newActivity再进行还原
                targetIntent.putExtra(KEY_TARGET_INTENT, intent);
            } else {
                targetIntent = intent;
            }

            Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class,
                    Activity.class, Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, targetIntent, requestCode, options);
        } catch (Exception e) {
            throw new RuntimeException("don't support context start!");
        }
    }

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        if (intent != null) {
            Intent targetIntent = intent.getParcelableExtra(KEY_TARGET_INTENT);
            //通过插件的ClassLoader来载入一个插件的Activity
            if (targetIntent != null && targetIntent.getComponent() != null) {
                ComponentName componentName = targetIntent.getComponent();
                return mInstrumentation.newActivity(mPluginContext.getClassLoader(), componentName.getClassName(), targetIntent);
            }
        }
        return mInstrumentation.newActivity(cl, className, intent);
    }

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        injectActivity(activity);
        mInstrumentation.callActivityOnCreate(activity, icicle);
    }

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
        injectActivity(activity);
        mInstrumentation.callActivityOnCreate(activity, icicle, persistentState);
    }


    /**
     * 载入插件的Activity之后,就可以替换这个插件的Activity属性,mResources、mBase、mApplication
     *
     * @param activity
     */
    private void injectActivity(Activity activity) {
        try {

            Intent targetIntent = activity.getIntent().getParcelableExtra(KEY_TARGET_INTENT);
            if (targetIntent == null) {
                Log.d(TAG, "没有目标参数,不是需要启动的插件Activity");
                return;
            }

            boolean isPluginActivity = isUnregisteredManifestActivity(activity, targetIntent);
            if (isPluginActivity) {
                Context base = activity.getBaseContext();

                Reflect.on(activity).set("mBase", mPluginContext);
                Reflect.on(base).set("mResources", mPluginContext.getResources());
                Reflect.on(activity).set("mResources", mPluginContext.getResources());
                Reflect.on(activity).set("mApplication", mPluginContext.getApplicationContext());

                //改变主题 ,解决DecorContentParent.setWindowCallback 空指针的问题 2131689738
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    activity.setTheme(mPluginContext.getTheme());
                } else {
                    //mTheme属性是Activity没有,在Activity的父类ContextThemeWrapper中定义了这个属性 ContextThemeWrapper,也需要设置否则出现空指针的问题
                    Reflect.on(activity).set("mTheme", mPluginContext.getTheme());
                }
                //设置Title直接反射Activity的mTitle,替换成插件的即可,参考xPlugin框架
                Reflect.on(activity).set("mTitle", "插件APK标题");
                //兼容AppCompat ,替换Activity父类的 mThemeResource 这个属性值 old 2131689738/7f0f010a  0x7f0f010a	Theme.AppCompat.NoActionBar	false, true	 ; new 2131689477/7f0f0005 0x7f0f0005	AppFullScreenTheme	false, @ref/0x00000000, true, false
                //当Application 没有设置,只有Activity设置了@style/AppFullScreenTheme,则需要替换调用下面的代码;如果Application 设置了@style/AppFullScreenTheme,则不需要执行下面这段代码
                setActivityResIdTheme(activity);
            }
        } catch (Exception ex) {
            Log.e("InjectActivity", "Error during injectActivity", ex);
        }
    }

    /**
     * 根据是否在Manifest中注册了Activity,来判断是否是插件Activity;
     *
     * @param activity
     * @return
     */
    private boolean isUnregisteredManifestActivity(Activity activity, Intent targetIntent) {
        try {
            ComponentName component = targetIntent.getComponent();
            // 获取包管理器
            PackageManager packageManager = activity.getPackageManager();
            // 获取当前应用的包信息
            PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_ACTIVITIES);
            // 获取注册的所有 Activity
            ActivityInfo[] activities = packageInfo.activities;
            if (activities != null) {
                for (ActivityInfo activityInfo : activities) {
                    String activityName = activityInfo.name;
                    String targetIntentName = component.getClassName();
                    if (activityName.equals(targetIntentName)) {
                        Log.d(TAG, "已在宿主项目中注册,不是需要启动的插件Activity");
                        return false;
                    }
                }
            } else {
                Log.i(TAG, "No activities registered in the manifest.");
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return true;
    }

    final Map<String, ActivityInfo> activityMap = new HashMap<>();   // <cl

    /**
     * 为了支持AppCompat.Theme主题,必须要执行此函数,否则会一直报错
     * java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
     * 是从ActivityInfo中取获取theme并设置到Activity中,而不能直接从,activity变量中的mTheme变量是null的值或者说是Activity的样式而不是我们需要的AppCompatActivity中的mTheme样式,所以需要从ActivityInfo中去获取,ActivityInfo右是根据ApplicationInfo来的
     * 参考了xPlugin 框架
     *
     * @param activity
     */
    private void setActivityResIdTheme(Activity activity) {
        PackageManager pm = mPluginContext.getApplicationContext().getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(mPluginContext.getPluginPath(),
                PackageManager.GET_META_DATA | PackageManager.GET_ACTIVITIES |
                        PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS |
                        PackageManager.GET_PROVIDERS);
        if (pkgInfo != null) {
            if (pkgInfo.activities != null) {
                ApplicationInfo appInfo = pkgInfo.applicationInfo;
                int appTheme = appInfo.theme;
                int appLabelRes = appInfo.labelRes;
                int appIcon = appInfo.icon;

                for (ActivityInfo info : pkgInfo.activities) {
                    info.theme = info.theme != 0 ? info.theme : appTheme;
                    info.labelRes = info.labelRes != 0 ? info.labelRes : appLabelRes;
                    info.icon = info.icon != 0 ? info.icon : appIcon;
                    String className = info.name.startsWith(".") ?
                            info.packageName + info.name : info.name;
                    activityMap.put(className, info);
                }

                ActivityInfo activityInfo = activityMap.get(activity.getClass().getName());
                if (activityInfo != null) {
                    ActivityInfo activityInfoNew = new ActivityInfo(activityInfo);
                    if (activityInfoNew.theme != 0) {
                        // 虽然ContextThemeWrapper已通过反射更改了Theme,但是Activity又重写了这个类的setTheme(int resId)函数,所以还需要在这个地方设置theme,注意这个theme是int类型的,所以可以从插件apk中的ActivityInfo来取到这个int值,这个地方需要多次尝试
                        // 设置方式1、调用公开访问的API ,推荐使用这种方式
                        activity.setTheme(activityInfoNew.theme);
                        //  设置方式2、也可以使用反射来间接调用
                        /**
                         * Reflect.on(activity).set("mThemeResource", activityInfoNew.theme);
                         Reflect.on(activity).call("initializeTheme");
                         Reflect.on(activity.getWindow()).call("setTheme", activityInfoNew.theme);
                         */
                    }
                }
            }
        }
    }
}


4.2、创建插件上下文类:PluginContext.java

mport android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.view.ContextThemeWrapper;

import java.lang.reflect.Method;

/**
 * @author james
 * @date 2024-08-20
 * @brief description
 * 独立构造一个新的插件的上下文
 * 其实就是自己扩展一个ContextThemeWrapper,来new 出一个插件的上下文
 */
public class PluginContext extends ContextThemeWrapper {

    private Context context;
    private Application application;
    private Resources resources;
    private ClassLoader classLoader;
    private AssetManager assetManager;
    private Resources.Theme theme;

    public String getPluginPath() {
        return pluginPath;
    }

    private String pluginPath;


    public PluginContext(Context context, Application application, ClassLoader classLoader, String pluginPath) {
        super(context, 0);
        this.context = context;
        this.application = application;
        this.classLoader = classLoader;
        this.pluginPath = pluginPath;

        generateResources();
    }

    private void generateResources() {
        try {
            assetManager = AssetManager.class.newInstance();
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
            method.setAccessible(true);
            method.invoke(assetManager, pluginPath);

            resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public Context getApplicationContext() {
        return application;
    }

    public PackageManager getPackageManager() {
        return context.getPackageManager();
    }

    @Override
    public AssetManager getAssets() {
        return getResources().getAssets();
    }

    @Override
    public Resources getResources() {
        return resources;
    }

    @Override
    public ClassLoader getClassLoader() {
        return classLoader;
    }

    /**
     * 必须要重新构建这个主题,不然会报一个错误
     * Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
     *
     * @return
     */
    @Override
    public Resources.Theme getTheme() {
        if (this.theme == null) {
            Resources.Theme oldTheme = super.getTheme();
            this.theme = this.getResources().newTheme();
            this.theme.setTo(oldTheme);
        }
        return this.theme;
    }

}

4.3、宿主中TestActivity调用

public class TestActivity extends AppCompatActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		//初始化插件上下文
		initPluginContext();

		//绕过getActivityInfo检查,解除不能打开未安装应用的Activity的限制
        hookPackageManager();
      
		//Context.startActivity(intent)方式,注入InstrumentationProxy代理类
        attachContext();
		//注意Context与Activity的startActivity函数对应的Instrumentation是不一样的,Context.startActivity对应的是ActivityThread中的mInstrumentation,实现类是ContextImpl,而Activity.startActivity对应的是Activity中的mInstrumentation,所以都需要Hook
		
		//Activity.startActivity(intent)方式,注入InstrumentationProxy代理类
        attachActivity();
	}
	
	
	private String pluginPath;//插件APK存放地址,你也可以自定义
    private File nativeLibDir;
    private File dexOutPath;
    private PluginContext pluginContext;//插件上下文,这是非常关键的一个类
    public static String pluginActivityName = "com.xx.plugindemo1.PluginAActivity";//插件APK内的Activity类名
    public static String pluginPackageName = "com.xx.plugindemo1";//插件APK内的包名
    private void initPluginContext() {
        String fileName = "plugin.apk";
        File filesDir = getFilesDir();//路径是:/data/data/< package name >/files/…,插件APK需要拷贝到这个路径下面,这个路径你也可以放在其他目录,只是要注意权限问题
        pluginPath = new File(filesDir, fileName).getAbsolutePath();
        nativeLibDir = new File(filesDir, "pluginlib");
        dexOutPath = new File(filesDir, "dexout");
        if (!dexOutPath.exists()) {
            dexOutPath.mkdirs();
        }
        DexClassLoader pluginClassLoader = new DexClassLoader(pluginPath, dexOutPath.getAbsolutePath(), nativeLibDir.getAbsolutePath(), this.getClassLoader());
        pluginContext = new PluginContext(this, getApplication(), pluginClassLoader, pluginPath);
    }
	
	private void hookPackageManager() {

        // 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
        // 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.

        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");

            Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            Object currentActivityThread = currentActivityThreadMethod.invoke(null);

            // 获取ActivityThread里面原始的 sPackageManager
            Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
            sPackageManagerField.setAccessible(true);
            Object sPackageManager = sPackageManagerField.get(currentActivityThread);

            // 准备好代理对象, 用来替换原始的对象
            Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
            Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
                    new Class<?>[]{iPackageManagerInterface},
                    new IPackageManagerHookHandler(sPackageManager));

            // 1. 替换掉ActivityThread里面的 sPackageManager 字段
            sPackageManagerField.set(currentActivityThread, proxy);

            // set ApplicationPackageManager#mPM
            // 2. 替换 ApplicationPackageManager里面的 mPM对象
            //这段代码非常关键,解决了代理类中AppCompatDelegateImpl.java:2669)  final ActivityInfo info = pm.getActivityInfo( new ComponentName(mContext, mHost.getClass()), flags); 不能被调用的问题
            PackageManager pm = this.getPackageManager();
            Field mPmField = pm.getClass().getDeclaredField("mPM");
            mPmField.setAccessible(true);
            mPmField.set(pm, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class IPackageManagerHookHandler implements InvocationHandler {
        Object realPackageManager;

        public IPackageManagerHookHandler(Object realPackageManager) {
            this.realPackageManager = realPackageManager;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getName().equals("getActivityInfo")) {
                Log.d(InstrumentationProxy.TAG, "getActivityInfo============>" + method.getName() + ", args :" + Arrays.deepToString(args));
                ComponentName component = (ComponentName) args[0];
                Intent intent = new Intent();
                intent.setPackage(component.getPackageName());
                intent.setComponent(component);

                if (((ComponentName) args[0]).getClassName().equals(pluginActivityName)) {
                    //intent的信息(主要就是指component信息包名、类名改掉就能跳转到你指定的目标)已被替换了,开始还原为系统已注册的Activity
                    intent.setClassName(component.getPackageName(), ProxyActivity.class.getName());
                    //更改原始的com.xx.escape.plan/com.xx.plugindemo1.PluginAActivity (未注册)变成已注册的(com.xx.escape.plan/com.xx.escape.plan.ProxyActivity)
                    args[0] = intent.getComponent();
                    ActivityInfo info = (ActivityInfo) method.invoke(realPackageManager, args);
                    Log.d(InstrumentationProxy.TAG, "info1========> " + info);
                    return info;
                } else {
                    ActivityInfo info = (ActivityInfo) method.invoke(realPackageManager, args);
                    Log.d(InstrumentationProxy.TAG, "info2========> " + info);
                    return info;
                }
            }
            return method.invoke(realPackageManager, args);
        }
    }
	
	private void attachContext() {
        try {

            // 获取ActivityThread类的全名
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            //根据ActivityThread类的全名访问其静态方法currentActivityThread(),主线程只有一个
            Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            //调用currentActivityThread()方法返回当前的ActivityThread
            Object realActivityThread = currentActivityThreadMethod.invoke(null);

            //拿到原始的mInstrumentation
            Field realInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
            realInstrumentationField.setAccessible(true);
            //注意是根据currentActivityThread来获取的属性值
            Instrumentation realInstrumentation = (Instrumentation) realInstrumentationField.get(realActivityThread);

            //创建重载函数的子类对象
            InstrumentationProxy myInstrumentation = new InstrumentationProxy(realInstrumentation);
            //开始为原来的字段,通过原来的对象赋予新的字段值
            realInstrumentationField.set(realActivityThread, myInstrumentation);

            //TODO 注入初始化
            myInstrumentation.inject(pluginContext);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 只调用这个方法有缺陷,会导致newActivity不会回调,但是可以结合上面 attachContext() 一起调用,就能回调newActivity,只是创建了两个InstrumentationProxy 代理对象
     */
    private void attachActivity() {
        try {
            // 获取Activity类的全名
            Class<?> activityClass = Class.forName("android.app.Activity");
            //根据Activity类的全名访问其静态方法获取其私有属性mInstrumentation
            Field instrumentationField = activityClass.getDeclaredField("mInstrumentation");

            instrumentationField.setAccessible(true);
            //注意是根据this(当前Activity)来获取的属性值
            Instrumentation realInstrumentation = (Instrumentation) instrumentationField.get(this);

            //创建重载函数的子类对象
            InstrumentationProxy myInstrumentation = new InstrumentationProxy(realInstrumentation);
            //通过this(当前Activity)为原来的字段赋予新的字段值
            instrumentationField.set(this, myInstrumentation);
            //TODO 注入初始化
            myInstrumentation.inject(pluginContext);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
	
	//注册一个xml的点击事件,打开插件的Activity,支持Activity及AppCompatActivity
	public void viewOpenPluginActivity(View view) {
        // 根据apk路径加载apk代码到DexClassLoader中
        try {
            ClassLoader classLoader = pluginContext.getClassLoader();
            Class clazz = classLoader.loadClass(pluginActivityName);
            Intent intent = new Intent(TestBActivity.this, clazz);
            intent.setPackage(pluginPackageName);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            getBaseContext().startActivity(intent);//Context.startActivity这种方式支持
            //startActivity(intent); //Activity.startActivity这种方式也支持
        } catch (Exception e) {
            Log.d(InstrumentationProxy.TAG, "TestBActivity==========>" + e.getMessage());
        }
    }
}

4.4、宿主与插件的主题

建议保持一致,可以避免很多奇怪的问题,最关键的是要支持Theme.AppCompat ,这个主题直接关系到extends AppCompatActivity会不会报错。
插件APK的主题设置< application android:theme=“@style/AppFullScreenTheme” >如下:

<resources>
    <!-- Base application theme. -->
    <style name="Theme.PluginDemo1" parent="Theme.AppCompat.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryDark">@color/purple_700</item>
        <item name="colorAccent">@color/white</item>
    </style>

    <style name="AppFullScreenTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowNoTitle">false</item>
        <item name="android:windowActionBar">false</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>
</resources>

宿主的主题设置< application android:theme=“@style/Theme.EscapePlanDemo” >如下:
宿主可以随便一点,只是插件主题设置需要有要求。

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.EscapePlanDemo" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryDark">@color/purple_700</item>
        <item name="colorAccent">@color/white</item>

    </style>
</resources>

4.5、插件依赖库保持简单一点

在插件APK中,每新增一个库都有可能导致不兼容,所以加库需谨慎。
插件的APP的build.gradle 配置如下如下:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.xx.plugindemo1'
    compileSdk 32

    defaultConfig {
        applicationId "com.xx.plugindemo1"
        minSdk 19
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

4.6、反射工具类Reflect.java

实现参考如下:


import java.lang.reflect.*;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 一个拥有流畅特性(Fluent-API)的反射工具类, 使用起来就像直接调用一样流畅易懂.
 *
 * @author Lody
 */
public class Reflect {

    private final Object object;
    private final boolean isClass;
    private boolean isSuper;

    private Reflect(Class<?> type) {
        this.object = type;
        this.isClass = true;
    }

    private Reflect(Object object) {
        this.object = object;
        this.isClass = false;
    }

    /**
     * 根据指定的类名构建反射工具类
     *
     * @param name 类的全名
     * @return 反射工具类
     * @throws ReflectException 如果反射出现意外
     * @see #on(Class)
     */
    public static Reflect on(String name) throws ReflectException {
        return on(forName(name));
    }

    /**
     * 从指定的类加载起寻找类,并构建反射工具类
     *
     * @param name        类的全名
     * @param classLoader 需要构建工具类的类的类加载器 loaded.
     * @return 反射工具类
     * @throws ReflectException 如果反射出现意外
     * @see #on(Class)
     */
    public static Reflect on(String name, ClassLoader classLoader) throws ReflectException {
        return on(forName(name, classLoader));
    }

    /**
     * 根据指定的类构建反射工具类
     * <p>
     * 当你需要访问静态字段的时候本方法适合你, 你还可以通过调用 {@link #create(Object...)} 创建一个对象.
     *
     * @param clazz 需要构建反射工具类的类
     * @return 反射工具类
     */
    public static Reflect on(Class<?> clazz) {
        return new Reflect(clazz);
    }

    // ---------------------------------------------------------------------
    // 构造器
    // ---------------------------------------------------------------------

    /**
     * Wrap an object.
     * <p>
     * Use this when you want to access instance fields and methods on any
     * {@link Object}
     *
     * @param object The object to be wrapped
     * @return A wrapped object, to be used for further reflection.
     */
    public static Reflect on(Object object) {
        return new Reflect(object);
    }

    /**
     * 让一个{@link AccessibleObject}可访问.
     *
     * @param accessible
     * @param <T>
     * @return
     */
    public static <T extends AccessibleObject> T accessible(T accessible) {
        if (accessible == null) {
            return null;
        }

        if (accessible instanceof Member) {
            Member member = (Member) accessible;

            if (Modifier.isPublic(member.getModifiers())
                    && Modifier.isPublic(member.getDeclaringClass().getModifiers())) {

                return accessible;
            }
        }
        if (!accessible.isAccessible()) {
            accessible.setAccessible(true);
        }

        return accessible;
    }

    // ---------------------------------------------------------------------
    // Fluent Reflection API
    // ---------------------------------------------------------------------

    /**
     * 将给定字符串的开头改为小写.
     *
     * @param string
     * @return
     */
    private static String property(String string) {
        int length = string.length();

        if (length == 0) {
            return "";
        } else if (length == 1) {
            return string.toLowerCase();
        } else {
            return string.substring(0, 1).toLowerCase() + string.substring(1);
        }
    }

    private static Reflect on(Constructor<?> constructor, Object... args) throws ReflectException {
        try {
            return on(accessible(constructor).newInstance(args));
        } catch (Exception e) {
            throw new ReflectException(e);
        }
    }

    private static Reflect on(Method method, Object object, Object... args) throws ReflectException {
        try {
            accessible(method);

            if (method.getReturnType() == void.class) {
                method.invoke(object, args);
                return on(object);
            } else {
                return on(method.invoke(object, args));
            }
        } catch (Exception e) {
            throw new ReflectException(e);
        }
    }

    /**
     * 取得内部维护的对象.
     */
    private static Object unwrap(Object object) {
        if (object instanceof Reflect) {
            return ((Reflect) object).get();
        }

        return object;
    }

    /**
     * 将Object数组转换为其类型的数组. 如果对象中包含null,我们用NULL.class代替.
     *
     * @see Object#getClass()
     */
    private static Class<?>[] types(Object... values) {
        if (values == null) {
            return new Class[0];
        }

        Class<?>[] result = new Class[values.length];

        for (int i = 0; i < values.length; i++) {
            Object value = values[i];
            result[i] = value == null ? NULL.class : value.getClass();
        }

        return result;
    }

    /**
     * 取得一个类,此操作会初始化类的static区域.
     *
     * @see Class#forName(String)
     */
    private static Class<?> forName(String name) throws ReflectException {
        try {
            return Class.forName(name);
        } catch (Exception e) {
            throw new ReflectException(e);
        }
    }

    private static Class<?> forName(String name, ClassLoader classLoader) throws ReflectException {
        try {
            return Class.forName(name, true, classLoader);
        } catch (Exception e) {
            throw new ReflectException(e);
        }
    }

    /**
     * 如果给定的Class是原始类型,那么将其包装为对象类型, 否则返回本身.
     */
    public static Class<?> wrapper(Class<?> type) {
        if (type == null) {
            return null;
        } else if (type.isPrimitive()) {
            if (boolean.class == type) {
                return Boolean.class;
            } else if (int.class == type) {
                return Integer.class;
            } else if (long.class == type) {
                return Long.class;
            } else if (short.class == type) {
                return Short.class;
            } else if (byte.class == type) {
                return Byte.class;
            } else if (double.class == type) {
                return Double.class;
            } else if (float.class == type) {
                return Float.class;
            } else if (char.class == type) {
                return Character.class;
            } else if (void.class == type) {
                return Void.class;
            }
        }

        return type;
    }

    /**
     * 取得内部维护的实际对象
     *
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T get() {
        return (T) object;
    }

    /**
     * 设置指定字段为指定值
     *
     * @param name
     * @param value
     * @return
     * @throws ReflectException
     */
    public Reflect set(String name, Object value) throws ReflectException {
        try {
            Field field = field0(name);
            field.setAccessible(true);
            field.set(object, unwrap(value));
            return this;
        } catch (Exception e) {
            throw new ReflectException(e);
        }
    }

    /**
     * @param name name
     * @param <T>  type
     * @return object
     * @throws ReflectException
     */
    public <T> T get(String name) throws ReflectException {
        return field(name).get();
    }

    /**
     * 取得指定名称的字段
     *
     * @param name name
     * @return reflect
     * @throws ReflectException
     */
    public Reflect field(String name) throws ReflectException {
        try {
            Field field = field0(name);
            return on(field.get(object));
        } catch (Exception e) {
            throw new ReflectException(object.getClass().getName(), e);
        }
    }

    private Field field0(String name) throws ReflectException {
        Class<?> type = type();

        // 先尝试取得公有字段
        try {
            return type.getField(name);
        }

        // 此时尝试非公有字段
        catch (NoSuchFieldException e) {
            do {
                try {
                    return accessible(type.getDeclaredField(name));
                } catch (NoSuchFieldException ignore) {
                }

                type = type.getSuperclass();
            } while (type != null);

            throw new ReflectException(e);
        }
    }

    /**
     * 取得一个Map,map中的key为字段名,value为字段对应的反射工具类
     *
     * @return Map
     */
    public Map<String, Reflect> fields() {
        Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
        Class<?> type = type();

        do {
            for (Field field : type.getDeclaredFields()) {
                if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
                    String name = field.getName();

                    if (!result.containsKey(name))
                        result.put(name, field(name));
                }
            }

            type = type.getSuperclass();
        } while (type != null);

        return result;
    }

    /**
     * 调用指定的无参数方法
     *
     * @param name
     * @return
     * @throws ReflectException
     */
    public Reflect call(String name) throws ReflectException {
        return call(name, new Object[0]);
    }

    /**
     * 调用方法根据传入的参数
     *
     * @param name
     * @param args
     * @return
     * @throws ReflectException
     */
    public Reflect call(String name, Object... args) throws ReflectException {
        Class<?>[] types = types(args);

        try {
            Method method = exactMethod(name, types);
            return on(method, object, args);
        } catch (NoSuchMethodException e) {
            try {
                Method method = similarMethod(name, types);
                return on(method, object, args);
            } catch (NoSuchMethodException e1) {
                throw new ReflectException(e1);
            }
        }
    }

    public Method exactMethod(String name, Class<?>[] types) throws NoSuchMethodException {
        Class<?> type = type();

        try {
            return type.getMethod(name, types);
        } catch (NoSuchMethodException e) {
            do {
                try {
                    return type.getDeclaredMethod(name, types);
                } catch (NoSuchMethodException ignore) {
                }

                type = type.getSuperclass();
            } while (type != null);

            throw new NoSuchMethodException();
        }
    }

    /**
     * 根据参数和名称匹配方法,如果找不到方法,
     */
    private Method similarMethod(String name, Class<?>[] types) throws NoSuchMethodException {
        Class<?> type = type();

        for (Method method : type.getMethods()) {
            if (isSimilarSignature(method, name, types)) {
                return method;
            }
        }

        do {
            for (Method method : type.getDeclaredMethods()) {
                if (isSimilarSignature(method, name, types)) {
                    return method;
                }
            }

            type = type.getSuperclass();
        } while (type != null);

        throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types)
                + " could be found on type " + type() + ".");
    }

    private boolean isSimilarSignature(Method possiblyMatchingMethod, String desiredMethodName,
                                       Class<?>[] desiredParamTypes) {
        return possiblyMatchingMethod.getName().equals(desiredMethodName)
                && match(possiblyMatchingMethod.getParameterTypes(), desiredParamTypes);
    }

    /**
     * 创建一个实例通过默认构造器
     *
     * @return Reflect
     * @throws ReflectException
     */
    public Reflect create() throws ReflectException {
        return create(new Object[0]);
    }

    /**
     * 创建一个实例根据传入的参数
     *
     * @param args 参数
     * @return Reflect
     * @throws ReflectException
     */
    public Reflect create(Object... args) throws ReflectException {
        Class<?>[] types = types(args);

        try {
            Constructor<?> constructor = type().getDeclaredConstructor(types);
            return on(constructor, args);
        } catch (NoSuchMethodException e) {
            for (Constructor<?> constructor : type().getDeclaredConstructors()) {
                if (match(constructor.getParameterTypes(), types)) {
                    return on(constructor, args);
                }
            }

            throw new ReflectException(e);
        }
    }

    /**
     * 创建一个动态代理根据传入的类型. 如果我们正在维护的是一个Map,那么当调用出现异常时我们将从Map中取值.
     *
     * @param proxyType 需要动态代理的类型
     * @return 动态代理生成的对象
     */
    @SuppressWarnings("unchecked")
    public <P> P as(Class<P> proxyType) {
        final boolean isMap = (object instanceof Map);
        final InvocationHandler handler = new InvocationHandler() {
            
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String name = method.getName();
                try {
                    return on(object).call(name, args).get();
                } catch (ReflectException e) {
                    if (isMap) {
                        Map<String, Object> map = (Map<String, Object>) object;
                        int length = (args == null ? 0 : args.length);

                        if (length == 0 && name.startsWith("get")) {
                            return map.get(property(name.substring(3)));
                        } else if (length == 0 && name.startsWith("is")) {
                            return map.get(property(name.substring(2)));
                        } else if (length == 1 && name.startsWith("set")) {
                            map.put(property(name.substring(3)), args[0]);
                            return null;
                        }
                    }

                    throw e;
                }
            }
        };

        return (P) Proxy.newProxyInstance(proxyType.getClassLoader(), new Class[]{proxyType}, handler);
    }

    /**
     * 检查两个数组的类型是否匹配,如果数组中包含原始类型,将它们转换为对应的包装类型.
     */
    private boolean match(Class<?>[] declaredTypes, Class<?>[] actualTypes) {
        if (declaredTypes.length == actualTypes.length) {
            for (int i = 0; i < actualTypes.length; i++) {
                if (actualTypes[i] == NULL.class)
                    continue;

                if (wrapper(declaredTypes[i]).isAssignableFrom(wrapper(actualTypes[i])))
                    continue;

                return false;
            }

            return true;
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public int hashCode() {
        return object.hashCode();
    }

    /**
     * {@inheritDoc}
     */
    public boolean equals(Object obj) {
        return obj instanceof Reflect && object.equals(((Reflect) obj).get());

    }

    /**
     * {@inheritDoc}
     */
    public String toString() {
        return object.toString();
    }

    /**
     * 取得我们正在反射的对象的类型.
     *
     * @see Object#getClass()
     */
    public Class<?> type() {
        if (isClass) {
            return (Class<?>) object;
        } else {
            if (isSuper) {
                return object.getClass().getSuperclass();
            }
            return object.getClass();
        }
    }

    public Reflect superClass() {
        isSuper = true;
        return this;
    }

    public static String getMethodDetails(Method method) {
        StringBuilder sb = new StringBuilder(40);
        sb.append(Modifier.toString(method.getModifiers()))
                .append(" ")
                .append(method.getReturnType().getName())
                .append(" ")
                .append(method.getName())
                .append("(");
        Class<?>[] parameters = method.getParameterTypes();
        for (Class<?> parameter : parameters) {
            sb.append(parameter.getName()).append(", ");
        }
        if (parameters.length > 0) {
            sb.delete(sb.length() - 2, sb.length());
        }
        sb.append(")");
        return sb.toString();
    }


    /**
     * 用来表示null的类.
     *
     * @author Lody
     */
    private static class NULL {
    }

    /**
     * 智能调用 但是只调用类本身声明方法 按照优先级 匹配
     * <p>
     * 1.完全匹配
     * 2.形参 Object...
     * 3.名字相同 无参数
     *
     * @param name
     * @param args
     * @return
     * @throws ReflectException
     */
    public Reflect callBest(String name, Object... args) throws ReflectException {
        Class<?>[] types = types(args);
        Class<?> type = type();

        Method bestMethod = null;
        int level = 0;
        for (Method method : type.getDeclaredMethods()) {
            if (isSimilarSignature(method, name, types)) {
                bestMethod = method;
                level = 2;
                break;
            }
            if (matchObjectMethod(method, name, types)) {
                bestMethod = method;
                level = 1;
                continue;
            }
            if (method.getName().equals(name) && method.getParameterTypes().length == 0 && level == 0) {
                bestMethod = method;
            }
        }
        if (bestMethod != null) {
            if (level == 0) {
                args = new Object[0];
            }
            if (level == 1) {
                Object[] args2 = {args};
                args = args2;
            }
            return on(bestMethod, object, args);
        } else {
            throw new ReflectException("no method found for " + name, new NoSuchMethodException("No best method " + name + " with params " + Arrays.toString(types)
                    + " could be found on type " + type() + "."));
        }
    }

    private boolean matchObjectMethod(Method possiblyMatchingMethod, String desiredMethodName,
                                      Class<?>[] desiredParamTypes) {
        return possiblyMatchingMethod.getName().equals(desiredMethodName)
                && matchObject(possiblyMatchingMethod.getParameterTypes());
    }

    private boolean matchObject(Class<?>[] parameterTypes) {
        Class<Object[]> c = Object[].class;
        return parameterTypes.length > 0 && parameterTypes[0].isAssignableFrom(c);
    }
}

4.7、反射异常类ReflectException.java

实现参考如下:

/**
 * @author Lody
 */
public class ReflectException extends RuntimeException {

	public ReflectException(String message, Throwable cause) {
		super(message, cause);
	}

	public ReflectException(Throwable cause) {
		super(cause);
	}
}

5、实践过程中的各种报错及原因

5.1、Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x101005a a=-1}

Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x101005a a=-1}
这个错误可能的原因就是宿主与插件库资源ID冲突的,必须要去找出来。

5.2、‘android.view.Window$Callback android.view.Window.getCallback()’ on a null object reference

这个错误就是插件库不支持AppCompatActivity,只能使用Activity来开发,因为这块需要针对Compact的Theme做单独适配。

5.3、Exception while getting ActivityInfo

android.content.pm.PackageManagerNameNotFoundException: ComponentInfo{com.xx.escape.plan/com.xxx.LoadActivity}
at android.app.ApplicationPackageManager.getActivityInfo(ApplicationPackageManager.java:435)
at androidx.appcompat.app.AppCompatDelegateImpl.isActivityManifestHandlingUiMode(AppCompatDelegateImpl.java:2669)
这个错误是因为ApplicationPackageManager.getActivityInfo这个函数为空,必须要通过Hook来跳过这段代码的检测,API28要替换掉ApplicationPackageManager#mPM属性,才能进入到getActivityInfo这个地方,否则hook代码不会执行。

5.4、You need to use a Theme.AppCompat theme (or descendant) with this activity.

原因1:这个错误是因为不支持AppCompat.Theme属性造成的,需要调用Activity的setTheme(int resid)的这个函数后,才能让样式生效,其中这个resid的获取方式很神奇,需要从ActivityInfo中来获取。
原因2:插件库引入依赖包必须要注意是否存在样式重复引入的问题,比如只在插件中引入了 androidx.legacy:legacy-support-v4:1.0.0 这个库,宿主没引入,运行插件就会一直报错。

6、参考资料

I、Android插件化实现动态加载Activity笔记
II、xPlugin源码

这是一篇精炼的原创文章,耗费了一定时间,所以阅读前需要熟悉一些插件化的知识,这样才会更容易理解,很多注释在代码中写得非常详细了,很容易看懂。
注意:本文的这种Hook方式,可能无法通过GooglePlay上架审核,要解决此问题可以使用腾讯的Shadow开源项目,但其采用的实现方式不同。






原创不易,求个关注。

在这里插入图片描述

微信公众号:一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。

公众号与博客的内容不同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2082920.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Codeforces Round 968 (Div. 2)

前言 掉大分的一场比赛aaa 原因是 D1 看错条件了一直在想 D2 最后还没想出来。 Standings&#xff1a;6922 题目链接&#xff1a;Dashboard - Codeforces Round 968 (Div. 2) - Codeforces A. Turtle and Good Strings 题意&#xff1a; 给一个字符串&#xff0c;判断是否能把…

AWS CodeCommit 停服,欢迎大家使用极狐GitLab!

2024 年 7 月 25 日&#xff0c;AWS 官方发布公告称&#xff0c;旗下的代码托管服务 AWS CodeCommit 不再接受新用户的注册。这也就意味着用户不得不选择其他同类型产品。 极狐GitLab 为 GitLab 的中国发行版&#xff0c;可以一键私有化部署&#xff0c;详情可查看官网指南。 A…

超实用的8个无版权、免费、高清图片素材网站整理

不管是设计、文章配图&#xff0c;还是视频制作&#xff0c;图片都至关重要。但是图片版权一直都是困扰很多设计、自媒体以及企业的大问题。现在&#xff0c;因为图片侵权被告的案例已经是司空见惯了&#xff0c;有的公众号甚至因为图片版权问题遭受致命打击。 1. Pexels Pexe…

2 Python开发工具:PyCharm的安装和使用

本文是 Python 系列教程第 2 篇&#xff0c;完整系列请查看 Python 专栏。 1 安装 官网下载地址https://www.jetbrains.com.cn/pycharm/&#xff0c;文件比较大&#xff08;约861MB&#xff09;请耐心等待 双击exe安装 安装成功后会有一个30天的试用期。。。本来想放鸡火教程&…

Elastic日志分析

目录 介绍步骤 介绍 Elasticsearch 是在 Apache Lucene 上构建的分布式搜索和分析引擎。Elasticsearch常用于日志分析、全文搜索、安全智能、业务分析和运维智能使用案例。 可以使用 JSON 文档形式或通过 API 等将数据发送到 Elasticsearch。 Elasticsearch 自动存储原始文档…

免杀笔记 ---> CS特性角度看Veh免杀

前一段时间在玩WBGlIl大佬以前发的一篇过卡巴的思路&#xff08;虽然现在不过了&#xff09;&#xff0c;但是在研究的时候发现如果我们我们在没有CS的特性基础下直接看这篇文章&#xff0c;或者说你去魔改他的脚本是不太可能的&#xff0c;刚好就来普及一下这个CS的一些简单特…

胃癌TMEscore的前瞻性临床研究(TME)

目录 ①关于胃癌TME分型介绍 ②TMEscore计算-TMEscore包 ③关于TMEscore的前瞻性研究 ①关于胃癌TME分型介绍 Tumor Microenvironment Characterization in Gastric Cancer Identifies Prognostic and Immunotherapeutically Relevant Gene Signatures - PubMed (nih.gov) …

【Linux —— POSIX信号量 - 基于环形队列的生产消费模型】

Linux —— POSIX信号量 - 基于环形队列的生产消费模型 POSIX信号量信号量的概念POSIX信号量的类型信号量的操作 POSIX信号量函数基于环形队列的生产消费模型设计思路同步和安全性代码 POSIX信号量 信号量的概念 POSIX信号量是一种用于进程和线程之间同步的机制&#xff0c;主…

【网络】网络层协议——IP协议

目录 1.TCP和IP的关系 2.IP协议报文 2.1. 4位首部长度&#xff0c;16位总长度&#xff0c;8位协议 2.2. 8位生存时间 &#xff0c;32位源IP地址和32位目的IP地址 3.IP地址的划分 3.1.IP地址的表现形式 3.2.旧版IP地址的划分 3.2.1.旧版IP地址的划分思路 3.2.2.分类划…

各种注意力评分函数的实现

预备知识 本文基于MXNet进行实现&#xff0c;需要对于注意力机制有一定初步了解。也需要对Python有足够了解。 另外这里稍加说明&#xff0c;在注意力机制中&#xff0c;本质上是“注意”的位置&#xff0c;即加权计算后进行Softmax回归的结果。在Nadaraya-Watson核回归中&am…

问界M7 Pro发布,又做回纯视觉?华为智驾系统这3年到底功夫下在哪?

北京时间8月26日下午14:00&#xff0c;华为又公布了一款新问界车型&#xff0c;时至今日&#xff0c;华为问界家族已有三大款&#xff0c;细分9个系列车型&#xff08;从动力方面看&#xff0c;各自都分为增程和纯电两种版本&#xff09;。 1个多小时的发布会上&#xff0c;除…

Zabbix和Prometheus

1.Zabbix 1.1 Zabbix监控获取数据的方式 zabbix-agent 适用于服务器&#xff0c;主机监控 SNMP协议 适用于网络设备&#xff08;交换机、路由器、防火墙&#xff09; IPMI协议 适用于监控硬件设备信息&#xff08;温度、序列号&#xff09; JMX协议 适用于Java应用监控 1.2 …

基于SSM+微信小程序的跑腿平台管理系统(跑腿3)(源码+sql脚本+视频导入教程+文档)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSM微信小程序的跑腿平台管理系统实现了管理员、接单员及用户三个角色。 1、管理员实现了首页、个人中心、管理员管理、基础数据管理、接单详情、跑腿任务管理等。 2、接单员实现了…

C++ TinyWebServer项目总结(14. 多线程编程)

早期Linux不支持线程&#xff0c;直到1996年&#xff0c;Xavier Leroy等人开发出第一个基本符合POSIX标准的线程库LinuxThreads&#xff0c;但LinuxThreads效率低且问题多&#xff0c;自内核2.6开始&#xff0c;Linux才开始提供内核级的线程支持&#xff0c;并有两个组织致力于…

离线环境下的 Prometheus 生态部署攻略

一、前言 在当今高度数字化的世界中&#xff0c;监控系统的稳定性和可靠性对于确保业务连续性和性能优化至关重要。特别是在网络隔离或无互联网接入的局域网环境下&#xff0c;离线部署监控解决方案成为了一种必要且挑战性的任务。本文将深入探讨如何在离线环境中成功部署 Pro…

深圳保障房、商品房、小产权房子类型对比

摘要&#xff1a; 整理了我认知以内的深圳房子类型&#xff0c;有安居房&#xff0c;可售人才房&#xff0c;共有产权房、配售型保障房、商品房、统建楼、农民房的区别。如果数据存疑&#xff0c;可以多方对比论证&#xff0c;我也主要靠百度。 我发现我很多同事是非深户&#…

秋招突击——算法练习——8/26——图论——200-岛屿数量、994-腐烂的橘子、207-课程表、208-实现Trie

文章目录 引言正文200-岛屿数量个人实现 994、腐烂的橘子个人实现参考实现 207、课程表个人实现参考实现 208、实现Trie前缀树个人实现参考实现 总结 引言 正文 200-岛屿数量 题目链接 个人实现 我靠&#xff0c;这道题居然是腾讯一面的类似题&#xff0c;那道题是计算最…

《分析模式》2024中译本-前言-01(加红色标注)

写在前面 今天开始&#xff0c;我们逐渐发布一些《分析模式》2024中译本的译文。 红色字体标出的文字&#xff0c;表示我认为之前的译本可能会让读者产生误解的地方。 感兴趣的读者&#xff0c;可以对照之前译本以及原文&#xff0c;捉摸一下为什么要标红。 主要原因当然是…

基于SpringBoot+Vue+MySQL的小区物业管理系统

系统背景 在当今信息化高速发展的时代&#xff0c;小区物业管理正经历着从传统模式向智能化、高效化转型的深刻变革。这一转变的核心驱动力&#xff0c;正是小区物业管理系统的全面智能化升级。该系统不仅极大地提升了物业管理的效率与精确度&#xff0c;还深刻重塑了物业与业主…

数分基础(03-1)客户特征分析

文章目录 客户特征分析1. 数据集2. 思路与步骤2.1 特征工程2.2 识别方法2.3 可视化 3. 分析准备3.1 读取数据集3.2 识别不同客户群体3.2.1 使用K-Means聚类进行初步细分3.2.2 关于聚类方法&#xff08;1&#xff09;特征缩放1&#xff09;平衡特征对模型的影响力&#xff0c;避…