根据组件化与插件化的区别来了解一下概念
组件化:是将一个APP分成多个模块,每个模块都是一个组件(module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件,但是最终发布的时候将这些组件合并成一个统一的APK。
插件化:是将整个APP拆分成很多模块,每个模块都是一个APK(组件化的每个模块是一个lib),最终打包的时候将宿主APK和插件APK分开打包,插件APK通过动态下发到宿主APK。
使用插件化必然就会有宿主apk和插件apk,要把插件化的东西用到宿主里面去那么会面临以下三个问题
插件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 suppressedExceptions = new ArrayList();
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 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为例:
从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 gDefault = new Singleton() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
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 {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
从上面的代码中可以知道,这里的AMS
定义为单例对象,这个单例使用上面的Singleton
来进行修饰,真正的创建方法由new
的时候来指定。而在gDefault
的创建过程中,使用了IBinder
和ActivityManagerProxy
进行转换。
得到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
对象。
上面分析的这个过程的时序图可以表示为:
经过上面的逻辑分析,我们知道当一个Activity
去启动另一个Activity
后,最终根据一系列的调用会到AMS
的startActivity
方法,这里再次粘贴一下相关代码:
// 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();
}
}
}
即可实现点击文本框然后进行跳转。