大家好,我是徐爱卿。博客地址:flutterall.com
网友说笑,中国新四大发明:高铁、支付宝、网购、单车。想想也是这个理,反正我的生活中没离开过这几个东西。现在的支付宝可谓是个全能助手了,集成了外卖、淘票票、天猫超市等等。估计没有那个APP有如此炸天的功能了。问题来了,向外卖、单车、天猫超市这些东西难道说是支付宝APP在发新包中就写死在里面的么?还是只是个H5页面呢?
下面看下支付宝中得天猫超市和淘票票
天猫超市是H5,没什么以外的,毕竟一个APP中使用H5页面很正常。可是淘票票呢?ofo小黄车呢?爱我去,是个native页面,这就厉害了。难道我支付宝的开发人员还要开发维护你ofo小黄车?又或者说我支付宝要集成你ofo小黄车,不可能 !否则的话,支付宝就炸了。
很显然,支付宝是使用了动态加载apk的解决方案。也就是说,支付宝作为一个宿主apk提前将要集成的apk作为一个插件(plugin)下载到本地,然后当使用该plugin(apk)的时候再去加载对应plugin(apk)的资源文件以及对应的native页面。往大了说,就是不去安装该plugin(apk)就可以直接运行该plugin(apk)中的页面。
本博客中得Plugin均指的是第三方apk,也就是相当于支付宝(宿主)中的ofo小黄车(插件-Plugin)。
动态加载Plugin(apk)分析
如何调用一个apk中的页面呢?我们可以动态加载Plugin中的文件资源使其以伪宿主身份运行在宿主apk中。本文以加载一个Activity页面来作为例子进行讲解。
怎么理解呢?
这么理解:如果说系统创建的Activity是一个拥有四肢能动能跳的人的话,那么我们手动创建的Activity只是一个人偶,这个人偶虽然也有四肢,但是他动不了,应为没有对应的掌控者。
这可怎么办?我们可以把这个人偶的四肢与真正的人的四肢绑在一起,这样的话,当真正的人的四肢动了,这个人偶也就动了,看起来人偶分真正的人一样,会动会跳。那么,这里动态加载Plugin中,宿主扮演者控制者,插件扮演者人偶。要让插件中的Activity活起来,我们可以在宿主中创建一个活生的Activity,然后去手动创建插件Activity的实例,然后使用活生的Activity的生命周期去调用插件Activity的生命周期,这样就可以让Plugin中的Activity活了起来。
- Plugin中Activity生命周期的处理
我们可以在宿主中使用一个特殊的Activity,这个Activity是一个空壳,没有任何页面。但是它有实际的Activity的生命周期,这样我们可以通过这个Activity的生命周期去调用我们自己创建的Plugin中的Activity中的生命周期,实现了Plugin中的Activity的伪生命周期。这个宿主Activity命名为ProxyActivity。下面来张图:
- Plugin中资源文件的获取
这个就好办了,我们可以使用AssetManager去得到Plugin包中的资源文件。
加载Plugin实现
step1 PluginInterface
我们的宿主要提供一套标准,这套标准用来规范宿主与Plugin之间的上下文以及生命周期关系的标准。我们称之为:PluginInterface。这个标准涉及到Activity生命周期以及上下文,定义如下:
public interface PluginInterface {
void onCreate(Bundle saveInstance);
void attachContext(FragmentActivity context);
void onStart();
void onResume();
void onRestart();
void onDestroy();
void onStop();
void onPause();
}
我们新建一个依赖库plugin,依赖库plugin中只有一个PluginInterface,这个interface作为一个依赖库的形式存在于宿主与Plugin中。
宿主gradle与Plugin gradle一致如下:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:23.0.0'
testCompile 'junit:junit:4.12'
compile project(':plugin')//重点
}
为了使得编译起来更方便,我这里将宿主apk,插件plugin(项目中称之为otherapk)与依赖库plugin放在同一个项目下,只不过这个项目有两个module。
step2 PluginManager
宿主需要一套工具,这个工具用来管理加载Plugin,以及获取Plugin中资源文件等,定义为:PluginManager。
-
获取Plugin的字节码文件对象
我们要拿到Plugin中的字节码文件对象,需要拿到Plugin对应的DexClassLoader
。可以使用DexClassLoader
的DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
方法。- dexPath是Plugin的路径
- optimizedDirectory是Plugin的缓存路径
- libraryPath可以为null
- parent为父类加载器
这样以来伪代码:new DexClassLoader(dexPath, ProxyActivity.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, ProxyActivityContext.getClassLoader());
就可以拿到Plugin的DexClassLoader了。然后就可以使用DexClassLoader.loadClass(PluginActivityName);
加载到PluginActivity的字节码文件对象了,进而创建PluginActivity的实例。
-
获取Plugin的Resources
我们可以使用Resource提供的下面的构造:
/**
* Create a new Resources object on top of an existing set of assets in an
* AssetManager.
*
* @param assets Previously created AssetManager.
* @param metrics Current display metrics to consider when
* selecting/computing resource values.
* @param config Desired device configuration to consider when
* selecting/computing resource values (optional).
*/
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
}
由于要获取Plugin中的资源,所以这个assets对象应当是Plugin中的资源对象;而对于一款手机的DisplayMetrics和Configuration来说,无论是宿主还是Plugin获取的值都是一样的,所以可以使用宿主的值。
获取AssetManager对象
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
这个path也就是Plugin包在手机中的位置,由于这个方法被hide了,我们需要使用反射。
AssetManager assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, dexPath);
到这里,成功拿到了Plugin的DexClassLoader和Resources。
完整代码如下:
public class PluginManager {
private static PluginManager ourInstance = new PluginManager();
private Context context;
private DexClassLoader pluginDexClassLoader;
private Resources pluginResources;
public PackageInfo getPluginPackageArchiveInfo() {
return pluginPackageArchiveInfo;
}
private PackageInfo pluginPackageArchiveInfo;
public static PluginManager getInstance() {
return ourInstance;
}
private PluginManager() {
}
public void setContext(Context context) {
this.context = context.getApplicationContext();
}
public void loadApk(String dexPath) {
//(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());
pluginPackageArchiveInfo = context.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
//Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
AssetManager assets = null;
try {
assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, dexPath);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
public DexClassLoader getPluginDexClassLoader() {
return pluginDexClassLoader;
}
public Resources getPluginResources() {
return pluginResources;
}
}
step3 ProxyActivity
ProxyActivity是宿主的Activity,这个ProxyActivity只是一个空壳,提供一套生命周期和上下文给我们自己创建的PluginActivity的的实例用的。
再次重申!我们自己加载的PluginActivity实例只是一个对象,没有任何意义的,要给它套上生命周期,给他的上下文赋值。
具体实现思路
启动PluginActivity时,先去启动ProxyActivity,然后再ProxyAcitivity中的oCreate方法中去创建PluginActivity的实例,然后去调用PluginActivity的onCreate方法。在ProxyActivity的onResume方法中调用PluginActivity的onResume方法等等。
具体实现:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//拿到要启动的Activity
String className = getIntent().getStringExtra("className");
try {
//加载该Activity的字节码对象
Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);
//创建该Activity的示例
Object newInstance = aClass.newInstance();
//程序健壮性检查
if (newInstance instanceof PluginInterface) {
pluginInterface = (PluginInterface) newInstance;
//将代理Activity的实例传递给三方Activity
pluginInterface.attachContext(this);
//创建bundle用来与三方apk传输数据
Bundle bundle = new Bundle();
//调用三方Activity的onCreate,
pluginInterface.onCreate(bundle);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
注意:记得重写ProxyActivity的getResources,因为这个时候要拿到的getResources是Plugin的。
/**
* 注意:三方调用拿到对应加载的三方Resources
* @return
*/
@Override
public Resources getResources() {
return PluginManager.getInstance().getPluginResources();
}
完整的ProxyActivity
public class ProxyActivity extends FragmentActivity {
private PluginInterface pluginInterface;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//拿到要启动的Activity
String className = getIntent().getStringExtra("className");
try {
//加载该Activity的字节码对象
Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);
//创建该Activity的示例
Object newInstance = aClass.newInstance();
//程序健壮性检查
if (newInstance instanceof PluginInterface) {
pluginInterface = (PluginInterface) newInstance;
//将代理Activity的实例传递给三方Activity
pluginInterface.attachContext(this);
//创建bundle用来与三方apk传输数据
Bundle bundle = new Bundle();
//调用三方Activity的onCreate,
pluginInterface.onCreate(bundle);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 注意:三方调用拿到对应加载的三方Resources
* @return
*/
@Override
public Resources getResources() {
return PluginManager.getInstance().getPluginResources();
}
@Override
public void startActivity(Intent intent) {
Intent newIntent = new Intent(this, ProxyActivity.class);
newIntent.putExtra("className", intent.getComponent().getClassName());
super.startActivity(newIntent);
}
@Override
public void onStart() {
pluginInterface.onStart();
super.onStart();
}
@Override
public void onResume() {
pluginInterface.onResume();
super.onResume();
}
@Override
public void onRestart() {
pluginInterface.onRestart();
super.onRestart();
}
@Override
public void onDestroy() {
pluginInterface.onDestroy();
super.onDestroy();
}
@Override
public void onStop() {
pluginInterface.onStop();
super.onStop();
}
@Override
public void onPause() {
pluginInterface.onPause();
super.onPause();
}
}
step4 Plugin的BaseActivity的构建
构建Plugin的BaseActivity的原因是统一上下文为ProxyActivity的实例,关于上下文的各种操作均是调用ProxyActivity的实例去进行操作。
public class BaseActivity extends FragmentActivity implements PluginInterface {
//注意:这里命名为protected,以便于子类使用
protected FragmentActivity thisContext;
@Override
public void onCreate(Bundle savedInstanceState) {
}
@Override
public void setContentView(int layoutResID) {
thisContext.setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
thisContext.setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
thisContext.setContentView(view, params);
}
@Override
public LayoutInflater getLayoutInflater() {
return thisContext.getLayoutInflater();
}
@Override
public Window getWindow() {
return thisContext.getWindow();
}
@Override
public View findViewById(int id) {
return thisContext.findViewById(id);
}
@Override
public void attachContext(FragmentActivity context) {
thisContext = context;
}
@Override
public ClassLoader getClassLoader() {
return thisContext.getClassLoader();
}
@Override
public WindowManager getWindowManager() {
return thisContext.getWindowManager();
}
@Override
public ApplicationInfo getApplicationInfo() {
return thisContext.getApplicationInfo();
}
@Override
public void finish() {
thisContext.finish();
}
public void onStart() {
}
public void onResume() {
}
@Override
public void onRestart() {
}
public void onPause() {
}
public void onStop() {
}
public void onDestroy() {
}
public void onSaveInstanceState(Bundle outState) {
}
public boolean onTouchEvent(MotionEvent event) {
return false;
}
public void onBackPressed() {
thisContext.onBackPressed();
}
@Override
public void startActivity(Intent intent) {
thisContext.startActivity(intent);
}
}
PluginMainActivity
public class PluginMainActivity extends BaseActivity implements View.OnClickListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin_main);
findViewById(R.id.btn).setOnClickListener(this);
}
@Override
public void onClick(View v) {
startActivity(new Intent(thisContext,SecondActivity.class));
}
}```
![PluginMainActivity布局](http://upload-images.jianshu.io/upload_images/3884536-0481f91191c0b343.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
##step5 在宿主中启动PluginMainActivity
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void loadApk(View view) {
//注意:使用运行时权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}
public void startApk(View view) {
Intent intent = new Intent(this, ProxyActivity.class);
String otherApkMainActivityName = PluginManager.getInstance().getPluginPackageArchiveInfo().activities[0].name;
intent.putExtra("className", otherApkMainActivityName);
startActivity(intent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PluginManager.getInstance().setContext(this);
PluginManager.getInstance().loadApk(Environment.getExternalStorageDirectory().getAbsolutePath()+"/otherapk-debug.apk");
}
}
别忘记在XML中添加读写SD卡权限了。
最后验证,我们将build的otherapk放到SD卡中(模拟下载),然后点击加载plugin,如下:
结束语
万事开头难,这个Activity的启动以及点击事件的启蒙篇完工了后,其他的三大组件也是慢慢可以类推的。
GitHub地址,欢迎多多start,你的start就是我的动力,谢谢!