使用ClassLoader加载外部的Dex或Apk文件,可以加载一些本地APP不存在的类,从而执行一些新的代码逻辑。
但是使用这种方法却不能直接启动插件里的Activity
这是由于,Activity等组件是需要在Manifest中注册后才能以标准Intent的方式启动的,通过ClassLoader加载并实例化的Activity实例只是一个普通的Java对象,能调用对象的方法,但是它没有生命周期,而且Activity等系统组件是需要Android的上下文环境的(Context等资源),没有这些东西Activity根本无法工作。
使用插件APK里的Activity需要解决两个问题:
1. 如何使插件APK里的Activity具有生命周期;(本篇关注重点)
2. 宿主中启动插件Activity后,如何使插件Activity使用R资源,也就是具有对应的上下文环境;(下文会讲到)
一、代理Activity模式
这种模式的特点是:主项目APK注册一个代理Activity(命名为ProxyActivity),ProxyActivity是一个普通的Activity,但只是一个空壳,自身并没有什么业务逻辑。每次打开插件APK里的某一个Activity的时候,都是在主项目里使用标准的方式启动ProxyActivity,再在ProxyActivity的生命周期里同步调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。
ProxyActivity + 没注册的Activity = 标准的Activity
一个Activity的启动,如果不采用标准的Intent方式,没有经历过Android系统Framework层级的一系列初始化和注册过程,它的生命周期方法是不会被系统调用的(除非你能够修改Android系统的一些代码,而这已经是另一个领域的话题了,可以看下第三种方式)
那我们可以在
- 主项目里创建一个ProxyActivity
- 然后,由它去代理调用插件Activity的生命周期方法**(这也是代理模式叫法的由来)。
用ProxyActivity(一个标准的Activity实例)的生命周期同步控制插件Activity(普通类的实例)的生命周期,同步的方式可以有下面两种:
- 在ProxyActivity生命周期里用反射调用插件Activity相应生命周期的方法,简单粗暴。
- 把插件Activity的生命周期抽象成接口,在ProxyActivity的生命周期里调用。
- 另外,多了这一层接口,也方便主项目控制插件Activity。
1.1 用反射调用插件Activity相应生命周期
- 在代理activity中去反射apk中activity的所有生命周期的方法,然后将activity的生命周期和代理activity的生命周期进行同步
- 还反射了onActivityResult这个方法,尽管它不是典型的生命周期方法,但是它很有用
protected void instantiateLifecircleMethods(Class<?> localClass) {
String[] methodNames = new String[] {
"onRestart",
"onStart",
"onResume",
"onPause",
"onStop",
"onDestory"
};
for (String methodName : methodNames) {
Method method = null;
try {
method = localClass.getDeclaredMethod(methodName, new Class[] { });
method.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put(methodName, method);
}
Method onCreate = null;
try {
onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });
onCreate.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put("onCreate", onCreate);
Method onActivityResult = null;
try {
onActivityResult = localClass.getDeclaredMethod("onActivityResult",
new Class[] { int.class, int.class, Intent.class });
onActivityResult.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put("onActivityResult", onActivityResult);
}
- 其次,同步生命周期,主要看一下onResume和onPause,其他方法是类似的。
- 看如下代码,很好理解,就是当系统调用代理activity生命周期方法的时候,就通过反射去显式调用apk中activity的对应方法
- 首先通过类加载器去加载apk中activity的类并创建一个新对象,然后通过反射去调用这个对象的setProxy方法和onCreate方法,setProxy方法的作用是将activity内部的执行全部交由宿主程序中的proxy(也是一个activity),onCreate方法是activity的入口,setProxy以后就调用onCreate方法,这个时候activity就被调起来了
public class ProxyActivity extends Activity {
private static final String TAG = "ProxyActivity";
public static final String FROM = "extra.from";
public static final int FROM_EXTERNAL = 0;
public static final int FROM_INTERNAL = 1;
public static final String EXTRA_DEX_PATH = "extra.dex.path";
public static final String EXTRA_CLASS = "extra.class";
private HashMap<String, Method> mActivityLifecircleMethods;
private String mClass;
private String mDexPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);
mClass = getIntent().getStringExtra(EXTRA_CLASS);
Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);
if (mClass == null) {
onCreateActivity();
} else {
onCreateActivity(mClass);
}
}
//采用DexClassLoader去加载apk,然后如果没有指定class,就调起主activity,否则调起指定的class。
@SuppressLint("NewApi")
protected void onCreateActivity() {
PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(
mDexPath, 1);
if ((packageInfo.activities != null)
&& (packageInfo.activities.length > 0)) {
String activityName = packageInfo.activities[0].name;
mClass = activityName;
launchTargetActivity(mClass);
}
}
@SuppressLint("NewApi")
protected void onCreateActivity(final String className) {
Log.d(TAG, "start launchTargetActivity, className=" + className);
File dexOutputDir = this.getDir("dex", 0);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,
dexOutputPath, null, localClassLoader);
try {
Class<?> localClass = dexClassLoader.loadClass(className);
//【核心】:初始化各种 反射函数
instantiateLifecircleMethods(localClass);
//【核心】:构建 插件Activity实例
Constructor<?> localConstructor = localClass
.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
Log.d(TAG, "instance = " + instance);
//【核心】:向 插件Activity实例 中注入 真实的Activity实例,以用于调用跳转和setContentView等函数
Method setProxy = localClass.getMethod("setProxy",
new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(instance, new Object[] { this });
//【生命周期】:
Method onCreate = mActivityLifecircleMethods.get("onCreate");
onCreate.setAccessible(true);
Bundle bundle = new Bundle();
bundle.putInt(FROM, FROM_EXTERNAL);
onCreate.invoke(instance, new Object[] { bundle });
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onResume() {
super.onResume();
//【生命周期】:
Method onResume = mActivityLifecircleMethods.get("onResume");
if (onResume != null) {
try {
onResume.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
//【生命周期】:
Method onPause = mActivityLifecircleMethods.get("onPause");
if (onPause != null) {
try {
onPause.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
super.onPause();
}
}
- 插件Activity的注意事项
- 对是否使用代理进行了判断,如果不使用代理,那么activity的逻辑仍然按照正常的方式执行,也就是说,这个apk既可以按照执行,也可以由宿主程序来执行
- 可以写在BaseActivity中
- 其中setProxy方法的作用就是为了让宿主程序能够接管自己的执行,一旦被接管以后,其所有的执行均通过proxy,且Context也变成了宿主程序的Context
public class PlugBaseActivity extends Activity {
private static final String TAG = "Client-BaseActivity";
public static final String FROM = "extra.from";
public static final int FROM_EXTERNAL = 0;
public static final int FROM_INTERNAL = 1;
public static final String EXTRA_DEX_PATH = "extra.dex.path";
public static final String EXTRA_CLASS = "extra.class";
public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";
public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";
protected Activity mProxyActivity;
protected int mFrom = FROM_INTERNAL;
// 【核心】
public void setProxy(Activity proxyActivity) {
Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);
mProxyActivity = proxyActivity;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);
}
if (mFrom == FROM_INTERNAL) {
super.onCreate(savedInstanceState);
mProxyActivity = this;
}
Log.d(TAG, "onCreate: from= " + mFrom);
}
protected void startActivityByProxy(String className) {
if (mProxyActivity == this) {
Intent intent = new Intent();
intent.setClassName(this, className);
this.startActivity(intent);
} else {
Intent intent = new Intent(PROXY_VIEW_ACTION);
intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);
intent.putExtra(EXTRA_CLASS, className);
mProxyActivity.startActivity(intent);
}
}
@Override
public void setContentView(View view) {
if (mProxyActivity == this) {
super.setContentView(view);
} else {
mProxyActivity.setContentView(view);
}
}
@Override
public void setContentView(View view, LayoutParams params) {
if (mProxyActivity == this) {
super.setContentView(view, params);
} else {
mProxyActivity.setContentView(view, params);
}
}
@Deprecated
@Override
public void setContentView(int layoutResID) {
if (mProxyActivity == this) {
super.setContentView(layoutResID);
} else {
mProxyActivity.setContentView(layoutResID);
}
}
@Override
public void addContentView(View view, LayoutParams params) {
if (mProxyActivity == this) {
super.addContentView(view, params);
} else {
mProxyActivity.addContentView(view, params);
}
}
}
public class PlugAcitivty extends PlugBaseActivity {
private static final String TAG = "Client-MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView(savedInstanceState);
}
private void initView(Bundle savedInstanceState) {
mProxyActivity.setContentView(generateContentView(mProxyActivity));
}
//即便是 PlugAcitivty, 也不能直接访问 插件中的R资源,因为是使用的主app的上下文,只访问主app的资源,具体解决办法见后续文章
//暂时可以使用 动态代码生成方式
private View generateContentView(final Context context) {
Button button = new Button(mProxyActivity);
button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
button.setBackgroundColor(Color.YELLOW);
button.setText("这是测试页面");
setContentView(button);
}
}
1.2 把插件Activity的生命周期抽象成接口,在ProxyActivity的生命周期里调用
用反射去管理activity的生命周期,这样存在一些不便,比如反射代码写起来复杂,并且过多使用反射有一定的性能开销。
针对这个问题,可以采用接口机制,将activity的大部分生命周期方法提取出来作为一个接口(DLPlugin),然后通过代理activity(DLProxyActivity)去调用插件activity实现的生命周期方法,这样就完成了插件activity的生命周期管理,并且没有采用反射
当我们想增加一个新的生命周期方法的时候,只需要在接口中声明一下同时在代理activity中实现一下即可.
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity, String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public boolean onTouchEvent(MotionEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
}
- 在主app的代理类DLProxyActivity中,由于插件PlugActivity一定继承了DLPlugin,可以直接调用
DLPlugin mRemoteActivity;
@SuppressLint("NewApi")
protected void onCreateActivity(final String className, Bundle bundle) {
Log.d(TAG, "start launchTargetActivity, className=" + className);
File dexOutputDir = this.getDir("dex", 0);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,
dexOutputPath, null, localClassLoader);
try {
Class<?> localClass = dexClassLoader.loadClass(className);
//【核心】:构建 插件Activity实例
Constructor<?> localConstructor = localClass
.getConstructor(new Class[] {});
mRemoteActivity = localConstructor.newInstance(new Object[] {});
Log.d(TAG, "instance = " + mRemoteActivity);
//【核心】:向 插件Activity实例 中注入 真实的Activity实例,以用于调用跳转和setContentView等函数
mRemoteActivity.setProxy(this, dexpath);
//【生命周期】:
mRemoteActivity.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onStart() {
mRemoteActivity.onStart();
super.onStart();
}
@Override
protected void onRestart() {
mRemoteActivity.onRestart();
super.onRestart();
}
@Override
protected void onResume() {
mRemoteActivity.onResume();
super.onResume();
}
@Override
protected void onPause() {
mRemoteActivity.onPause();
super.onPause();
}
@Override
protected void onStop() {
mRemoteActivity.onStop();
super.onStop();
}
二、动态创建Activity
我们在代理Activity模式一文里谈到启动插件APK里的Activity的两个难题吗,由于插件里的Activity没在主项目的Manifest里面注册,所以无法经历系统Framework层级的一系列初始化过程,最终导致获得的Activity实例并没有生命周期和无法使用res资源。
使用代理Activity能够解决这两个问题,但是有一些限制
- 实际运行的Activity实例其实都是ProxyActivity,并不是真正想要启动的Activity;
- ProxyActivity只能指定一种LaunchMode,所以插件里的Activity无法自定义LaunchMode;
- 不支持静态注册的BroadcastReceiver;
- 往往不是所有的apk都可作为插件被加载,插件项目需要依赖特定的框架,还有需要遵循一定的”开发规范”;
特别是最后一个,无法直接把一个普通的APK作为插件使用
怎么避开这些限制呢?代理模式需要注册一个代理的ProxyActivity,那么能不能在主项目里注册一个通用的Activity(比如TargetActivity)给插件里所有的Activity用呢?
解决对策就是:
在需要启动插件的某一个Activity(比如PlugActivity)的时候,动态创建一个TargetActivity
- 新创建的TargetActivity会继承插件PlugActivity的所有行为
这个TargetActivity的包名与类名刚好与我们事先在主app中注册的TargetActivity一致,我们就能以标准的方式启动这个Activity
2.1 dexmaker 和 asmdex 动态生成类
运行时动态创建并编译一个Activity类,这种想法不是天方夜谭,动态创建类的工具有asmdex 和 dexmaker
二者均能实现动态字节码操作,最大的区别是前者是创建dex文件,而后者是创建class文件
2.1.1 使用dexmaker动态创建一个类
运行时创建一个编译好并能运行的类叫做“动态字节码操作(runtime bytecode manipulation)”,使用dexmaker工具能创建一个dex文件,之后我们再反编译这个dex看看创建出来的类是什么样子。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onMakeDex(View view){
try {
DexMaker dexMaker = new DexMaker();
// Generate a HelloWorld class.
TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
generateHelloMethod(dexMaker, helloWorld);
// Create the dex file and load it.
File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
if (!outputDir.exists())outputDir.mkdir();
ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
Class<?> helloWorldClass = loader.loadClass("HelloWorld");
// Execute our newly-generated code in-process.
helloWorldClass.getMethod("hello").invoke(null);
} catch (Exception e) {
Log.e("MainActivity","[onMakeDex]",e);
}
}
/**
* Generates Dalvik bytecode equivalent to the following method.
* public static void hello() {
* int a = 0xabcd;
* int b = 0xaaaa;
* int c = a - b;
* String s = Integer.toHexString(c);
* System.out.println(s);
* return;
* }
*/
private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
// Lookup some types we'll need along the way.
TypeId<System> systemType = TypeId.get(System.class);
TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);
// Identify the 'hello()' method on declaringType.
MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");
// Declare that method on the dexMaker. Use the returned Code instance
// as a builder that we can append instructions to.
Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);
// Declare all the locals we'll need up front. The API requires this.
Local<Integer> a = code.newLocal(TypeId.INT);
Local<Integer> b = code.newLocal(TypeId.INT);
Local<Integer> c = code.newLocal(TypeId.INT);
Local<String> s = code.newLocal(TypeId.STRING);
Local<PrintStream> localSystemOut = code.newLocal(printStreamType);
// int a = 0xabcd;
code.loadConstant(a, 0xabcd);
// int b = 0xaaaa;
code.loadConstant(b, 0xaaaa);
// int c = a - b;
code.op(BinaryOp.SUBTRACT, c, a, b);
// String s = Integer.toHexString(c);
MethodId<Integer, String> toHexString
= TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
code.invokeStatic(toHexString, s, c);
// System.out.println(s);
FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
code.sget(systemOutField, localSystemOut);
MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
TypeId.VOID, "println", TypeId.STRING);
code.invokeVirtual(printlnMethod, null, localSystemOut, s);
// return;
code.returnVoid();
}
}
点击button触发onMakeDex()
,我们会在在SD卡的dexmaker目录下找到刚创建的文件“Generated1.jar”
把里面的“classes.dex”解压出来,然后再用“dex2jar”工具转化成jar文件,最后再用“jd-gui”工具反编译jar的源码
【图Generated1.jar】
至此,已经成功在运行时创建一个编译好的类
2.2 修改需要启动的目标Activity
接下来的问题是如何把需要启动的、在Manifest里面没有注册的PlugActivity换成主app中有注册的TargetActivity。
在Android,虚拟机加载类的时候,是通过ClassLoader的loadClass方法,而loadClass方法并不是final类型的,这意味着我们可以创建自己的类去继承ClassLoader,以重载loadClass方法并改写类的加载逻辑,在需要加载PlugActivity的时候,偷偷把其换成TargetActivity。
public class CJClassLoader extends ClassLoader{
@override
public Class loadClass(String className){
if(当前上下文插件不为空) {
if( className 是 TargetActivity){
找到当前实际要加载的原始PlugActivity,动态创建类(TargetActivity extends PlugActivity )的dex文件
return 从dex文件中加载的TargetActivity
}else{
return 使用对应的PluginClassLoader加载普通类
}
}else{
return super.loadClass() //使用原来的类加载方法
}
}
}
不过还有一个问题,主项目启动插件Activity的时候,我们可以替换Activity.
但是如果在插件Activity(比如 PlugActivity )启动另一个Activity(Plug2Activity)的时候怎么办?插件是普通的第三方APK,我们无法更改里面跳转Activity的逻辑
其实,从主项目启动插件PlugActivity的时候,其实启动的是我们动态创建的TargetActivity(extends PlugActivity),而我们知道Activity启动另一个Activity的时候都是使用其“startActivityForResult”方法,所以我们可以在创建TargetActivity时,重写其“startActivityForResult”方法,让它在启动其他Activity的时候,也采用动态创建Activity的方式
就能实现 插件Activity 到 插件Activity之间的跳转
2.3 动态类创建Activity缺陷
动态类创建的方式,使得注册一个通用的Activity就能给多给Activity使用,对这种做法存在的问题也是明显的
- 使用同一个注册的Activity,所以一些需要在Manifest注册的属性无法做到每个Activity都自定义配置;
- 插件中的权限,无法动态注册,插件需要的权限都得在宿主中注册,无法动态添加权限;
- 插件的Activity无法开启独立进程,因为这需要在Manifest里面注册;
- 动态字节码操作涉及到Hack开发,所以相比代理模式起来不稳定;
其中不稳定的问题出现在对Service的支持上,使用动态创建类的方式可以搞定Activity和Broadcast Receiver,但是使用类似的方式处理Service却不行,因为“ContextImpl.getApplicationContext” 期待得到一个非ContextWrapper的context,如果不是则继续下次循环,目前的Context实例都是wrapper,所以会进入死循环。
推荐一个动态代理的开源项目:houkx/android-pluginmgr
三、总结
- 代理Activity模式与动态创建Activity模式的区别简单地说,最大的不同是代理模式使用了一个代理的Activity,而动态创建Activity模式使用了一个通用的Activity
代理模式中,使用一个代理Activity去完成本应该由插件Activity完成的工作,这个代理Activity是一个标准的Android Activity组件,具有生命周期和上下文环境(ContextWrapper和ContextCompl),但是它自身只是一个空壳,并没有承担什么业务逻辑;而插件Activity其实只是一个普通的Java对象,它没有上下文环境,但是却能正常执行业务逻辑的代码。代理Activity和不同的插件Activity配合起来,就能完成不同的业务逻辑了。所以代理模式其实还是使用常规的Android开发技术,只是在处理插件资源的时候强制调用了系统的隐藏API,因此这种模式还是可以稳定工作和升级的。
动态创建Activity模式,被动态创建出来的Activity类是有在主项目里面注册的,它是一个标准的Activity,它有自己的Context和生命周期,不需要代理的Activity
- hook伪注册方式则是通过hook framework层的java代码进行实现, 绕过相关校验,但是可能存在一定的风险
参考文献
- csdn伯努力不努力:Android插件化学习之路
- 拥有者csdnsingwhatiwanna
- csdn郑海波:Android插件化
- 简书H3c:Android插件化开发