Android再愛我一次(2)——載入其他app的資源
被xx跳動大佬使勁兒蹂躪了一把,趕緊回來總結總結
本文參考紅橙大神的部落格
<ImageView android:id="@+id/iv" android:src="@mipmap/ic_test" android:layout_width="wrap_content" android:layout_height="wrap_content" /> iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher_background));
一句話就能把圖片資源載入完成,憑啥啊?
1. 翻原始碼
首先,src是通過TypedArray獲取的,點開ImageView的原始碼直接找物件
final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.ImageView, defStyleAttr, defStyleRes); final Drawable d = a.getDrawable(R.styleable.ImageView_src); if (d != null) { setImageDrawable(d); }
有兩個方法值得注意一下:
- getDrawable 返回Drawable物件
- setImageDrawable,這個方法是不是很眼熟
@Nullable public Drawable getDrawableForDensity(@StyleableRes int index, int density) { 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); } if (density > 0) { // If the density is overridden, the value in the TypedArray will not reflect this. // Do a separate lookup of the resourceId with the density override. mResources.getValueForDensity(value.resourceId, density, value, true); } return mResources.loadDrawable(value, value.resourceId, density, mTheme); } return null; }
本質還是呼叫了mResource裡面的方法.
context中的處理
首先看Context關於getresource()方法 , 最終定位到這裡
private Resources getResourcesInternal() { if (mResources == null) { if (mOverrideConfiguration == null) { mResources = super.getResources(); } else { final Context resContext = createConfigurationContext(mOverrideConfiguration); mResources = resContext.getResources(); } } return mResources; }
Resource的例項由Context 的子類建立,但是無法找到mBase的具體例項。 我們現在 ofollow,noindex">ContextImpl 中找找。
private ContextImpl(ContextImpl container, ActivityThread mainThread, LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags, Display display, Configuration overrideConfiguration, int createDisplayWithId) { ...... Resources resources = packageInfo.getResources(mainThread); if (resources != null) { // 不會走此分支,因為6.0中還不支援多屏顯示,雖然已經有不少相關程式碼了,7.0以及正式支援多屏操作了 if (displayId != Display.DEFAULT_DISPLAY || overrideConfiguration != null || (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)) { ...... } } ...... mResources = resources; } // packageInfo.getResources 方法 public Resources getResources(ActivityThread mainThread) { // 快取機制,如果LoadedApk中的mResources已經初始化則直接返回, // 否則通過ActivityThread建立resources物件 if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs, mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this); } return mResources; }
接下來跟到ResourceManager中
Resources getTopLevelResources( String resDir, //app資原始檔夾路徑,實際上是apk檔案的路徑,如/data/app/包名/base.apk String[] splitResDirs,//針對一個app由多個apk組成(將原本一個apk切片為若干apk)時,每個子apk中的資原始檔夾 String[] overlayDirs, String[] libDirs, // app依賴的共享jar/apk路徑 int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { final float scale = compatInfo.applicationScale; Configuration overrideConfigCopy = (overrideConfiguration != null) ? new Configuration(overrideConfiguration) : null; // 以apk路徑為引數建立key 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 && r.getAssets().isUpToDate()) { if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir + ": appScale=" + r.getCompatibilityInfo().applicationScale + " key=" + key + " overrideConfig=" + overrideConfiguration); return r; } } //使用apk的路徑來初始化AssetsManager 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; } } ... // 沒有弱引用,重新建立資源 r = new Resources(assets, dm, config, compatInfo); if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale); synchronized (this) { WeakReference<Resources> wr = mActiveResources.get(key); Resources existing = wr != null ? wr.get() : null; if (existing != null && existing.getAssets().isUpToDate()) { r.getAssets().close(); return existing; } // 將弱引用持有,快取 mActiveResources.put(key, new WeakReference<>(r)); if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size()); return r; } }
接下來不考慮快取的情況 , 我們跟蹤new Resource的情況
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config, CompatibilityInfo compatInfo) { mAssets = assets; mMetrics.setToDefaults(); if (compatInfo != null) { mCompatibilityInfo = compatInfo; } updateConfiguration(config, metrics); assets.ensureStringBlocks(); }
- packageInfo.getResources(mainThread)→
- mainThread.getTopLevelResources() →
- getTopLevelResources中, 已APK的路徑作為key,首先查詢是否存在弱引用,如不存在,直接建立Resource,建立過程必須要有AssetManager,DisplayMetrics,Configuration
AssetManager的建立是通過直接例項化物件呼叫了一個addAssetPath(path)方法把應用的apk路徑新增到AssetManager,addAssetPath()方法請看原始碼解釋。
建立好Resource之後會再去快取中找Resource如果沒有,那麼則會建立Resource並將其快取。 new Resources(assets, dm, config, compatInfo) 具體請看 6.0原始碼240han 。
至此,我們已經大致瞭解了Resource的查詢過程。
3 Demo時間
在getTopLevelResources的方法中,傳入了apk的路徑,這個路徑不止作為key來快取Resource例項,而且是AssetsManager的初始化引數,進而將AssetsManager傳入Resource的構造方法後,才完成了Resource的構造。那麼,如果我們將其他akp的路徑傳進來,會不會就可以在我們的App裡面使用其他apk的資源了?
需求: 點選按鈕改變ImageView的顯示圖片, 且圖片不在本App
首先,建立一個新的moudle, 只放入一張圖片在drawable裡 , 編譯打包,名字隨便改 我這裡寫test.apk
然後我們模擬Resource的建立方法
btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { //獲取已建立好的Resource例項 Resources superResource = getResources(); //AssetsManager建立例項, 可以設定載入目標apk AssetManager manager = AssetManager.class.newInstance(); //新增資源包 Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); String skinPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "test.apk"; //反射執行方法 method.invoke(manager, skinPath); Resources resources = new Resources(manager, superResource.getDisplayMetrics(), superResource.getConfiguration()); //第三個引數為包名 int drawableID2 = resources.getIdentifier("ic_test", "drawable", "ziye.skinplugin"); Drawable drawable = resources.getDrawable(drawableID2); iv.setImageDrawable(drawable); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } });
AssetsManager都帶有@hide標記,無法直接呼叫,這裡用了反射

image