作为移动端的黑科技,插件化技术一直受大厂的青睐。插件化技术有减少宿主Apk体积,可以独立更新,模块化开发等优点,让宿主APP极具扩展性。那么,现在就来聊聊其中的技术实现,国际惯例,先上效果图
这篇文章将用到以下知识
- dexClassLoader:用来加载插件中的activity
- resources : 用来获取插件中的资源
- PackageInfo:用来获取插件中activity信息
- library:用来创建加载插件规则
一、插件管家——PluginManager
由于插件是没有安装在手机中的,因此我们需要一个对象来管理插件中的资源,保存PackageI的信息,这点与之前一篇的换肤技术有点类似,在此基础上加上获取PackageInfo,这里就不赘述了,show the code
private PackageInfo packageInfo ;
private DexClassLoader dexClassLoader ;
private Resources resources ;
private AssetManager assetManager ;
private Context mCtx ;
public void loadApk(String path){
File cache = mCtx.getDir("dex",mCtx.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(path,
cache.getAbsolutePath(),null,mCtx.getClassLoader());
try {
assetManager = AssetManager.class.newInstance();
Method method = AssetManager.class.getMethod("addAssetPath", String.class);
method.invoke(assetManager,path);
resources = new Resources(assetManager,mCtx.getResources().getDisplayMetrics(),
mCtx.getResources().getConfiguration());
PackageManager packageManager = mCtx.getPackageManager() ;
packageInfo = packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
另外,需要提供get、set来获取packageInfo 等参数,这里就不多写了。
这个类比较简单,主要提供loadApk方法给外部来加载插件的本地路径,在loadApk 方法里,我们通过传进来的path去获取插件的dexClassLoader,然后通过反射的方式获取插件的assetManager对象,接着通过assetManager对象来获取resource对象,最后通过packageManager来获取插件的packageInfo
二、插件的壳
插件没有安装到手机, 没有将组件注册到手机中,这就意味着插件中的activity没有生命周期,不能通过传统的startActivity去启动。那我们应该如何启动插件中的activity呢?这是插件化遇到的第一个难题,既然宿主apk是安装到手机中的,那么我们能不能通过宿主apk给予插件中的activity生命周期呢?答案是肯定的,我们可以在宿主apk中提供一个壳来加载插件中的activity,在空壳的生命周期中调用插件activity的方法,从而实现插件activity的重生~~
既然如此,我们就需要定义一些规则去加载插件中的activity,首先,创建一个library,在Java文件目录下创建一个接口,该接口包含了activity的生命周期,目录如下
PluginInterface 接口代码如下
public interface PluginInterface {
void attach(Activity activity);
void onCreate(Bundle savedInstanceState);
void onStart();
void onStop();
void onDestroy();
}
定义好规则后,再回到宿主目录下创建一个壳,姑且叫PluginActivity吧,在插件中所有activity都要实现PluginInterface接口,因此,我们可以先获取插件的activity的name,获取对应的class,再将class强转为PluginInterface接口,不就可以传递生命周期了吗?
我们先来看宿主的MainActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PluginManager.getInstance().setCtx(this);
File file = new File(Environment.getExternalStorageDirectory(),"plugin-debug.apk");
PluginManager.getInstance().loadApk(file.getAbsolutePath());
}
public void jump(View view){
Intent intent = new Intent(this,PluginActivity.class);
intent.putExtra("activityName",PluginManager.getInstance().getPackageInfo().activities[0].name);
startActivity(intent);
}
mainactivity主要任务是,通过PluginManager来完成插件资源的初始化工作,在jump方法中我们会通过PackageInfo中的ActivityInfo数组来获取插件的MainActivity的name参数,再把该参数通过intent传递给PluginActivity
再来看看PluginActivity的工作
private String activityName ;
private PluginInterface pluginInterface ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityName = getIntent().getStringExtra("activityName");
try {
Class<?> loadClass = PluginManager.getInstance().getDexClassLoader().loadClass(activityName);
pluginInterface = (PluginInterface) loadClass.newInstance();
pluginInterface.attach(this);
pluginInterface.onCreate(savedInstanceState);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
@Override
protected void onStart() {
pluginInterface.onStart();
super.onStart();
}
在oncreate方法中,通过获取到的DexClassLoader去加载目标activity, 然后进行强转为PluginInterface,这时候我们就已经拿到插件的activity了
还有两个重点,在壳activity中,所有获取到的资源应该都是插件中的资源,所以我们需要重写getResource 方法
@Override
public Resources getResources() {
return PluginManager.getInstance().getResources() ;
}
同时在插件中启动其他activity也是需要获取对应的name参数,再重新启动新的PluginActivity
@Override
public void startActivity(Intent intent) {
String activityName = intent.getStringExtra("activityName");
Intent pluginIntent = new Intent(this,PluginActivity.class);
pluginIntent.putExtra("activityName",activityName);
super.startActivity(pluginIntent);
}
再啰嗦几句,这里我们要明白,在插件中启动新的activity,实际上需要启动新的壳去加载对应的资源(列如xml资源),所以需要重写startActivity来不断启动自身。
三、插件本体
终于来到最关键的时刻了~~插件化技术的主角其实是插件(跟没说一样~_~),不啰嗦,show the code
先创建一个新的module,名叫 plugin,工程目录如下
有以下几点需要明确的
- 插件本身没有上下文说法:因为插件没有生命周期,其用到的上下文,是代理壳的上下文
- 插件中的activity所有常用的方法都要重写:由于其没有上下文,所有activity用到的方法都需要交个代理壳去实现
- 在xml文件下的根节点尽量用系统的viewGroup,例如relativelayout,不要用support包下的ConstraintLayout,xml解析会出错(如有误区,欢迎留言)
先来看插件的基类—>BasePluginActivity
public class BasePluginActivity extends Activity implements PluginInterface {
private static final String TAG = "BasePluginActivity";
protected Activity that;
@Override
public void attach(Activity activity) {
this.that = activity;
}
@Override
public void startActivity(Intent intent) {
that.startActivity(intent);
}
@Override
public void setContentView(int layoutResID) {
that.setContentView(layoutResID);
}
@Override
public <T extends View> T findViewById(int id) {
return that.findViewById(id);
}
}
这里由于篇幅,我就列了几个比较重要的方法,记住,所有能用到的方法都要重写,交给代理壳去处理
接下来就跟平常开发一样,唯一不同就是在跳转activity的时候需要获取目标类的name,不能直接传一个string或者class给intent
MainActivity如下
public class MainActivity extends BasePluginActivity {
private static final String TAG = "MainAct";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(that, "come to Plugin ", Toast.LENGTH_SHORT).show();
findViewById(R.id.jump).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(that,"plugin",Toast.LENGTH_SHORT).show();
Intent intent = new Intent();
intent.putExtra("activityName",SecondActivity.class.getName());
startActivity(intent);
}
});
}
}
第二个界面这里就不列出来了,就一个界面
四、权限、运行
给予宿主读写权限,这里要确定APP在运行时候能获取到该权限,详情请Google
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
将我们的插件APP打包,并将打包得到的apk文件copy到手机根目录下(其他目录也行),修改宿主MainActivity 加载apk路径,接着打包宿主APP吧!点击run it~~
源码下载