插件化是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,都拿到了,最后给背景设置一下,就可以完成切换了,到这里,本篇博客就到此结束了,这里仅仅是我目前对于插件化一些理解,插件化还有很多需要深入研究的地方,等深入研究之后,会继续和大家进行分享。