在360安全卫士一些应用中,有些功能需要添加(下载)后才可以运行,例如360安全卫士中的抢红包功能。
这是因为这些功能被插件化分离出来成一个apk/zip文件,当用户使用这些功能时,再去下载相应的插件(不安装插件apk)来实现功能,当然也可以删除掉插件文件来实现删除功能的效果,实现了功能模块的解耦。
Demo项目的效果图:
【开始时 主应用本身未实现“红包助手”功能,然后点击按钮“添加并运行”按钮后,下载功能插件(未安装)后来实现“红包助手功能”。】
一、主应用apk中的逻辑
-
因为要读文件进行读写,在清单文件中进行权限注册:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
MainActivity中“添加并运行”按钮的点击事件:加载“抢红包的功能”
public void loadRedPaper(View view) { dynamicLoader("redpaper"); }
-
加载功能插件的函数 dynamicLoader(String pluginName) :
不安装功能插件apk的情况下,启动插件apk中的Activity的方案一般是不可以的,因为插件中的Activity没有在我们的主应用的清单文件mainfests中注册过,又因为Fragment不需要注册。所以我们接下来要做的就是获取插件apk中的Fragment,使它加载在我们主应用的宿主Activity中,使用这个宿主Activity专门来装载功能插件apk的Fragment,在Fragment中实现相应的功能。
private void dynamicLoader(String pluginName) { // 查找功能插件apk是否存在: String apkPath = findPlugin(pluginName); if(apkPath==null){ // 不存在时可以从网络上下载,为方便演示这里先忽略 Toast.makeText(this,"请先下载该插件apk",Toast.LENGTH_SHORT).show(); }else { // 启动装载Fragment的宿主Activity Intent intent = new Intent(this,LoaderActivity.class); //传递功能插件apk的存放路径 intent.putExtra("apkPath",apkPath); /** 传递功能插件apk中的功能Fragment的完整类名 * 注意完整类名的设置与功能插件名有关 */ intent.putExtra("class","com.cxmscb.cxm."+pluginName+".DynamicFragment"); // 启动宿主Activity: startActivity(intent); } }
-
查看功能插件apk是否已被下载:
private String findPlugin(String pluginName) { //为方便演示,这里直接将插件apk放置在SD卡根目录 String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+pluginName+".apk"; File apk = new File(apkPath); if(apk.exists()){ return apkPath; } return null; }
二、宿主Activity中的逻辑
宿主Activity专门用来加载功能插件apk/zip中的Fragment。
加载外部功能插件apk/zip使用到了DexClassLoader和AssetManager来构建加载插件apk的类加载器和加载插件资源的Resources对象,具体原理可参考 DexClassLoader&AssetManager中的介绍。
为加载插件apk中的类,我们需要构造一个自己的DexClassLoader来加载插件apk中的dex文件,这样插件中的类才能被找到。
下面我们直接使用:
public class LoaderActivity extends Activity {
//宿主Activity,专用于加载插件apk的Fragment
private String apkPath;//功能插件apk路径
private String className;//功能插件中Fragment的完整类名
//功能插件apk的类加载器、资源对象、资源管理器
private DexClassLoader dexClassLoader;
private Resources resources;
private AssetManager assetManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
apkPath = intent.getStringExtra("apkPath");
className = intent.getStringExtra("class");
try {
// 先准备好装载插件Fragment的容器布局:
FrameLayout frameLayout = new FrameLayout(this);
frameLayout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
// 设置布局id,以备Fragment的插入
frameLayout.setId(2);
//设置宿主Activity界面
setContentView(frameLayout);
// 创建功能插件apk的类加载器
dexClassLoader = new DexClassLoader(apkPath,this.getDir("dex",Context.MODE_PRIVATE).getAbsolutePath(),null,super.getClassLoader());
// 创建功能插件apk的资源管理器
assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class)
.invoke(assetManager, apkPath);
// 创建功能插件apk的资源对象
resources = new Resources(assetManager,this.getResources().getDisplayMetrics(),this.getResources().getConfiguration());
/** 创建好上面三个对象后,重写宿主Activity的三个方法:
* getClassLoader()、getResources()、getAssetManager()
* 这样就可以使用了这三个对象来对功能插件apk中的Fragment进行加载
*/
// 通过反射获取Fragment对象
Fragment fragment = (Fragment) dexClassLoader.loadClass(className).newInstance();
FragmentManager fm = getFragmentManager();
FragmentTransaction fragmentTransaction = fm.beginTransaction();
// 将Fragment对象放入前面定义好的布局当中
fragmentTransaction.add(2,fragment);
fragmentTransaction.commit();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} /*catch (ClassNotFoundException e) {
e.printStackTrace();
}*/ catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
public ClassLoader getClassLoader() {
return dexClassLoader==null?super.getClassLoader():dexClassLoader;
}
@Override
public Resources getResources() {
return resources==null?super.getResources():resources;
}
public AssetManager getAssetManager() {
return assetManager==null?super.getAssets():assetManager;
}
//这样一来,在apk中的Fragment就可以通过R来访问资源
}
三、功能插件Apk中的逻辑
-
创建功能插件的Fagment及其布局文件。
public class DynamicFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { //简单地解析layout文件、获取控件和设置监听 View v = inflater.inflate(R.layout.fragment_dynamic, container, false); final Button button = (Button) v.findViewById(R.id.start); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 弹出Toast,注意Context的传参。 Toast.makeText(getActivity().getApplication(),"开始抢红包",Toast.LENGTH_SHORT).show(); } }); return v; } }
-
注意功能插件的Fragment完整类名的设置,要与主应用的逻辑一致。例:
-
皮肤插件不需要启动Activity:可以清除Activity及其布局文件及其注册。
后续问题:
1.在插件apk打包后可能会对Fragment类名进行混淆,这样会无法被主应用反射到。
2.上述主应用的逻辑并未完整,为了方便演示省去了皮肤插件的下载(不需要安装)
3.功能插件apk最好存放在较私密的地方,为了不方便被清理软件扫描到可更后缀为zip文件
4.既然可以添加插件功能,当然也可以删除插件功能。再添加一个删除功能插件apk文件功能即可。