插桩式插件化跳转
什么是插件化
所谓插件化 就是将app分为一个宿主apk和多个插件apk,由宿主apk启动插件apk,在宿主apk中实现不安装插件apk也可完成之间活动跳转,发送广播,启动服务等。举个例子:比如 支付宝内接了海量的应用,如 ofo小黄车,饿了么等等,首先我们知道他们不可能是在一个项目里完成开发,海量的应用,维护起来简直是个噩梦。支付宝就是一个宿主app,内接的海量应用就是插件apk,当我们在支付宝内点击饿了么图标时,就是先从服务器上下载下饿了么的apk,然后由支付宝调用饿了么插件apk的首页,完成跳转到另外一个apk。
插件化分为几种
插件化 原理上可分为:插桩式 和 Hook式插桩式原理
- 由宿主apk订制一套规则,宿主和插件共同遵守,只有按规则开发才能接入到宿主apk内
- 宿主context语法
- 瞒天过海式代理跳转
下面 一个个解读:
1.宿主app订制的规则是什么,按什么规则开发:
项目中有两个app,一个支付宝app(宿主app),另外一个为饿了么app(插件app),还有一个制定的标准lib,两个app都需要依赖于这个标准,此lib中有三个接口,分别为Acticity,广播,服务 定制的标准接口,以activity的标准为例
public interface PayInterfaceActivity {
/**
* 生命周期的方法
*/
public void attach(Activity proxyActivity);
public void onCreate(Bundle savedInstanceState);
public void onStart();
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onSaveInstanceState(Bundle outState);
public boolean onTouchEvent(MotionEvent event);
public void onBackPressed();
}
两个apk会通过这些接口来进行调用或传值,以实现接口的规则进行开发,后面会有详细代码说明。
- 宿主context语法是什么?
此时你会不会想到 既然我是从一个宿主app内跳转到另外一个没有安装的插件apk时,那插件内部还能否继续调用自身的Context上下文?答案肯定是不能的,因为插件apk都没有进行安装,也没有运行,肯定是拿不到自身上下文的,那么插件app中要用到上下文怎么办?答案是用宿主传过来的Context。
public class BaseActivity extends Activity implements PayInterfaceActivity {
//这是插件的基类 在插件的基类中实现订制标准的接口 在attach中将从宿主app内传过来的上下文进行赋值
//然后重写那些用到上下文的方法,将上下文改为宿主的上下文对象,此即为Context语法
protected Activity that;
@Override
public void attach(Activity proxyActivity) {
this.that = proxyActivity;
}
@Override
public void setContentView(View view) {
if (that != null) {
that.setContentView(view);
}else {
super.setContentView(view);
}
}
@Override
public void setContentView(int layoutResID) {
that.setContentView(layoutResID);
}
@Override
public ComponentName startService(Intent service) {
Intent m = new Intent();
m.putExtra("serviceName", service.getComponent().getClassName());
return that.startService(m);
}
@Override
public View findViewById(int id) {
return that.findViewById(id);
}
@Override
public Intent getIntent() {
if(that!=null){
return that.getIntent();
}
return super.getIntent();
}
@Override
public ClassLoader getClassLoader() {
return that.getClassLoader();
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
return that.registerReceiver(receiver, filter);
}
@Override
public void sendBroadcast(Intent intent) {
that.sendBroadcast(intent);
}
@Override
public void startActivity(Intent intent) {
// ProxyActivity --->className
Intent m = new Intent();
m.putExtra("className", intent.getComponent().getClassName());
that.startActivity(m);
}
@NonNull
@Override
public LayoutInflater getLayoutInflater() {
return that.getLayoutInflater();
}
@Override
public ApplicationInfo getApplicationInfo() {
return that.getApplicationInfo();
}
@Override
public Window getWindow() {
return that.getWindow();
}
@Override
public WindowManager getWindowManager() {
return that.getWindowManager();
}
@Override
public void onCreate(Bundle savedInstanceState) {
}
@Override
public void onStart() {
}
@Override
public void onResume() {
}
@Override
public void onPause() {
}
@Override
public void onStop() {
}
@Override
public void onDestroy() {
}
@Override
public void onSaveInstanceState(Bundle outState) {
}
}
- 重头戏:瞒天过海式代理跳转
3.1 首先在宿主app内将插件的app加载到宿主app的目录中去,在跳转前一定要先完成加载,宿主apk可放在服务器上,在点击跳转前从服务器下载apk到本地。
private void loadPlugin() {
File filesDir = this.getDir("plugin", Context.MODE_PRIVATE);
String name = "pluginb.apk"; //插件名称
String filePath = new File(filesDir, name).getAbsolutePath();
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
InputStream is = null;
FileOutputStream os = null;
try {
Log.i(TAG, "加载插件 " + new File(Environment.getExternalStorageDirectory(), name).getAbsolutePath());
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
File f = new File(filePath);
if (f.exists()) {
Toast.makeText(this, "dex overwrite", Toast.LENGTH_SHORT).show();
}
PluginManager.getInstance().loadPath(this);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
os.close();
is.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
3.2 跳转到插件的MainActivity
public void click(View view) {
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
startActivity(intent);
}
咦,咋一看,这也不对啊,你这是骗我啊,说好得跳转到MainActivity,怎么一言不合就跳转到本地的ProxyActivity中去了,别慌,首先看名字,ProxyActivity 代理activity的意思,作为跳转插件apk的代理app,一定要在xml文件中注册,否则 会报错,一会看ProxyActivity中具体做了什么,先来看看 intent.putExtra中以className为键,以PluginManager.getInstance().getPackageInfo().activities[0].name)为值,这个值传了什么,首先先看最重要的类PluginManager。
public class PluginManager {
private PackageInfo packageInfo;
private Resources resources;
private Context context;
private DexClassLoader dexClassLoader;//首先理解DexClassLoader和PathClassLoader的区别
//简要来说DexClassLoader加载任意路径的类,PathclassLoader只能加载本地自己应用的类
private static final PluginManager ourInstance = new PluginManager();
public static PluginManager getInstance() {
return ourInstance;
}
private PluginManager() {
}
public void setContext(Context context) {
this.context = context;
}
public PackageInfo getPackageInfo() {
return packageInfo;
}
public void loadPath(Context context) {
//找到插件apk的文件 用PackageManager管理器获取PackageInfo,PackageInfo内有我们想要的插件apk的包名,类名等信息。
File filesDir = context.getDir("plugin", Context.MODE_PRIVATE);
String name = "pluginb.apk";
String path = new File(filesDir, name).getAbsolutePath();
PackageManager packageManager=context.getPackageManager();
packageInfo=packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
//根据插件apk的路径,加载路径,缓存路径,和本地类加载器 生成一个插件的类加载器,通过类加载器加载插件中的类
File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(path, dexOutFile.getAbsolutePath()
, null, context.getClassLoader());
try {
//通过反射获取插件中resources 资源,包括布局文件等
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath=AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path);
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch ( Exception e) {
e.printStackTrace();
}
}
public Resources getResources() {
return resources;
}
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
}
}
回过头来再继续看
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
packageInfo.activities[0].name返回的就是插件的第一个activity的全路径,即com.test.eleme.MainActivity,将类全路径传到代理ProxyActivity中去,用ProxyActivity来欺骗AMS,通过AMS检查,接着看最后一个关键点,代理类ProxyActivity中做了什么:
public class ProxyActivity extends Activity {
private String className;
PayInterfaceActivity payInterfaceActivity;
@Override
public void onCreate(@Nullable Bundle savedInstanceState ) {
super.onCreate(savedInstanceState );
//1.获取到从上一个活动传进来的启动插件的类全名
className = getIntent().getStringExtra("className");
try {
//2.用类加载器创建出MainActivity的类
Class activityClass = getClassLoader().loadClass(className);
Constructor constructor = activityClass.getConstructor(new Class[]{});
Object instance= constructor.newInstance(new Object[]{});
//到此 instance已经是MainActivity的类对象了
//将MainActivity强转成订制的activity标准
payInterfaceActivity = (PayInterfaceActivity) instance;
//将上下文对象传入到插件中去,
payInterfaceActivity.attach(this);
//此处可以对插件Activity进行传值
Bundle bundle = new Bundle();
//依次调用插件activity生命周期的方法 完成启动
payInterfaceActivity.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className1=intent.getStringExtra("className");
Intent intent1 = new Intent(this, ProxyActivity.class);
intent1.putExtra("className", className1);
super.startActivity(intent1);
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance().getDexClassLoader();
}
@Override
public Resources getResources() {
return PluginManager.getInstance().getResources();
}
@Override
protected void onStart() {
super.onStart();
payInterfaceActivity.onStart();
}
@Override
protected void onDestroy() {
super.onDestroy();
payInterfaceActivity.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
payInterfaceActivity.onPause();
}
}
总结:到此,插桩式插件化Activity跳转已经完成,首先双方需要共同遵守一套标准的接口,由接口进行传值和调用,用类加载器实现加载插件中的类,用反射的方法调用插件中的资源,插件中因为没有自己的上下文对象,所以凡是用到Context上下文对象时,都要调用从宿主app中传进来的Context对象。