现在app体积越来越大,占的手机内存越来越多,这对于承载几十个app的手机来说无疑是个硬伤。怎么样才能做到既可以满足用户多样化需求,又不明显增大app体积呢?答案是,应用插件化技术。插件化思想概括起来:宿主与插件分离,从结构上解耦,即装即用,宿主可动态加载插件,有必要时宿主开放部分接口给插件调用。众多一线互联网公司纷纷提出自家的解决方案,甚至有些个人开发者也开源网站分享了自己源码。其中,360公司基于深度hook思想实现插件化(也有开发者称之为“动态加载”)。接下来主要介绍下DroidPlugin的工作原理与实现过程。
首先谈下核心部分:定义一个BinderHook继承Hook,重写invoke和onInstall方法。然后ContentService、InputMethodManager、LocationManager、NotificationManager、SearchManager、Sms、Telephony、WindowManager、ServiceManager等20种Service以及Manager继承BinderHook,以供后续动态加载调用。另外提供一个HookFatory工厂来安装所有的Hook。
然后是IPluginManagerImpl类,该类提供了加载插件过程的一系列步骤方法。按照处理逻辑归纳,大致如下图:
还有一个进程管理服务,请主要功能为:
1、系统预定义N个进程。每个进程下有4种launchMode的activity,1个服务,一个ContentProvider。
2、每个插件可以在多个进程中运行,这由插件自己的processName属性决定。
3、插件系统最多可以同时运行N个进程,M个插件(M <= N or M >= N)。
4、多个插件运行在同一个进程中,如果他们的签名相同。(可以通过一个开关来决定。)
5、在运行第M+1个插件时,如果预定义的N个进程被占满,最低优先级的进程会被kill掉。腾出预定义的进程用来运行此插件。
另外有一个类RunningActivities。它作用是在activity被创建时,以map形式记录四大组件信息;等到activity被销毁时,四大组件信息从map中移除。
接下来是运用DroidPlugin库,加载外部插件,加载成功后启动该插件。需要在application初始化插件帮助类:
public void onCreate() {
super.onCreate();
PluginHelper.getInstance().applicationOnCreate(getBaseContext());
}
开启子线程,下载apk插件,下载完毕通知handler开始安装apk:
private void downloadAPK(final String apkUrl, final String filename, final ApplicationBean app) {
new Thread(new Runnable() {
@Override
public void run() {
try {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (mProgressBar.getVisibility() != View.VISIBLE) {
mProgressBar.setVisibility(View.VISIBLE);
}
String sdPath = Environment.getExternalStorageDirectory() + "/";
String mSavePath = sdPath + "PluginDownLoad";
File dir = new File(mSavePath);
boolean isMade = false;
if (!dir.exists())
isMade = dir.mkdir();
Log.d("PluginPresenter", isMade + "");
// 下载文件
HttpURLConnection conn = (HttpURLConnection) new URL(apkUrl).openConnection();
conn.setConnectTimeout(15000);
conn.connect();
InputStream is = conn.getInputStream();
int length = conn.getContentLength();
File apkFile = new File(mSavePath, filename + ".apk");
FileOutputStream fos = new FileOutputStream(apkFile);
int count = 0;
byte[] buffer = new byte[2048];
while (!mIsCancel) {
int read = is.read(buffer);
count += read;
// 计算进度条的当前位置
mProgress = (int) (((float) count / length) * 100);
// 更新进度条
mUpdateProgressHandler.sendEmptyMessage(DOWNLOADING);
// 下载完成
if (read < 0) {
Message msg = new Message();
msg.what = DOWNLOAD_FINISH;
msg.getData().putString("key", filename);
msg.getData().putSerializable("app", app);
mUpdateProgressHandler.sendMessage(msg);
break;
}
fos.write(buffer, 0, read);
}
fos.close();
is.close();
}
} catch (Exception e) {
e.printStackTrace();
mUpdateProgressHandler.sendEmptyMessage(DOWNLOAD_ERROR);
}
}
}).start();
}
安装过程中,可能会提示“安装失败,插件请求的权限太多”。这说明你没有为该插件预申请它需要的所有权限,请检查并且在manifest.xml文件声明对应权限。这是DroidPlugin不够完美之处,请理解:事物难以做到完美无缺。哪位开发者热爱钻研插件化技术的可以在此基础上改进,或者提出自己的创新解决方案。
另外,也可能提示“宿主不支持插件的abi环境,可能宿主运行时为64位,但插件只支持32位”。这说明你的手机是64位的CPU架构,但没有为它提供加载so库的路径。此时,你需要在gradle的defultConfig里面列举abi环境:
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86", "mips"
}
在工程的src—>main—>jniLibs目录下,提供对应的文件夹来加载so库:
等等,如果这样还报abi宿主环境的错误。那么需要看下gradle.properties的配置了。在该文件添加:
android.useDeprecatedNdk=true
public void doOpenApp(final AdapterView<?> parent, final int position, String packageName){
final ApplicationBean applicationBean = ((DroidDragAdapter) parent.getAdapter()).getItem(position);
if (applicationBean == null) return;
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(packageName);
if (intent == null) {
return;
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
读到这里,也许大家有疑问:我加载过的插件,不需要了可不可以卸载掉呢?能提出这样问题的读者说明比较细心,答案是可以的。您只需长按该插件图标,弹出对话框询问:“您确定要卸载该应用吗?”点击确定后,开始卸载,实现代码如下:
//卸载app插件
public void doUninstall(final ApkItem item) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("提示");
builder.setMessage("你确定要卸载" + item.title + "吗?");
builder.setNegativeButton("卸载", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!PluginManager.getInstance().isConnected()) {
Toast.makeText(context, "服务未连接", Toast.LENGTH_SHORT).show();
} else {
try {
PluginManager.getInstance().deletePackage(item.packageInfo.packageName, 0);
Toast.makeText(context, "卸载完成", Toast.LENGTH_SHORT).show();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
});
builder.setNeutralButton("取消", null);
builder.show();
}
好了,DroidPlugin加载插件与卸载插件的流程介绍完了。希望对大家有所帮助,如果有什么问题,欢迎交流或者指出。