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