网友说笑,中国新四大发明:高铁、支付宝、网购、单车。想想也是这个理,反正我的生活中没离开过这几个东西。现在的支付宝可谓是个全能助手了,集成了外卖、淘票票、天猫超市等等。估计没有那个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)); } }
step5 在宿主中启动PluginMainActivity
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的启动以及点击事件的启蒙篇完工了后,其他的三大组件也是慢慢可以类推的。