Android插件化浅析

2022-06-22 09:50:37 浏览数 (1)

插件化是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

代码语言:javascript复制
<?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的布局代码

代码语言:javascript复制
<?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>

初始化控件

代码语言:javascript复制
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的逻辑

代码语言:javascript复制
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()方法,这个方法是我自己定义的,用来返回手机中该项目的插件列表,该方法逻辑如下:

代码语言:javascript复制
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,他们就可以相互识别出来。

以下是我的清单文件:

代码语言:javascript复制
<?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设置点击事件了,点击之后会进行主程序背景图片的切换,逻辑如下

代码语言:javascript复制
@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,逻辑如下:

代码语言:javascript复制
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,代码如下:

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

0 人点赞