插件化是2016年移动端最火爆的几个名词之一,目前淘宝、百度、腾讯等都有成熟的动态加载框架,包括apkplug, 本篇博客就来探讨一下插件化设计。本博客主要从以下几个方面对插件化进行解析:
Ø 为什么会提出插件化?
Ø 插件化概述
Ø 插件化例子
1. 为什么会提出插件化?
一个Android应用在开发到了一定阶段以后,功能模块将会越来越多,APK安装包也越来越大。此时可能就需要考虑如何分拆整个应用了。随着Android应用的不断成熟,一般会遇到如下的问题:
1) 代码越来越庞大,维护的困难度增加,应对bug反应越来越慢
2) 需求越来越多,某一模块的小改动都要重新发布版本,发布时间越来越不可控。
3) 还有就是65535方法数的问题,如果超过最大限制,无法编译
在这些问题下,Android插件化开发就应运而生了。
2. 插件化概述
Ø 插件化的概念:
Android 插件化 —— 指将一个程序划分为不同的部分,也就说把一个很大的app分成n多个比较小的app,其中有一个app是主app,比如一般 App 的皮肤样式就可以看成一个插件。目前来说,结合插件包的格式来说插件的方式有三种:
1,apk安装,
2,apk不安装,
3,dex包.
三种方式其实主要是解决两个方面的问题:
1,加载插件中的类,
2,加载插件中的资源.
第一个加载类的问题,这三个方式都可以很好的解决.但目前三种方式都没有很完美的解决第2个问题.
Ø 插件化的优缺点
插件化的优点主要有以下几个方面:
1) 模块解耦,应用程序扩展性强
2) 解除单个dex函数不能超过 65535的限制
3) 动态升级,下载更新节省流量
4) 高效开发(编译速度更快)
Ø 插件化的缺点:
1) 增加了主应用程序的逻辑难度
2) 技术有难度,目前一些成熟的框架都是闭源的
3. 插件化例子
在介绍完插件化的概念和优缺点之后,我们就先一个小的案例,来帮助大家更好的理解插件的原理是什么样的。
先上项目效果图:
项目描述:该Demo很简单,就是点击“切换背景”的按钮之后,会弹出一个PopupWindow,里面是一个listview,这个listview里面item显示是插件的名字,点击相应插件的名字,背景图片就会更改为插件中图片。
布局代码activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/relativeLayout"
android:background="@drawable/kenan1"
tools:context="com.example.jikeyoujikeyou.plugindemo.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/button"
android:text="切换背景"/>
</RelativeLayout>
PopupWindow的布局代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/listview"/>
</LinearLayout>
初始化控件
public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
private ListView mListview;
private RelativeLayout mRelativeLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
mRelativeLayout = (RelativeLayout) findViewById(R.id.relativeLayout);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showPopWindow(view);
}
});
}
点击按钮弹出PopupWindow的逻辑
private void showPopWindow(View v) {
View popview = getLayoutInflater().inflate(R.layout.popwindow_layout, null);
ListView listView = (ListView) popview.findViewById(R.id.listview);
PopupWindow popupwindow = new PopupWindow(popview, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popupwindow.setBackgroundDrawable(getResources().getDrawable(R.drawable.kenan1));
popupwindow.setFocusable(true);
popupwindow.setOutsideTouchable(true);
List<Map<String, String>> pluginList = findPluginList();
if (pluginList == null || pluginList.size() == 0) {
Toast.makeText(this, "手机里并没有插件哦!", Toast.LENGTH_SHORT).show();
return;
}
SimpleAdapter simpleAdapter = new SimpleAdapter(this, pluginList, android.R.layout.simple_list_item_1, new String[]{"label"}, new int[]{android.R.id.text1});
listView.setAdapter(simpleAdapter);
popupwindow.setHeight(100 * pluginList.size());
popupwindow.setWidth(300);
popupwindow.showAsDropDown(v);
listView.setOnItemClickListener(this);
}
这一段代码十分简单,没什么需要解释的,唯一需要强调的是popupwindow.setBackgroundDrawable(getResources().getDrawable(R.drawable.kenan1));必须给popupwindow设置一个背景,否则它弹不出来,具体原因请参考popupwindow源码,这里面有一个findPluginList()方法,这个方法是我自己定义的,用来返回手机中该项目的插件列表,该方法逻辑如下:
private List<Map<String, String>> findPluginList() {
List<Map<String, String>> pluginList = new ArrayList<Map<String, String>>();
//如何获取插件列表?
PackageManager packageManager = this.getPackageManager();
//获取已经安卓的app
List<PackageInfo> packages = packageManager.getInstalledPackages(PackageManager.GET_ACTIVITIES);
//获取当前应用的包信息
try {
PackageInfo currentPackageInfo = packageManager.getPackageInfo(getPackageName(), 0);
for (PackageInfo packageInfo : packages) {
String packageName = packageInfo.packageName;
String shareUserId = packageInfo.sharedUserId;
//判断当前的包,是不是我们需要的插件
//如果是以下三种情况,就不是我们的插件,直接返回
if (currentPackageInfo.packageName.equals(packageName) || !currentPackageInfo.sharedUserId.equals(shareUserId) || TextUtils.isEmpty(shareUserId)) {
continue;
}
//就是我们的插件
Map<String, String> pluginMap = new HashMap<String, String>();
//获取应用程序的名字
String label = packageInfo.applicationInfo.loadLabel(packageManager).toString();
pluginMap.put("packageName", packageName);
pluginMap.put("label", label);
pluginList.add(pluginMap);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return pluginList;
}
这个方法内主要就是通过packageManager获取已经安装在手机里的应用程序列表,然后进行判断是否是我们主应用的插件,如果是的话,就将其应用程序名字和包名存入一个map集合中,然后添加到我创建的pluginList中,值得强调的一点是,如何确定是我们应用的插件呢?在这里我们主要通过在清单文件中声明android:sharedUserId="com.android.plugin",只要主程序和插件程序具有相同的sharedUserId,他们就可以相互识别出来。
以下是我的清单文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.jikeyoujikeyou.plugindemo"
android:sharedUserId="com.android.plugin">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
上述代码,我们就已经完成了popupwindow显示插件列表的逻辑,接下来就是给popupwindow中的listview设置点击事件了,点击之后会进行主程序背景图片的切换,逻辑如下
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
//点击插件,加载资源
//资源需要通过资源加载器进行加载--context
//记住是plugin的context
//1.获取插件的上下文
Context pluginContext = findPluginContext(position);
//2.从插件上下文加载资源
int resId = findResoucesId(pluginContext, position);
if (resId != 0) {
Drawable drawable = pluginContext.getResources().getDrawable(resId);
mRelativeLayout.setBackgroundDrawable(drawable);
}
}
需要加载插件应用中的资源,那就必须使用到插件的上下文,所以我定义了一个方法findPluginContext,来获取插件应用的Context,逻辑如下:
private Context findPluginContext(int position) {
Map<String, String> map = this.findPluginList().get(position);
String packageName = map.get("packageName");
try {
return createPackageContext(packageName, CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
这里有一个方法需要说吗一下createPackageContext(packageName,CONTEXT_IGNORE_SECURITY);该方法可以通过包名来获取对应的上下文。
最后我还定义了一个方法findResoucesId,里面逻辑就是通过反射机制,使用插件的Context来获取R.java文件下的静态类drawable,返回插件应用里的图片id,代码如下:
private int findResoucesId(Context pluginContext, int position) {
//使用反射机制
ClassLoader classLoader = new PathClassLoader(pluginContext.getPackageResourcePath(), PathClassLoader.getSystemClassLoader());
String pluginPackageName = this.findPluginList().get(position).get("packageName");
try {
//获取R下的静态类drawable
Class<?> drawableClass = Class.forName(pluginPackageName + ".R$drawable", true, classLoader);
//获取里面的属性
Field[] fields = drawableClass.getFields();
for (Field field : fields) {
//获取属性名称
String name = field.getName();
if ("kenan1".equals(name)) {
//获取资源的id
return field.getInt(R.drawable.class);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
插件的图片id,都拿到了,最后给背景设置一下,就可以完成切换了,到这里,本篇博客就到此结束了,这里仅仅是我目前对于插件化一些理解,插件化还有很多需要深入研究的地方,等深入研究之后,会继续和大家进行分享。