插件化之APK动态加载

news2024/10/6 14:24:14

插件化相关概念:

根据组件化与插件化的区别来了解一下概念

组件化和插件化的区别

组件化:是将一个APP分成多个模块,每个模块都是一个组件(module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件,但是最终发布的时候将这些组件合并成一个统一的APK。

插件化:是将整个APP拆分成很多模块,每个模块都是一个APK(组件化的每个模块是一个lib),最终打包的时候将宿主APK和插件APK分开打包,插件APK通过动态下发到宿主APK

插件化的优点

  • 减小安装APK的体积,按需下载模块
  • 动态更新插件
  • 宿主和插件分开编译,提升团队开发效率
  • 解决方法数超过65535问题

插件化框架对比 

插件化实现思路 

 使用插件化必然就会有宿主apk和插件apk,要把插件化的东西用到宿主里面去那么会面临以下三个问题

  • 如何加载资源
  • 如何动态加载类
  • 如何启动组件

如何加载插件资源?(res的动态加载)

插件apk动态加载时,并不会走正常的application初始化那一套流程,资源文件也不会加载到宿主apk的resouces里面,需要手动加载。

实现一个resource获取插件的资源,在创建resource之前,我们先实例化一个AssetManager对象,然后通过反射调用addAssetPath方法,将我们插件apk的地址设置进去,最后通过Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)方法,新建一个resource(该resource只含有插件apk中的res资源)

/**
     * 加载资源
     *
     * @return
     */
    public Resources loadResources(Context context) throws Exception {
        AssetManager pluginAssets = AssetManager.class.newInstance();
        Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        String apkPath = context.getExternalFilesDir(null).getAbsolutePath() + "/plugin-debug.apk";
        addAssetPathMethod.invoke(pluginAssets, apkPath);
        addAssetPathMethod.setAccessible(true);
        Resources pluginRes = new Resources(pluginAssets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

        return pluginRes;
    }

统一一下getResource的入口,application 继承 ContextWrapper,ContextWrapper中有getResources方法,我们需要重写

public class MyApplication extends Application {
    private Resources resources;

    @Override
    public void onCreate() {
        super.onCreate();
        PluginManager pluginManager = PluginManager.getInstance(this);
        try {
            this.resources = pluginManager.loadResources();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    @Override
    public Resources getResources() {
        // 插件资源不为空就用插件的,否则用默认的
       return resources == null ? super.getResources(): resources;
    }
}

同样的,对于activity也要写个基类重写getResources,统一从application拿。

如何动态加载类

这涉及到类的动态加载,常用的类加载器有:

  • BootClassLoader:系统启动时用于加载系统常用类,ClassLoader内部类。

  • PathClassLoader:加载系统类和应用程序类,一般不建议开发者使用。

  • DexClassLoader:加载dex文件及包含dex文件的apk或jar。也支持从SD卡进行加载,这也

    就意味着DexClassLoader可以在应用未安装的情况了不加载dex相关文件。因此,它是热修

    复和插件化技术的基础。

简单提一下类加载的双亲委派机制

双亲委派并不是我们Java继承层面的父类子类的关系。比如PsathClassLoader和DexClassLoader的父类都是BaseDexClassLoader,他们是兄弟关系。

双亲委派机制的优点

  • 避免重复加载,若已加载直接从缓存中读取。
  • 更加安全,避免开发者修改系统类。

具体的类加载器相关内容移步Android插件化开发指南——类加载器_贺兰猪的博客-CSDN博客 

可以知道这里要使用到的是DexClassLoader(String dexPath,String optimizedDirectory,String librarySearchPath,ClassLoader parent)
* dexPath 填写含dex的文件位置即可(应用内目录)
* optimizedDirectory 这是存放dex加载后会生存缓存的路径。Android5.0以下采用的是Dalvik虚拟机,会在optimizedDirectory目录生成一个"文件名.dex"的缓存,而Android5.0以上由于采用的是Art运行时,则会在补丁的apk同级目录生成oat文件夹,并生成"文件名.apk.cur.prof存放缓存
* librarySearchPath c、c++库,大部分情况null即可
* parent 该装载器的父装载器,一般为当前执行类的装载器。

//加载dex
        File file = new File(Environment.getExternalStorageDirectory(), "plugin-debug.apk");
        //dex -> odex缓存路径
        File odexPath = this.getDir("cache_plugin", Context.MODE_PRIVATE);
        //使用类加载器加载dex
        DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), odexPath.getAbsolutePath(), null, getClassLoader());
        try {
            Class<?> aClass = dexClassLoader.loadClass("com.example.myapplication.plugin.ToastUtils");
            Method showInfo = aClass.getMethod("showInfo", Context.class);
            showInfo.setAccessible(true);
            showInfo.invoke(aClass.newInstance(), PluginActivity.this);
        } catch (Exception e) {
            e.printStackTrace();
        }

需要添加读写内存权限。 

但是上面这种方式虽然可以加载外部插件,每次调用都要得到DexClassLoader对象。虽然我们可以写为单例模式,但是从代码的书写角度来讲,每次都需要来得到外部dex的DexClassLoader比较麻烦。而且如果在一个应用中有很多个外部dex或者apk插件的时候,难道需要让程序员记住每个dex中有哪些类?这显然不现实。所以需要通过另外一种方式来实现。

-------------------偷懒,以下转自,看到这之后大家可以直接去看博主的Android插件化开发指南——Hook技术(一)【长文】_android hook 插件开发_梦否的博客-CSDN博客

将外部dex加载到宿主app的dexElements中

为了知道宿主App中在哪里加载类的,所以需要从类加载器开始看起。这里从OtherActivity中的getClassLoader()方法开始追踪。如下图所示:

在这里插入图片描述

这里的Context为一个抽象类,且getClassLoader方法为一个抽象方法,所以我们需要找到其实现类ContextImpl。其源码可以查看链接:ContextImpl.java。

// ContextImpl
final LoadedApk mPackageInfo;
@Override
public ClassLoader getClassLoader() {
    return mPackageInfo != null ?
        mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}

所以需要先了解下mPackageInfo这个变量在哪里赋值:

// ContextImpl
static ContextImpl createSystemContext(ActivityThread mainThread) {
    LoadedApk packageInfo = new LoadedApk(mainThread);
    ContextImpl context = new ContextImpl(null, mainThread,
            packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
    context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
            context.mResourcesManager.getDisplayMetrics());
    return context;
}

也就是说这个LoadedApk是和Main线程挂钩的。这里继续查看getClassLoader这个方法:

// LoadedApk.java
public ClassLoader getClassLoader(){
    synchronized (this) {
        if (mClassLoader == null) {
            createOrUpdateClassLoaderLocked(null);
        }
        return mClassLoader;
    }
}

至于这个createOrUpdateClassLoaderLocked方法中,其实也是使用ClassLoader.getSystemClassLoader()来获取一个类加载器。
这个方法最终会调用ClassLoader.createSystemLoader()方法,该方法如下:

private static ClassLoader createSystemClassLoader() {
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");
    return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

也就是说其实返回的是一个PathClassLoader类加载器。也就是说在ContextImpl文件中的getClassLoader()方法调用之后,返回得到的是一个PathClassLoader类加载器。找到PathClassLoader.java的源文件:PathClassLoader.java。一目了然,这个类的功能基本来自其父类BaseDexClassLoader,因为其只有两个构造方法。所以这里可以查看BaseDexClassLoader.java的源文件。

这个文件的代码也比较简单,如下:

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    ...
}

从上面的代码中可以看出其实查找是从其属性字段DexPathList中查找。也就是这里其实还需要进一步查找这个类的源码,因为DexPathList.java这个类的代码较多,这里就只查看pathList.findClass方法。

// DexPathList
public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

观察上面的代码可以知道其实在执行findClass的时候,其实是在dexElements中进行查找。而这个dexElements直接定义为一个数组:

private Element[] dexElements;

故而如果我们能够将外部插件的dex或者apk文件中的dexElements加入到宿主app的dexElements中就可以完成预期。

不妨将上面的逻辑用时序图来进行表示:

那么对应的可以写一个工具类,用于完成上面的步骤。代码如下:

public class LoadUtils {

    private static String pluginPath = "/sdcard/plugin-debug.apk";

    public static void init(Context context) {
        if(context == null) return;
        try {
            // 获取应用程序App的dexElements
            PathClassLoader classLoader = (PathClassLoader) context.getClassLoader();
            Class<?> baseDexClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");
            Field dexPathListField = baseDexClassLoaderClazz.getDeclaredField("pathList");
            dexPathListField.setAccessible(true);
            Object dexPathListValue = dexPathListField.get(classLoader);
            Field dexElementsField = dexPathListValue.getClass().getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object dexElementsValue = dexElementsField.get(dexPathListValue);

            // 获取外部插件的dexElements
            DexClassLoader dexClassLoader = new DexClassLoader(pluginPath,
                    context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath(),
                    null, context.getClassLoader());

            Object pluginDexPathListValue = dexPathListField.get(dexClassLoader);
            Object pluginDexElementsValue = dexElementsField.get(pluginDexPathListValue);

            // 合并两个dexElements
            int appDexElementsLength = Array.getLength(dexElementsValue);
            int pluginDexElementsLength = Array.getLength(pluginDexElementsValue);
            int newLength = appDexElementsLength + pluginDexElementsLength;

            Class<?> componentType = dexElementsValue.getClass().getComponentType();
            Object newArray = Array.newInstance(componentType, newLength);
            System.arraycopy(dexElementsValue, 0, newArray, 0, appDexElementsLength);
            System.arraycopy(pluginDexElementsValue, 0, newArray, appDexElementsLength, pluginDexElementsLength);

            // 设置新的内容到app的PathList中的Elements[]
            dexElementsField.set(dexPathListValue, newArray);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

那么对应的加载方法为:

public class OtherActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_other);

        LoadUtils.init(this);

        try {
            Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.ToastUtils");
            Method showInfo = aClass.getMethod("showInfo", Context.class);
            showInfo.setAccessible(true);
            showInfo.invoke(aClass.newInstance(), OtherActivity.this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

就可以直接使用context中得到的PathClassLoader来进行反射。

如何启动组件

在上面的加载外部apk中的类的时候,我们加载的只是一个普通的类。而在Android中四大组件具有一定的特殊性,因为都需要在清单文件中注册。对于外部插件中的Activity或者Service等我们却又不可能在宿主App中进行注册,所以需要一种方式可以绕过系统加载的时候对于清单文件中配置信息的检查。

对startActivity进行Hook

首先需要搞清楚在startActivity(intent)之后发生了什么事情。那么首先需要了解的就是AMS(ActivityManagerService)虽然这个字面意思是Activity的管理服务,但是其实四大组件都归它管。

AMS主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,其职责与操作系统中的进程管理和调度模块相类似。当发起进程启动或者组件启动时,都会通过Binder通信机制将请求传递给AMS,AMS再做统一处理。

既然AMS管理着四大组件,那么为什么不直接在AMS层对想要的功能进行Hook呢?因为如果可以在AMS层进行Hook,很明显就是病毒程序了,所以在Android中也不允许这么做。所以我们的Hook点只能是在四大组件,因为至少需要保证受影响的只是当前程序,而不能影响别的程序。

这里以ActivityA启动ActivityB为例:

  • ActivityA向AMS发送需要启动ActivityB的消息;
  • AMS保存ActivityB的信息,同时AMS要检查ActivityB是否在清单文件中注册,如果注册了才继续下面的步骤;
  • ActivityA休眠,AMS通知ActivityThread去启动ActivityB;

从startActivity(intent);出发,可以看见下面的调用流程:(这里源代码是api 25的版本 也就是7.1!!!!)

在这里插入图片描述
也就是说通过startActivity(intent);最终会请求Activity类的startActivityForResult方法,在方法中可以看见两个成员变量:

mInstrumentation  // Instrumentation
mMainThread   // ActivityThread

// 对应逻辑摘要
Instrumentation.ActivityResult ar =
    mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, this,
            intent, requestCode, options);
    if (ar != null) {
mMainThread.sendActivityResult(
        mToken, mEmbeddedID, requestCode, ar.getResultCode(),
        ar.getResultData());
}

 那么首先看下Instrumentation这个类中的execStartActivity方法。

// Instrumentation
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ...
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManagerNative.getDefault()
                .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);
    }
    return null;
}

 在execStartActivity这个方法中,最终会通过ActivityManagerNative.getDefault().startActivity()方法来启动目标的Activity。而ActivityManagerNative.getDefault()最后返回的其实也就是一个IActivityManager对象,也就是常说的AMS对象。不妨继续看看这个AMS是如何得到的,我们继续追踪:

// ActivityManagerNative
static public IActivityManager getDefault() {
	return gDefault.get();
}

private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
    protected IActivityManager create() {
        IBinder b = ServiceManager.getService("activity");
        if (false) {
            Log.v("ActivityManager", "default service binder = " + b);
        }
        IActivityManager am = asInterface(b);
        if (false) {
            Log.v("ActivityManager", "default service = " + am);
        }
        return am;
    }
};

static public IActivityManager asInterface(IBinder obj) {
    if (obj == null) {
        return null;
    }
    IActivityManager in =
            (IActivityManager)obj.queryLocalInterface(descriptor);
    if (in != null) {
        return in;
    }

    return new ActivityManagerProxy(obj);
}
而对于这里的单例泛型类Singleton为:
public abstract class Singleton<T> {
    private T mInstance;

    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}

从上面的代码中可以知道,这里的AMS定义为单例对象,这个单例使用上面的Singleton<T>来进行修饰,真正的创建方法由new的时候来指定。而在gDefault的创建过程中,使用了IBinderActivityManagerProxy进行转换。

得到AMS实例对象

可以在通过反射来得到这个单例对象gDefault。然后再得到其中定义为泛型的mInstance,也就是AMS对象。然后就可以通过动态代理的方式来拦截AMS调用的startActivity方法。那么这里可以简单通过反射来得到AMS对象,由于AMS是在ActivityManagerNative.java文件中,通过getDefault()得到的,所以这里为:
 

public class HookAMSUtils {

    public static void getActivityManagerService() {
        try {
            Class<?> aClass = Class.forName("android.app.ActivityManagerNative");
            Field getDefault = aClass.getDeclaredField("gDefault");
            getDefault.setAccessible(true);

            // 获取静态的gDefault对象
            Object getDefaultObj = getDefault.get(null);
            // 而实际上AMS在单例Singleton中
            Class<?> singletonClazz = Class.forName("android.util.Singleton");
            Field mInstance = singletonClazz.getDeclaredField("mInstance");
            mInstance.setAccessible(true);
            Object amsObj = mInstance.get(getDefaultObj); // AMS
            Log.e("TAG", "getActivityManagerService: " + amsObj.toString());

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

 也就是说这里可以Hook容纳AMS的单例得到得到AMS对象。

上面分析的这个过程的时序图可以表示为:

在这里插入图片描述 对startActivity进行Hook

经过上面的逻辑分析,我们知道当一个Activity去启动另一个Activity后,最终根据一系列的调用会到AMSstartActivity方法,这里再次粘贴一下相关代码:

// Instrumentation
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ...
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManagerNative.getDefault()
                .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);
    }
    return null;
}

在前面得到的AMS对象,根据ActivityManagerNative中getDefault()的返回值类型,很容易我们知道其为IActivityManager 接口对象。

而如果我们需要在AMS中做欺骗,即绕过清单文件中对四大组件的注册检查。这里需要使用动态代理模式,然后拦截AMS的startActivity方法。

创建AMS的代理对象

 这里是代理接口IActivityManager.java。所以使用动态代理在进行invoke的时候会得到很多的方法,而这里我们只需要startActivity,这个方法定义为:

// IActivityManager.java
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;

那么我们在动态代理方法中,就可以拦截到这个startActivity方法。为了能够做到绕过清单文件检查的目的,我们可以事先在清单文件中注册一个代理的Activity,然后在拦截到的startActivity方法中进行Activity对象的替换即可。比如下面的代码:

public class HookAMSUtils {

    public static final String ORIGIN_INTENT = "ORIGIN_INTENT";

    public static void getActivityManagerService(Context context, Class<? extends Activity> proxyActivityClazz) {
        try {
            Class<?> aClass = Class.forName("android.app.ActivityManagerNative");
            Field getDefault = aClass.getDeclaredField("gDefault");
            getDefault.setAccessible(true);

            // 获取静态的gDefault对象
            Object getDefaultObj = getDefault.get(null);
            // 而实际上AMS在单例Singleton中
            Class<?> singletonClazz = Class.forName("android.util.Singleton");
            Field mInstance = singletonClazz.getDeclaredField("mInstance");
            mInstance.setAccessible(true);
            Object amsObj = mInstance.get(getDefaultObj); // AMS
            Log.e("TAG", "getActivityManagerService: " + amsObj.toString());

            // 创建AMS的代理对象
            Class<?> aClass1 = Class.forName("android.app.IActivityManager");
            // 得到AMS的代理对象
            Object amsProxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{aClass1}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    // 代理方法处理
                    Log.e("TAG", "invoke: startActivity");
                    if (method.getName().equals("startActivity")) {
                        // 查找参数,找到Intent对象
                        int index = 0;
                        for (int i = 0; i < args.length; i++) {
                            if (args[i] instanceof Intent) {
                                index = i;
                                break;
                            }
                        }
                        // 拿到意图
                        Intent oldIntent = (Intent) args[index];
                        String name = oldIntent.getStringExtra("NAME");
                        Log.e("TAG", "invoke: " + name);
                        // 创建一个新的意图,将这个旧的意图添加到新的意图中
                        Intent newIntent = new Intent(context, proxyActivityClazz);
                        // 将旧的意图放入到新的意图中
                        newIntent.putExtra(ORIGIN_INTENT, oldIntent);
                        // 设置startActivity的意图对象为新的意图
                        args[index] = newIntent;
                    }
                    return method.invoke(amsObj, args);
                }
            });
            // 将AMS代理对象设置为原本的AMS对象,
            // 也就是设置ActivityManagerNative.java中属性字段gDefault的值为代理对象
            mInstance.set(getDefaultObj, amsProxy);

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

当然需要创建一个在清单文件中注册的ProxyActivity类。然后在MainActivity中测试:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LoadUtils.init(this);
        HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);
        try {
            Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.MainActivity");
            Log.e("TAG", "onCreate: " + aClass.getName());
            Intent intent = new Intent(MainActivity.this, aClass);
            intent.putExtra("NAME", "123");
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果:

在这里插入图片描述
也就是说这里的跳转替换为了代理的Activity对象。所以我们还需要在某个地方将原本的目标com.weizu.plugin.MainActivity替换回来。当然,具体将这里的代理Activity替换为原本的MainActivity这里需要在ActivityThread中完成。这个过程的时序图可以表示为:

在这里插入图片描述
从上图中可以知道在ActivityThread中使用了Hanlder来发送消息。所以我们可以处理Handler的回调接口来进行Activity的替换。故而首先第一步为得到ActivityThread的实例对象,然后再将处理消息的方法设置为我们自己的方法。而ActivityThread中定义了自己的一个静态引用,故而可以比较容易的得到该对象。对应的代码为:

 

public static void hookActivityThreadToLaunchActivity(){
    try {
        // 得到ActivityThread的对象
        Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
        Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
        sCurrentActivityThreadField.setAccessible(true);
        Object activityThreadValue = sCurrentActivityThreadField.get(null);

        // 找到Handler,即mH
        Field mHField = activityThreadClazz.getDeclaredField("mH");
        mHField.setAccessible(true);
        Object mHValue = mHField.get(activityThreadValue);

        // 重新赋值
        Class<?> handlerClazz = Class.forName("android.os.Handler");
        Field mCallBackField = handlerClazz.getDeclaredField("mCallback");
        mCallBackField.setAccessible(true);
        mCallBackField.set(mHValue, new HandlerCallBack());

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

// 因为在ActivityThread中通过Handler来接受消息,
// 所以这里为了替换,就实现其回调接口

private static class HandlerCallBack implements Handler.Callback{

    @Override
    public boolean handleMessage(Message message) {
        // 处理消息
        if(message.what == 100) { // H.LAUNCH_ACTIVITY
            handleLaunchActivity(message);
        }

        return false;
    }

    private void handleLaunchActivity(Message message) {
        try {
            // 得到ActivityClientRecord r对象
            Object r = message.obj;
            // 而在得到ActivityClientRecord中就存储着传进来的Intent意图对象
            // 所以可以先获取到意图,然后修改意图对象
            Field intentField = r.getClass().getDeclaredField("intent");
            // 取出intent的值
            intentField.setAccessible(true);
            Intent newIntent = (Intent) intentField.get(r);
            // 从这个newIntent得到真正的意图
            Intent oldIntent = newIntent.getParcelableExtra(ORIGIN_INTENT);
            Log.e("TAG", "handleLaunchActivity: " + oldIntent.toString());
            if(oldIntent != null){
                // 设置r中的intent为当前的这个oldIntent
                intentField.set(r, oldIntent);
            }

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

那么在调用的时候使用:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LoadUtils.init(this);
        HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);
        HookAMSUtils.hookActivityThreadToLaunchActivity();
    }

    // onClick
    public void jump(View view){
        try {
            Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.MainActivity");
            Log.e("TAG", "onCreate: " + aClass.getName());
            Intent intent = new Intent(MainActivity.this, aClass);
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

即可实现点击文本框然后进行跳转。

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

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

相关文章

故障注入测试的作用和应用你了解多少?

故障注入是一种测试技术&#xff0c;用于模拟系统或应用程序中的故障。故障注入测试通常被用来评估系统或应用程序的可靠性和鲁棒性&#xff0c;以便确定系统或应用程序是否能够在各种异常情况下正常运行&#xff0c;那故障注入测试的作用和应用你了解多少&#xff1f; 故障注入…

苹果智能眼镜新技术曝光,智能穿戴与苹果Find My技术相结合

知名分析师郭明錤发文表示&#xff0c;苹果正在开发“超透镜”(metalens)技术&#xff0c;预计将于2024年实现量产&#xff0c;用于取代iPad的Face ID Tx的塑胶镜头&#xff0c;并将大量应用于Apple Glasses上。据悉&#xff0c;Apple Glasses(非AR/MR头显)预计最快在2026或202…

国网B接口语音对讲和广播技术探究及与GB28181差别

接口描述 在谈国网B接口的语音广播和语音对讲的时候&#xff0c;大家会觉得&#xff0c;国网B接口是不是和GB28181大同小异&#xff1f;实际上确实信令有差别&#xff0c;但是因为要GB28181设备接入测的对接&#xff0c;再次做国网B接口就简单多了。 语音对讲和广播包括信令接…

小白学Pytorch系列--Torch.optim API Scheduler(4)

小白学Pytorch系列–Torch.optim API Scheduler(4) 方法注释lr_scheduler.LambdaLR将每个参数组的学习率设置为初始lr乘以给定函数。lr_scheduler.MultiplicativeLR将每个参数组的学习率乘以指定函数中给定的因子。lr_scheduler.StepLR每个步长周期衰减每个参数组的学习率。lr_…

RFID技术在智慧图书馆盘点系统中的优势

RFID射频识别及技术&#xff0c;作为一种新兴的非接触式的自动识别技术&#xff0c;其基本原理是电磁理论&#xff0c;因其操作便捷高效&#xff0c;无需人工干预&#xff0c;可在各种恶劣环境下&#xff0c;通过射频信号自动识别目标并获取相关数据&#xff0c;可识别高速运动…

LightGBM论文翻译

0.摘要 Gradient Boosting Decision Tree (GBDT)是一个非常流行的机器学习算法&#xff0c;却只有像XGBoost和pGBRT的一些实现。尽管许多工程上的优化方案已经在这些实现中应用了&#xff0c;但是当特征维度较高和数据量巨大的时候&#xff0c;仍然存在效率和可扩展性的问题。…

家庭智能插座一Homekit智能

传统的灯泡是通过手动打开和关闭开关来工作。有时&#xff0c;它们可以通过声控、触控、红外等方式进行控制&#xff0c;或者带有调光开关&#xff0c;让用户调暗或调亮灯光。 智能灯泡内置有芯片和通信模块&#xff0c;可与手机、家庭智能助手、或其他智能硬件进行通信&#x…

L2-031 深入虎穴

著名的王牌间谍 007 需要执行一次任务&#xff0c;获取敌方的机密情报。已知情报藏在一个地下迷宫里&#xff0c;迷宫只有一个入口&#xff0c;里面有很多条通路&#xff0c;每条路通向一扇门。每一扇门背后或者是一个房间&#xff0c;或者又有很多条路&#xff0c;同样是每条路…

电脑频繁出现0x0000000A蓝屏错误怎么重装系统?

电脑频繁出现0x0000000A蓝屏错误怎么重装系统&#xff1f;有的小伙伴使用电脑的时候&#xff0c;总是会出现蓝屏的问题&#xff0c;导致自己不得不进行系统的重装。遇到这个情况只能是使用U盘来进行系统的重装了。一起来看看以下的具体操作方法教学吧。 准备工作&#xff1a; 1…

ClickHouse实现大数据探索性分析

分析数据一般会从探索性分析开始&#xff0c;即尝试理解数据本身的概况。通常包括中位数、平均值或分布情况&#xff0c;Python Numpy/Pandas很容易实现&#xff0c;但如果数据量为Tb级&#xff0c;不能简单依赖RAM工具实现。ClickHouse提供的强大的工具来挖掘数据&#xff0c;…

7.基于概率距离快速削减法的风光场景生成与削减方法

matlab代码&#xff1a;基于概率距离快速削减法的风光场景生成与削减方法 参考代码资源&#xff1a;风、光、负荷场景生成&#xff1b;风电出力各场景及概率&#xff1b;光伏出力各场景及概率&#xff1b;负荷各场景及概率&#xff1b;场景的削减&#xff1b;样本概率初始化&a…

【大数据基础】基于 TMDB 数据集的电影数据分析

https://dblab.xmu.edu.cn/blog/2400/ 实验内容 环境搭建 pip3 install bottle数据预处理 本次项目使用的数据集来自知名数据网站 Kaggle 的 tmdb-movie-metadata 电影数据集&#xff0c;该数据集包含大约 5000 部电影的相关数据。本次实验使用数据集中有关电影的数据表 tm…

五一临近,赋能乡村振兴,低代码也有话讲!

中国作为农业生产大国&#xff0c;农业已成为近千年来中国主要的经济来源&#xff0c;农民人口基数庞大。因此&#xff0c;乡村振兴战略的提出对推进农业农村现代化具有重要意义&#xff0c;治理好乡村成为解决“三农”问题的必要举措。 随着时代的发展&#xff0c;人们反而更向…

【STC8A8K64D4开发板】——有源蜂鸣器鸣响控制

学习目的 掌握有源蜂鸣器驱动电路的设计&#xff1a;控制方式、限流电阻的计算和确定。了解有源蜂鸣器的特性以及和无源蜂鸣器的区别。 硬件电路设计 开发板上的蜂鸣器驱动电路如下图所示&#xff0c;使用的蜂鸣器是3V有源蜂鸣器&#xff0c;这里&#xff0c;我们有必要了解…

Vue学习——【第二弹】

前言 上一篇文章 Vue学习——【第一弹】 中我们学习了Vue的相关特点及语法&#xff0c;这篇文章接着通过浏览器中的Vue开发者工具扩展来进一步了解Vue的相关工作机制。 Vue的扩展 我们打开Vue的官方文档&#xff0c;点击导航栏中的生态系统&#xff0c;点击Devtools 接着我…

C/C++|物联网开发入门+项目实战|嵌入式C语言高级|简介及GCC参数|define|编译过程-学习笔记(7)

课程介绍 参考&#xff1a;麦子学院-嵌入式C语言高级 本套课程的定位 前导课程:掌握简单C语言的基本语法 计算机程序语言的学习思路? 基本程序设计思想&#xff0b;语言工具的特性 基本程序设计思想: 数据类型、运算符、条件分支、循环设计 面向对象的设计 C语言工具的特性…

数据库系统笔记CH5(初)

计组的知识 虚拟存储 IEEE754标准 5.1存储层级 二级存储器磁盘和固态硬盘 红色部分是磁道&#xff0c;蓝色部分是扇区&#xff0c;绿色指示部分是数据库中的一个块/页 块/页是我们用来存储介质的一个物理单元,数据写入磁盘以块位单位写入内存,一个块的大小一般是4KB或者8KB&…

项目管理中,这些思维误区一定要避开

项目需要在限定的时间要求完成的事情&#xff0c;可控的关键把握是&#xff1a;人、时、事。 但是&#xff0c;项目实施时间一般较长&#xff0c;总有很多项目实施结果不尽人意。那么&#xff0c;IT项目管理过程中&#xff0c;容易出现哪些思维误区呢&#xff1f; 1、忘记项…

react性能优化之shouldComponentUpdate的原理剖析

shouldComponentUpdate原理讲解shouldComponentUpdate是干什么的怎么使state更新而render函数不执行呢&#xff1f;使用shouldComponentUpdate完成性能优化当组件的state没有变化&#xff0c;props也没有变化&#xff0c;render函数可能执行吗&#xff1f;pureComponent的基本用…

北京小厂Java实习面经

目录1.数据库的三大范式2.事务四个特性3.知道多少种索引&#xff0c;分别讲讲4.主键索引和唯一索引的区别5.索引失效的场景6.数据库的日志知道哪些&#xff0c;分别讲讲7.redis的数据结构和应用场景8.缓存击穿是怎么产生的&#xff0c;解决方案9.redis中key的过期策略10.redis内…