編輯:關於Android編程
Android插件化顧名思義,就是把APP分成N多插件,可以隨意對插件進行熱插拔。插件化帶來的好處是,減小了軟件耦合,同時開發人員可以模塊開發,提高了開發效率,而且線上bug可以通過升級插件方式快速修復。
一般情況下Android的插件就是一個單獨的apk,開發模式與Android原生應用沒有太大區別。我們要解決的問題是,如何在不安裝的情況下把這個apk運行起來。apk內主要包含class.dex和相關資源文件,class.dex內部是Android虛擬機字節碼。所以要想APP能運行,需要載入Android虛擬機字節碼與插件apk中的資源。
本文寫了一個簡單的插件化框架,下載地址https://github.com/pengyuntao/yuntao-plugin
本插件使用fragment來構建頁面,沒有實現service,receiver,provider等的動態加載,這裡只是作為學習的例子,當然純界面應用也可以使用這種架構來分模塊開發,動態升級替換模塊。
該例子代碼不多,閱讀下文請
對照例子。核心類只有PluginInstallUtils,PluginHostActivity
兩個類。host工程是宿主工程,plugin1,plugin2,plugin3是插件工程,pluginlib是依賴庫工程。運行時候請將三個插件工程打包成apk放在sdcard/yuntao-plugin目錄下,沒有請創建該目錄。
類加載
參考PluginInstallUtils類
要想實現動態加載,第一步需要實現加載插件中的類。JAVA提供了類加載器來加載jar中的類。但是Android識別的是dex文件不同與class文件,所以不能使用JAVA原生的類加載器,還好Android提供了用於加載dex中類的類加載器,DexClassLoader,PathClassLoader。
這兩者的區別在於DexClassLoader需要提供一個可寫的outpath路徑,用來釋放.apk包或者.jar包中的dex文件。換個說法來說,就是PathClassLoader不能主動從zip包中釋放出dex,因此只支持直接操作dex格式文件,或者已經安裝的apk(因為已經安裝的apk在cache中存在緩存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,並且會在指定的outpath路徑釋放出dex文件。
看DexClassLoader構造方法的幾個參數
String dexPath 要加載的apk的絕對路徑
String optimizedDirectory apk解壓出來的目錄
String libraryPath native lib的路徑,可以為null
ClassLoader parent 父類加載器
所以我們可以使用以下代碼方式來加載一個apk中的類到內存中。
private DexClassLoader createDexClassLoader(Context context,String dexPath){?
File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
dexOutputPath = dexOutputDir.getAbsolutePath();
DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, null, context.getClassLoader());
return loader;
}
資源加載
參考PluginInstallUtils類
資源加載的方法是調用AssetManager中的addAssetPath方法,我們可以將一個apk中的資源加載到Resources中,由於addAssetPath是隱藏api我們無法直接調用,所以只能通過反射,通過注釋我們可以看出,傳遞的路徑可以是zip文件也可以是一個資源目錄,而apk就是一個zip,所以直接將apk的路徑傳給它,資源就加載到AssetManager中了,然後再通過AssetManager來創建一個新的Resources對象,這個對象就是我們可以使用的apk中的資源了,這樣我們的問題就解決了。
/**
* 創建AssetManager對象
*
* @param dexPath apk路徑
* @return
*/
private AssetManager createAssetManager(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 創建Resource對象
*
* @param assetManager 上邊方法創建的assetManager
* @return
*/
private Resources createResources(AssetManager assetManager) {
Resources superRes = mContext.getResources();
Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
return resources;
}
如何讓插件環境載入APP上下文
參考PluginHostActivity類
類與資源已經載入到內存中了,那麼如何使用他們呢。
由於我們的界面使用的是fragment,然而fragment必須附加到一個Activity上,因此fragment使用的資源,ClassLoader等都是與附加的Activity相同的。Activity是繼承自Context,Context涉及到資源與類加載的有下列三個抽象方法,我們只要實現下列方法就可以了,讓下列方法返回上兩節中創建的插件的AssetManager,Resources,ClassLoader。
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
/** Return a class loader you can use to retrieve classes in this package.*/
public abstract ClassLoader getClassLoader();
由於我們做的是界面相關的還要再實現一個Theme的方法
/** Return the Theme object associated with this Context.*/
public abstract Resources.Theme getTheme();
最後我們還是要提供一個Activity當做宿主,我們重寫這個Activity的上述四個方法(如果了解Context的架構可以修改ContextImpl也可以,我們直接修改Activity的滿足要求了,就沒必要去改ContextImpl了)。當然這個時候通過反射加載類並調用方法,有興趣可以往類中隨便寫個方法打log測試下。
使用fragment構建頁面
參考PluginHostActivity類
由於大多數APP都是由一個個頁面組成的,使用Activity來構建頁面需要在mainifest文件中注冊Activity,APP在安裝之初內部的Activity就固定了,不能動態任意添加刪除Activity,同時Activity還需要管理生命周期,比較復雜(當然有好多插件框架實現了動態加載任意四大組件,例如DroidPlugin,DL等,有興趣可以自己去了解相關技術),這裡為了簡單我們使用fragment來創建界面,fragment沒有mainifest的限制,同時fragment由FragmentManager管理,可以任意的創建銷毀,所以我們可以做一個純fragment的應用。
/**
* 反射創建fragment,通過fragmentManager把創建的fragment附加到Activity
* @param fragClass
*/
protected void installPluginFragment(String fragClass) {
try {
if (isFinishing()) {
return;
}
ClassLoader classLoader = getClassLoader();
Fragment fg = (Fragment) classLoader.loadClass(fragClass).newInstance();
Bundle bundle = getIntent().getExtras();
fg.setArguments(bundle);
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.primary, fg).commitAllowingStateLoss();
} catch (Exception e) {
e.printStackTrace();
}
}
上述代碼通過fragment類全名打開一個fragment,添加到PluginHostActivity中,詳情參考PluginHostActivity
fragment之間跳轉
參考BaseFragment,PluginHostActivity類
我們設置Activity啟動模式為standard模式,每次打開一個Activity實例附加一個fragment,通過Intent把fragment類名傳遞給Activity,讓Activity去反射創建fragment並且加載他,可以BaseFragment裡封裝一個startFragment方法用來打開頁面
加載多個插件
參考PluginInstallUtils,PluginEnv類
加載插件的過程還是很耗時的,所以我們可以通過一個Map把插件加載的數據緩存起來,下次遇到相同的插件就直接取出。
public final static HashMap mPackagesHolder = new HashMap();
這裡使用apkPath作為key,當然這個key只要是唯一的就行,比如可以定義插件id之類的,總之就是為了不重復加載,下次可以根據這個標志能拿到緩存的數據即可。使用插件id也可以達到通過服務器下發插件id來加載插件的目的。value裡存儲的就是一些插件相關環境,Resource,ClassLoader等
/**
* 插件的運行環境
*/
public class PluginEnv {
public ClassLoader pluginClassLoader;
public Resources pluginRes;
public AssetManager pluginAsset;
public Resources.Theme pluginTheme;
public String localPath;
public PluginEnv(String localPath, ClassLoader pluginClassLoader, Resources pluginRes, AssetManager pluginAsset, Resources.Theme pluginTheme) {
this.pluginClassLoader = pluginClassLoader;
this.pluginRes = pluginRes;
this.pluginAsset = pluginAsset;
this.pluginTheme = pluginTheme;
this.localPath = localPath;
}
}
類重復加載導致的類沖突異常問題
參考PluginClassLoader,PluginInstallUtils類
加載多個插件的時候會遇到一個問題,就是當多個插件都引用一個lib,該lib內的類會被加載多次,這個時候使用這些類的時候,就會發生錯誤。
兩種解決方案:讓依賴包只參與編譯,不打入最終的包內,這個好處是能讓插件包小一些;重寫類加載器,Android的類加載器是支持雙親委派的,可以保證宿主加載lib的類一份就行了。我們這裡使用了第二種重寫類加載器的方式。
public class PluginClassLoader extends DexClassLoader {
public PluginClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
@Override
protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {
//如果vm已經加載了,返回該類,否則返回null
Class clazz = findLoadedClass(className);
if (clazz == null) {
//如果vm沒有加載讓該類的父加載器加載
try {
clazz = this.getParent().loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (clazz == null) {
//當前加載器加載
clazz = findClass(className);
}
}
return clazz;
}
}
插件之間跳轉
參考PluginHostActivity
其實PluginHostActivity是在宿主中注冊的,插件不依賴宿主,所以不能直接顯示的startActivity,我們可以通過action方式來讓宿主的PluginHostActivity來加載其他插件的fragment。
LayoutInflate加載布局文件自定義控件導致類轉換錯誤問題
參考WidgetLayoutInflaterFactory,PluginHostActivity類
在使用插件升級的時候,使用新的插件替換了舊插件,當layout布局文件中寫了自定義控件的時候,打開該頁面通常會拋出以下異常。
......
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.yuntao.host/com.yuntao.pluginlib.PluginHostActivity}: java.lang.ClassCastException: com.yuntao.plugin3.CustomTextView cannot be cast to com.yuntao.plugin3.CustomTextView
......
Caused by: java.lang.ClassCastException: com.yuntao.plugin3.CustomTextView cannot be cast to com.yuntao.plugin3.CustomTextView
......
在LayoutInflate內部有個靜態的Map保存了曾經解析出的View的構造函數,key為控件的name,所以在升級插件之後,由於不同的插件使用了不同的類加載器,類加載器變了,在解析View的時候首先根據name去map中取,會返回舊插件的View。然而當前插件與上個版本插件的ClassLoader不一樣,所以會出現類轉換錯誤。
研究下源碼,下邊代碼為緩存的數據結構
private static final HashMap> sConstructorMap =
new HashMap>();
查看創建view的過程,就是根據view的name,把constructor緩存了起來,存在就直接實例化了,不存在才創建
public final View createView(String name, String prefix, AttributeSet attrs{//略去throws
Constructor constructor = sConstructorMap.get(name);
Class clazz = null;
//略去try catch
if (constructor == null) {
clazz = mContext.getClassLoader().loadClass(?prefix != null ? (prefix + name) : name).asSubclass(View.class);
//此處略去幾行
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
//此處略去n多行
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
}
查看調用createView的代碼片段,調用之前會執行幾個factory的onCreateView方法,如果factory創建了view,則就返回這個view,就不會走createView的邏輯了。
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
所以我們可以給LayoutInflate設置一個factory,由於factory2在最前邊我們設置factory2,在PluginHostActivity的onCreate方法中設置下列代碼
getLayoutInflater().setFactory2(new WidgetLayoutInflaterFactory());
WidgetLayoutInflaterFactory實現Factory2接口,自己接管了自定義控件的創建過程。詳細可以見Demo的WidgetLayoutInflaterFactory類
class WidgetLayoutInflaterFactory implements LayoutInflater.Factory2 {
private final HashMap> sConstructorMap = new
HashMap>();
private final Class[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};
private final Object[] mConstructorArgs = new Object[2];
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
//如果沒有'.',說明是Android系統控件,直接返回null,讓系統自己createView
if (-1 == name.indexOf('.')) {
return null;
}
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = context;
Class clazz = null;
//先從本地緩存讀取
Constructor constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
//沒有緩存,根據類名創建Constructor對象存入緩存
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
}
Object[] args = mConstructorArgs;
args[1] = attrs;
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (NoSuchMethodException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + name);
ie.initCause(e);
throw ie;
} catch (ClassNotFoundException e) {
// If loaded class is not a View subclass
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class not found " + name);
ie.initCause(e);
throw ie;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (clazz == null ? "" : clazz.getName()));
ie.initCause(e);
throw ie;
} finally {
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
}
思考
插件包傳輸過程需要加密,然而虛擬機不能解密,只能解密後加載入虛擬機,要及時刪除解密後的插件。
1.問題是如何發生的,會在什麼情況下發生此類問題?當用戶運用手機清理助手或後台回收我們的應用造成我們應用程序進程被殺死的時候就有可能出現這種空指針的問題,下面舉個例子我們
1.Android中使用Matrix對圖像進行縮放、旋轉、平移、斜切等變換的。Matrix是一個3*3的矩陣,其值對應如下:下面給出具體坐標對應變形的屬性|scaleX,
這次做一個圖片加載器,裡面涉及到線程池,bitmap的高效加載,LruCache,DiskLruCache。接下來我先介紹這四個知識點一.線程池優點:(1)重用線程池中的
ProgressDialog類似於ProgressBar類。用於顯示一個過程,通常用於耗時操作。 幾個方法介紹:1.setMax()設置對話框中進度條的最大值。