1.简介
我们在布局文件中使用View的一些属性时,有没有想过是怎么加载进来的? 比如说在布局文件中使用ImageView设置图片时;
<ImageView
android:id="@+id/iv_skin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/skin1" />
下面我们尝试着去源码里边寻找答案;
2.扒源码分析,资源是如何加载的
这里我们还是以最常用的ImageView为例,查看src属性是怎么加载进来的:先查看ImageView.java源码(6.0源码为例)
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initImageView();
// 部分代码如下
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ImageView, defStyleAttr, defStyleRes);
// 通过TypedArray获取图片drawable
Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d); // 设置到imageView上
}
...省略代码...
}
下面追踪 TypedArray.java的getDrawable(int index)方法
@Nullable
public Drawable getDrawable(int index) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
//我们可以看到是通过Resources类来加载drawable图片的--加载资源的方式
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null;
}
其实多看几个View的资源加载过程,都是通过Resources类来加载资源的
3.源码中Resources对象的创建过程分析
我们在activity中也经常这样使用 getResources().getDrawable(R.drawable.account)
那么这个Resources实例是如何创建的呢?
我们点进去追踪 ContextThemeWrapper.java 的
` @Override
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}`
我们接着看 super.getResources()调用,发现
`ContextWrapper.java类
@Override
public Resources getResources()
{
return mBase.getResources();
}`
继续追踪,最终发现是Context.java的抽象方法 public abstract Resources getResources();
我们只能来找Context的实现类了,那么我们从ContextImpl.java这个实现类入手(android.app包下的),查找mResources = 在那里赋值的
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
...省略代码,我们只看我们关心的代码...
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
// 是通过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;
继续追踪LoadedApk.java
public Resources getResources(ActivityThread mainThread) {
// 我们看到采用了缓存策略,继续追踪
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
ActivityThread.java类
/**
* 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());
}
/**
* Creates the top level Resources for applications with the given compatibility info.
*
* @param resDir the resource directory.
* @param splitResDirs split resource directories.
* @param overlayDirs the resource overlay directories.
* @param libDirs the shared library resource dirs this app references.
* @param displayId display Id.
* @param overrideConfiguration override configurations.
* @param compatInfo the compatibility info. Must not be null.
*/
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
...省略不关心的代码...
Resources 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;
}
}
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
Configuration config;
final boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
final boolean hasOverrideConfig = key.hasOverrideConfiguration();
if (!isDefaultDisplay || hasOverrideConfig) {
config = new Configuration(getConfiguration());
if (!isDefaultDisplay) {
applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
}
if (hasOverrideConfig) {
config.updateFrom(key.mOverrideConfiguration);
if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration);
}
} else {
config = getConfiguration();
}
// 可以看到Resources类最终是通过new来进行实例化的
r = new Resources(assets, dm, config, compatInfo);
这里附下AssetManager.java的添加资(产)源文件的方法,可以接收资产文件的目录,或者是压缩文件的路径,
另外观察到该方法是隐藏的方法,是调用不到的,只能通过反射
/**
* 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) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
4.总结下Resources实例的创建流程
packageInfo.getResources(mainThread) --> mainThread.getTopLevelResources --> mResourceManager.getTopLevelResources() -->
r = new Resources(assets, dm, config, compatInfo);
好了,我们知道了Resources的创建流程,就可以自己来实例化一个Resources对象,然后加载一个我们本地的资源文件,我们可以用来做一键换肤功能;
5.通过Resources类,加载另外一个apk文件中的资源
我事先准备了一个plugin.apk,在其res/drawable目录下有一个skin2.png图片,下面我们实现一键切换实现图片应用到到我们apk中来,
我们修改该apk后缀为plugin.zip,然后放置到SD卡的跟目下备用;
新建一个工程,在布局文件中放置一个Button和ImageView,比较简单,上代码不解释.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 无论该根布局是线性还是相对,我们都可以添加NavigationBar了 -->
<Button
android:text="一键换肤"
android:id="@+id/btn_change_skin"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/iv_skin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/skin1" />
</LinearLayout>
在代码中为Button添加点击事件,我们实现一键实现切换图片的功能(一键换肤)
final ImageView ivSkin = (ImageView) findViewById(R.id.iv_skin);
findViewById(R.id.btn_change_skin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//AssetManager assets = new AssetManager(); // 使用{@hide} 标注,隐藏了,我们只能通过反射来调用了
AssetManager assets = null;
try {
assets =AssetManager.class.newInstance();
// 加载资源
// assets.addAssetPath(String path) 又隐藏了 ,反射获取方法,然后再调方法
Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
// method.setAccessible(true); 如果是私用的,修改权限
method.invoke(assets, Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "plugin.zip");
Resources superRes = getResources();
Resources resources = new Resources(assets,superRes.getDisplayMetrics(), superRes.getConfiguration());
// 其实metrics和config都是可以直接new的,这里我们就不直接new了
// DisplayMetrics mMetrics = new DisplayMetrics();
// DisplayMetrics mMetrics = new DisplayMetrics();
int drawableId = resources.getIdentifier("skin2","drawable","com.xialm.skinplugin");
Drawable drawable = resources.getDrawable(drawableId);
ivSkin.setImageDrawable(drawable);
} catch (Exception e) {
e.printStackTrace();
}
}
});
附效果图为: