最近一直在忙公司的业务,有两个月时间没有更新博客了,感叹坚持真是不容易。今天分享一下插件化的一些预备知识点,插件化是一个很大的话题,写一本书也不必定能说完。总体就是跨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的两种方式等。
本文结束。
欢迎你们关注哈。