1. 程式人生 > >Android外掛化淺析

Android外掛化淺析

外掛化是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,都拿到了,最後給背景設定一下,就可以完成切換了,到這裡,本篇部落格就到此結束了,這裡僅僅是我目前對於外掛化一些理解,外掛化還有很多需要深入研究的地方,等深入研究之後,會繼續和大家進行分享。