最近一直在忙公司的業務,有兩個月時間沒有更新博客了,感嘆堅持真是不容易。今天分享一下插件化的一些預備知識點,插件化是一個很大的話題,寫一本書也不必定能說完。總體就是跨APP去加載資源或者代碼,在Android裏面尤爲是加載四大組件,涉及到更多的姿式。今天咱們不涉及四大組件,主要是看下怎麼去跨APP調用代碼或者加載資源。涉及到下面幾個知識點:html
- gradle打包和移動apk
- 資源加載機制,包括resources/assets等
- 移動apk位置,會涉及到兩種io方式
- 構造DexClassLoader
寫了一個小Demo,後面插件化的相關知識都會往這個Demo裏面去補充,先看看此次的實現效果。android
整個Demo裏面會有三個application
工程,一個 library
工程,佈局文件很簡單,點擊上面兩個按鈕,app主工程回去調用另外兩個工程下面的代碼和加載對應的圖片資源。兩個按鈕下面有個TextView
和`ImageView``,分別用來顯示調用代碼返回的字符串和加載獲得的圖片。shell
先看下整個工程的目錄結構api
先看看Demo裏面的多工程配置,主要是是兩類文件, build.gradle
和settings.gradle
, plugin1和plugin2中的build.gradle
基本是同樣的,就看plugin1下面的build.gradle
,要編譯成apk須要使用Android的application插件,一行代碼bash
apply plugin: 'com.android.application' 複製代碼
com這個目錄是要編譯成Android的library,須要加載library插件markdown
apply plugin: 'com.android.library' 複製代碼
com這個Module下面是一個接口文件,另外三個Module都依賴這個工程,在調用的時候就不用去經過反射拿到方法,方便舒爽。接口下就兩個api,一個調用代碼獲取字符串,一個拿到圖片資源。cookie
public interface ICommon {
String getString();
int getDrawable();
}
複製代碼
同時要配置工程根目錄下的settings.gradle
文件,這個目錄是告訴編譯時須要編譯哪幾個工程,app
include ':app', ':plugin1', ':plugin2', ':com' 複製代碼
上面就是項目多工程編譯須要注意的點。另一個就是三個工程都依賴com庫less
dependencies { ... implementation project(':com') } 複製代碼
接下來咱們就須要編譯plugin1和plugin2兩個apk,最終須要再app中去加載這兩個apk文件中的內容,因此咱們在編譯後自動把這兩個apk移動到app的assets目錄下。在assemble
這個task
下面的doLast中去添加移動邏輯就行。ide
assemble.doLast { android.applicationVariants.all { variant -> println "onAssemble===" if (variant.name.contains("release") || variant.name.contains("debug")) { variant.outputs.each { output -> File originFile = output.outputFile println originFile.absolutePath copy { from originFile into "$rootDir/app/src/main/assets" rename(originFile.name, "plugin1.apk") } } } } } 複製代碼
而後在命令行中經過gradle assemble
完成編譯apk並移動的任務。
通過上面的步驟,兩個apk已經移動到app目錄下面的assets,而且分別命名爲plugin1.apk
和plugin2.apk
,接下來看看對apk的操做。
在assets下的資源是不能經過路徑去直接操做的,必須經過AssetManager
,因此咱們把apk複製到包下面進行操做,這就涉及到io操做,有兩種方式能夠,一種是okio,另一種是傳統的Java IO。咱們分別來看下這兩種方式的實現方式和耗時。
先看下okio的方式, okio的方式能夠經過Okio.buffer的方式構造一個讀緩衝區,buffer有個最大值是64K,能夠減小讀的次數。
AssetManager assets = context.getAssets(); InputStream inputStream = null; try { inputStream = assets.open(apkName); Source source = Okio.source(inputStream); BufferedSource buffer = Okio.buffer(source); Log.i(MainActivity.TAG, "" + context.getFileStreamPath(apkName)); buffer.readAll(Okio.sink(context.getFileStreamPath(apkName))); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } 複製代碼
看下用這種方式移動兩種apk的時間須要多久:
另一種方式是傳統的io方式:
AssetManager am = context.getAssets(); InputStream is = null; FileOutputStream fos = null; try { is = am.open(apkName); File extractFile = context.getFileStreamPath(apkName); fos = new FileOutputStream(extractFile); byte[] buffer = new byte[1024]; int count = 0; while ((count = is.read(buffer)) > 0) { fos.write(buffer, 0, count); } fos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { closeSilently(is); closeSilently(fos); } 複製代碼
看下耗時:
固然在傳統方式中把緩衝區改大一點時間上是會快一點,可是okio給咱們提供了緩衝區的自動管理,更省心一點不用擔憂oom,因此仍是推薦用okio的方式。
上面的okio的截圖能夠看出apk最終移動到包下面的files目錄。這裏說一個小知識點,經過run-as 包名就能看見兩個apk了。
adb shell
run-as com.example.juexingzhe.plugindemo
複製代碼
如今已經有了兩個apk了,接下來就是經過操做來調用代碼和資源了。
Android裏面說資源(除了代碼)通常分爲兩類,一類是在/res目錄,一類是在/assets目錄。/res目錄下的資源會在編譯的時候經過aapt工具在項目R類中生成對應的資源ID,經過resources.arsc
文件就能映射到對應資源,/res目錄下能夠包括/drawable圖像資源,/layout佈局資源,/mipmap啓動器圖標,/values字符串顏色style等資源。而/assets目錄下會保存原始文件名和文件層次結構,以原始形式保存任意文件,可是這些文件沒有資源ID,只能使用AssetManager
讀取這些文件。
平時在Activity中經過getResources().getXXX
其實都會經過AssetManager去讀取,好比咱們看下getText
:
@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException { CharSequence res = mResourcesImpl.getAssets().getResourceText(id); if (res != null) { return res; } throw new NotFoundException("String resource ID #0x" + Integer.toHexString(id)); } 複製代碼
看下getDrawable()
:
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException { final Drawable d = getDrawable(id, null); if (d != null && d.canApplyTheme()) { Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme " + "attributes! Consider using Resources.getDrawable(int, Theme) or " + "Context.getDrawable(int).", new RuntimeException()); } return d; } public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) { final TypedValue value = obtainTempTypedValue(); try { final ResourcesImpl impl = mResourcesImpl; impl.getValueForDensity(id, density, value, true); return impl.loadDrawable(this, value, id, density, theme); } finally { releaseTempTypedValue(value); } } 複製代碼
在ResourcesImpl
中會經過loadDrawableForCookie
加載, 若是不是xml類型就直接經過AssetManager
加載,
/** * Loads a drawable from XML or resources stream. */ private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) { ... if (file.endsWith(".xml")) { final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme); rp.close(); } else { final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); is.close(); } } catch (Exception e) { ... } ... return dr; } 複製代碼
若是是xml,會經過調用loadXmlResourceParser
加載,能夠看見最終仍是AssetManager
加載:
@NonNull XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, @NonNull String type) throws NotFoundException { if (id != 0) { try { synchronized (mCachedXmlBlocks) { .... // Not in the cache, create a new block and put it at // the next slot in the cache. final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file); if (block != null) { ... } } catch (Exception e) { final NotFoundException rnf = new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); } 複製代碼
上面簡單說了下Android中資源的類型和它們的關係,因此咱們若是要加載插件中的資源,關鍵就是AssetManager
,而AssetManager
加載資源實際上是經過addAssetPath
來添加資源路徑,而後就能加載到對應資源。
/** * Add an additional set of assets to the asset manager. This can be * either a directory or ZIP file. Not for use by applications. Returns * the cookie of the added asset, or 0 on failure. * {@hide} */ public final int addAssetPath(String path) { return addAssetPathInternal(path, false); } 複製代碼
因此咱們就能夠把插件apk的路徑添加到addAssetPath
中,而後再構造對應的Resources,那麼就能夠拿到插件裏面res目錄下的資源了。而系統addAssetPath
是不對外開放的,咱們只能經過反射拿到。
有了上面思路,代碼實現就簡單了,在Demo裏面點擊按鈕的時候去經過反射拿到addAssetPath
,而後把插件apk的路徑傳給它,而後構造一個新的AssetManager
,和新的Resources
.
public static void addAssetPath(Context context, String apkName) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, pluginInfos.get(apkName).getDexPath()); sAssetManager = assetManager; sResources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } 複製代碼
而後在Activity
中重寫接口,返回新的AssetManager
和Resources
:
@Override public AssetManager getAssets() { return AssetUtils.sAssetManager == null ? super.getAssets() : AssetUtils.sAssetManager; } @Override public Resources getResources() { return AssetUtils.sResources == null ? super.getResources() : AssetUtils.sResources; } 複製代碼
最後奉上一段英文解釋/res和/assets區別的:
Resources are an integral part of an Android application. In general, these are external elements that you want to include and reference within your application, like images, audio, video, text strings, layouts, themes, etc. Every Android application contains a directory for resources (`res/`) and a directory for assets (`assets/`). Assets are used less often, because their applications are far fewer. You only need to save data as an asset when you need to read the raw bytes. The directories for resources and assets both reside at the top of an Android project tree, at the same level as your source code directory (`src/`). The difference between "resources" and "assets" isn't much on the surface, but in general, you'll use resources to store your external content much more often than you'll use assets. The real difference is that anything placed in the resources directory will be easily accessible from your application from the `R` class, which is compiled by Android. Whereas, anything placed in the assets directory will maintain its raw file format and, in order to read it, you must use the [AssetManager] (https://developer.android.com/reference/android/content/res/AssetManager.html) to read the file as a stream of bytes. So keeping files and data in resources (`res/`) makes them easily accessible. 複製代碼
如今就差最後一步,就是經過自定義ClassLoader去加載插件apk中的ICommon
的實現類,而後調用方法獲取字符串和圖像。
ClassLoader
咱們都知道Java能跨平臺運行關鍵就在虛擬機,而虛擬機能識別的文件是class文件,Android的虛擬機Dalvik
和ART
則對class
文件進行優化,它們加載的是dex
文件。
Android系統中有兩個類加載器分別爲PathClassLoader
和DexclassLoader
,PathClassLoader
和DexClassLoader
都是繼承與BaseDexClassLoader
,BaseDexClassLoader
繼承於ClassLoader
,看下Android 8.0裏面的ClassLoader
的loadClass
方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } return c; } 複製代碼
上面就是Java裏面的雙親委託機制,加載一個類都會先經過parent.loadClass
,最終找到BootstrapClassLoader
,若是仍是沒找到,會經過 findClass(name)
去查找,這個就是咱們自定義classLoader
須要本身實現的方法。
可是在Android 8.0系統裏面,
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
複製代碼
這是由於Android的基類BaseDexClassLoader
實現了findClass
去加載指定的class。Android系統默認的類加載器是它的子類PathClassLoader
,PathClassLoader
只能加載系統中已經安裝過的apk,而DexClassLoader
可以加載自定義的jar/apk/dex。
BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent)
複製代碼
兩者構造函數差很少,區別就是一個參數optimizedDirectory
,這個是指定dex優化後的odex文件,PathClassLoader
中optimizedDirector
y爲null,DexClassLoader
中爲new File(optimizedDirectory)
。PathClassLoader
在app安裝的時候會有一個默認的優化odex的路徑/data/dalvik-cache
,DexClassLoader
的dex輸出路徑爲本身輸入的optimizedDirectory路徑。
因此咱們須要去構造一個DexClassLoader
來加載插件的代碼。先抽出一個bean來保存關鍵的信息,一個就是apk的路徑,另一個就是自定義的DexClassLoader
:
/** * 插件包信息 */ public class PluginInfo { private String dexPath; private DexClassLoader classLoader; public PluginInfo(String dexPath, DexClassLoader classLoader) { this.dexPath = dexPath; this.classLoader = classLoader; } public String getDexPath() { return dexPath; } public DexClassLoader getClassLoader() { return classLoader; } } 複製代碼
再接着看下構造DexClassLoader
的方法:
/** * 構造apk對應的classLoader * * @param context * @param apkName */ public static void extractInfo(Context context, String apkName) { File apkPath = context.getFileStreamPath(apkName); DexClassLoader dexClassLoader = new DexClassLoader( apkPath.getAbsolutePath(), context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader()); PluginInfo pluginInfo = new PluginInfo(apkPath.getAbsolutePath(), dexClassLoader); pluginInfos.put(apkName, pluginInfo); } 複製代碼
先看下apk1裏面的接口代碼:
package com.example.juexingzhe.plugin1; import com.example.juexingzhe.com.ICommon; public class PluginResources implements ICommon { @Override public String getString() { return "plugin1"; } @Override public int getDrawable() { return R.drawable.bg_1; } } 複製代碼
很簡單,就是實現com
包下的ICommon
接口,接着看下點擊按鈕時候怎麼去調用代碼和拿到資源的。
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { PluginInfo pluginInfo = AssetUtils.getPluginInfo(APK_1); AssetUtils.addAssetPath(getBaseContext(), APK_1); DexClassLoader classLoader = pluginInfo.getClassLoader(); try { Class PluginResources = classLoader.loadClass("com.example.juexingzhe.plugin1.PluginResources"); ICommon pluginObject = (ICommon) PluginResources.newInstance(); textView.setText(pluginObject.getString()); imageView.setImageResource(pluginObject.getDrawable()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } }); 複製代碼
- 首先調用
addAssetPath
構造AssertManager
和Resources
- 從pluginInfo中拿到
DexClassLoader
,pluginInfo是在onCreate
中賦值的- 經過上面
DexClassLoader
加載apk1中的接口com.example.juexingzhe.plugin1.PluginResources
- 將上面Class構造實例並強轉爲接口
ICommon
,這樣就能夠直接調用方法,不用反射調用- 調用方法得到字符串和圖像資源
簡單總結下,上面經過構造AssetManager
和Resources
去加載插件apk中的資源,固然代碼調用須要經過DexClassLoader
,這個也須要本身去構造,才能加載指定路徑的apk代碼。還簡單介紹了下gradle打包和複製的功能,資源加載,雙親委託機制,IO的兩種方式等。
本文結束。
歡迎你們關注哈。