思考android
通常状况下,咱们在设计一个插件化框架的时候,要解决的无非是下面几个问题:缓存
四大组件的动态注册bash
组件相关的类的加载数据结构
资源的动态加载app
实际上从目前的主流插件化框架来看,都是知足了以上的特色,固然由于Activity是你们最经常使用到的,所以一些插件化框架便只考虑了对Activity的支持,好比Small框架,从原理上来看,基本都差很少,Hook了系统相关的API来接管本身的加载逻辑,特别是Hook 了AMS(ActivityManagerService)以及ClassLoader这2个,由于这2个控制着四大组件的加载以及运行逻辑,这里的Hook指的是Hook了远端服务在本地进程的代理对象而已,因为进程隔离的存在,是没办法直接Hook远端进程(Xposed能够Hook掉系统服务,暂时不讨论这个),但根据Binder原理,只须要Hook掉远端进程在本地进程的代理对象便可为咱们服务,从而实现咱们想要的逻辑,而资源的动态加载仅仅是本地进程的事情,今天咱们来简单讨论一下。框架
动态加载资源例子ide
下面咱们首先经过一个例子来讲说,很简单的例子,就是动态加载图片,文本和布局,首先新建一个application的Model,函数
咱们在string.xml加入一个文本,好比:布局
<resources>
<string name="app_name">ResourcesProject</string>
<string name="dynamic_load">动态加载文本测试</string>
</resources>
复制代码
而后弄一个支付宝的图片用来测试,测试
而后写一个布局activity_text.xml用来动态加载,代码以下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:text="动态加载布局"
android:layout_width="wrap_content"
android:textSize="20sp"
android:layout_height="wrap_content" />
</LinearLayout>
复制代码
咱们将这个项目打包成一个apk文件,命名为plugin.apk,打包文件放在assets目录下面,最后放到SD卡目录下面的plugin目录下面就好,代码以下
public static void copyFileToSD(Context context) {
try {
InputStream fis = context.getAssets().open("plugin.apk");
String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
File file = new File(sdPath, "plugin");
if (!file.exists()) {
file.mkdirs();
}
OutputStream bos = new FileOutputStream(file.getAbsolutePath() + File.separator + "plugin.apk");
byte[] buffer = new byte[1024];
int readCount = 0;
while ((readCount = fis.read(buffer)) != -1) {
bos.write(buffer, 0, readCount);
}
bos.flush();
fis.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
固然6.0以上注意一下SD卡权限就好,
private void loadPlugResources() {
try {
String resourcePath = Environment.getExternalStorageDirectory().toString() + "/plugin/plugin.apk";
AssetManager mAsset=AssetManager.class.newInstance();
Method method=mAsset.getClass().getDeclaredMethod("addAssetPath",String.class);
method.setAccessible(true);
method.invoke(mAsset,resourcePath);
/**
* 构建插件的资源Resources对象
*/
Resources pluginResources=new Resources(mAsset,getResources().getDisplayMetrics(),getResources().getConfiguration());
/**
* 根据apk的文件路径获取插件的包名信息
*/
PackageInfo packageInfo=getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
//获取资源的id并加载
int imageId=pluginResources.getIdentifier("alipay","mipmap",packageInfo.packageName);
int strId = pluginResources.getIdentifier("dynamic_load", "string", packageInfo.packageName);
int layoutID = pluginResources.getIdentifier("activity_test", "layout", packageInfo.packageName);
//生成XmlResourceParser
XmlResourceParser xmlResourceParser=pluginResources.getXml(layoutID);
imageView.setImageDrawable(pluginResources.getDrawable(imageId));
textView.setText(pluginResources.getString(strId));
View view= LayoutInflater.from(this).inflate(xmlResourceParser,null);
mView.addView(view,0);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
咱们简单分析一下上面的流程:
1.首先是根据AssetManager 的原理,调用隐藏方法addAssetPath把外部apk文件塞进一个AssetManager ,而后根据
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
}
复制代码
生成一个插件的Resource对象。
2.根据Resources对象调用getIdentifier方法获取了图片,文本以及布局的id,分别设置图片和文本,再动态加载了一个布局,调用Resources.getXml()方法获取XmlResourceParser 来解析布局,最后再加载布局显示,运行如图;
能够看到已经成功加载显示在界面上了。
动态加载资源原理分析
上面咱们看了如何以插件的形式加载外部的资源,实际上不管是加载外部资源,仍是加载宿主自己的资源,它们的原理都是相同的,只要咱们弄懂了宿主自身的资源是如何加载的,那么对于上面的过程天然也就理解了.
在Android中,当咱们须要加载一个资源时,通常都会先经过getResources()方法,获得一个Resources对象,再经过它提供的getXXX方法获取到对应的资源,下面将分析一下具体的调用逻辑,首先是当咱们调用在Activity/Service/Application中调用getResources()时,因为它们都继承于ContextWrapper,该方法就会调用到ContextWrapper的getResources()方法,而该方法又会调用它内部的mBase变量的对应方法,
@Override
public Resources getResources()
{
return mBase.getResources();
}
复制代码
这里的mBase是一个ContextImpl对象,由于Context是一个抽象类,真正的实现是在ContextIImpl里面的,它的getResources()方法,返回的是其内部的成员变量mResources,以下代码:
@Override
public Resources getResources() {
return mResources;
}
复制代码
可见是直接返回了一个mResources对象了,那么这个mResources是怎么来的呢,咱们能够看到是在ContextImpl的构造函数里面赋值的,代码以下:
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
mMainThread = mainThread;
mActivityToken = activityToken;
mRestricted = restricted;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
? createDisplayWithId
: (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;
CompatibilityInfo compatInfo = null;
if (container != null) {
compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
}
if (compatInfo == null) {
compatInfo = (displayId == Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
}
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setConfiguration(overrideConfiguration);
mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
: ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
//resources 是由packageInfo(LoadedApk )的getResources()方法获取;
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
//这里赋值
mResources = resources;
}
复制代码
其中packageInfo的类型为LoadedApk,LoadedApk是apk文件在内存中的表示,它内部包含了所关联的ActivityThread以及四大组件,咱们在ContextImpl中赋值的其实就是它内部的mResources对象,代码以下: `
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
复制代码
能够看到若是为null,那么返回mainThread.getTopLevelResources方法,这个是主线程的方法,若是已经有了,那么就直接返回mResources对象,咱们来看看主线程的getTopLevelResources方法:
/**
* Creates the top level resources for the given package.
*/
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, Configuration overrideConfiguration,
LoadedApk pkgInfo) {
return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo());
}
复制代码
这里也是根据安装的apk的目录来获取的,为了更加理解参数,咱们来debug一下,如图:
经过debug,咱们能够清楚的看到构造Resource对象所必须的参数的来源,所以,只要具有了这些,就能够任意构造,而无论位置是在哪里,所以最终调用的是mResourcesManager的getTopLevelResources方法,其实里面也差很少,主要是建立资源,而后缓存起来,也是利用了AssetManager原理:
//建立ResourcesKey
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
//判断缓存,若是有缓存,直接返回,不然才建立
Resources r;
synchronized (this) {
// Resources is app scale dependent.
if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
//if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
if (r != null && r.getAssets().isUpToDate()) {
if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale
+ " key=" + key + " overrideConfig=" + overrideConfiguration);
return r;
}
}
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
if (splitResDirs != null) {
for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}
// 缓存起来
mActiveResources.put(key, new WeakReference<>(r));
复制代码
下面咱们来分析一下资源的管理者ResourcesManager的一些代码:
private static ResourcesManager sResourcesManager;
private final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources =
new ArrayMap<>();
private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
new ArrayMap<>();
CompatibilityInfo mResCompatibilityInfo;
Configuration mResConfiguration;
public static ResourcesManager getInstance() {
synchronized (ResourcesManager.class) {
if (sResourcesManager == null) {
sResourcesManager = new ResourcesManager();
}
return sResourcesManager;
}
}
复制代码
咱们能够看到是一个单例模式,而且有使用了mActiveResources 做为缓存资源对象,sResourcesManager在整个应用程序中只有一个实例的存在,咱们上面分析了在建立mResources的时候,是首先判断是否有缓存的,若是有缓存了,则直接返回须要的mResources对象,没有的时候再建立而且存入缓存。
ResourcesKey 和ResourcesImpl 以及 Resources 和AssetManager的关系
上面建立资源的代码中都出现了他们,那他们究竟是什么关系呢?
●. Resources其实只是一个代理对象,只是暴露给开发者的一个上层接口,咱们平时调用的getResources().getString(),getgetIdentifier方法等都是给开发者直接用的.对于资源的使用者来讲,看到的是Resources接口,其实在构建Resources对象时,同时也会建立一个ResourcesImpl对象做为它的成员变量,Resources会调用它来去获取资源,而ResourcesImpl访问资源都是经过AssetManager来完成
●. ResourcesKey 是一个缓存Resources的Key,也就是说对于一个应用程序,能够保存不一样的Resource,是否返回以前的Resources对象,取决于ResourcesKey的equals方法是否相等
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ResourcesKey)) {
return false;
}
ResourcesKey peer = (ResourcesKey) obj;
if (!Objects.equals(mResDir, peer.mResDir)) {
return false;
}
if (mDisplayId != peer.mDisplayId) {
return false;
}
if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) {
return false;
}
if (mScale != peer.mScale) {
return false;
}
return true;
}
复制代码
● ResourcesImpl ,看到命名,咱们已经基本明白了是Resources的实现类,其内部包含了一个AssetManager,全部资源的访问都是经过它的Native方法来实现的
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
mAssets.ensureStringBlocks();
}
复制代码
经过构造函数即可以得知mAssets的来源,全部的资源都是经过mAssets访问的,好比:
int getIdentifier(String name, String defType, String defPackage) {
if (name == null) {
throw new NullPointerException("name is null");
}
try {
return Integer.parseInt(name);
} catch (Exception e) {
// Ignore
}
return mAssets.getResourceIdentifier(name, defType, defPackage);
}
复制代码
其余也是相似的。
● AssetManager:做为资源获取的执行者,它是ResourcesImpl的内部成员变量。
经过上面的分析,咱们已经知道了资源的访问最终是由AssetManager来完成,在AssetManager的建立过程当中咱们首先告诉它资源所在的路径,以后它就会去如下的几个地方查看资源,经过反射调用的addAssetPath。动态加载资源的关键,就是如何把包含资源的插件路径添加到AssetManager当中
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
复制代码
能够看到Java层的AssetManager只是个包装,真正关于资源处理的全部逻辑,其实都位于native层由C++实现的AssetManager。 执行addAssetPath就是解析这个格式,而后构造出底层数据结构的过程。整个解析资源的调用链是:
public final int addAssetPath(String path)
=jni=> android_content_AssetManager_addAssetPath
=> AssetManager::addAssetPath => AssetManager::appendPathToResTable => ResTable::add => ResTable::addInternal => ResTable::parsePackage
复制代码
解析的细节比较繁琐,就不细细说明了,有兴趣的能够一层层研究下去。
今天的文章就写到这里,感谢你们阅读。