Android 通過自己創建Resources加載非身生apk的資源

在我們開發中我們在xml里面寫布局

     <ImageView
        android:src="@mipmap/ic_launcher_round"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

我們的src中賦值@mipmap/ic_launcher_round,然后就會得到對應的資源,那么有沒有好奇我們這些資源是怎么加載進去的呢?我們點進ImageView去查看:

        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
            setImageDrawable(d);
        }

      @Nullable
    public Drawable getDrawable(@StyleableRes 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);
            }
            return mResources.loadDrawable(value, value.resourceId, mTheme);
        }
        return null;
    }

 mResources.loadDrawable(value, value.resourceId, mTheme);

調用的是mResourcesloadDrawable方法,這樣就獲取了相應的值。下面我們通過來分析Resources是怎么創建和尋找對應的資源的。如果我們想要用自己的Resources來加載資源,我們應該怎么做呢?
我們從ActivitygetResources()里面入手:

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }    

可以發現這個mResources是在Context里面獲取到的,在Context里面:

 /**
     * Returns a Resources instance for the application's package.
     * <p>
     * <strong>Note:</strong> Implementations of this method should return
     * a Resources instance that is consistent with the AssetManager instance
     * returned by {@link #getAssets()}. For example, they should share the
     * same {@link Configuration} object.
     *
     * @return a Resources instance for the application's package
     * @see #getAssets()
     */
    public abstract Resources getResources();

這是一個抽象方法,從注釋上我們可以知道這個應該和getAssets(),也就是和AssetManager有關,我們找到Context的實現類ContextImpl查看:

  @Override
    public Resources getResources() {
        return mResources;
    }
   private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
    //....
      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;
    //....
}

發現是ResourcesManager里面調用了getTopLevelResources函數,點進去:

/**
     * 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) {
      //.....

    
        Configuration overrideConfigCopy = (overrideConfiguration != null)
                ? new Configuration(overrideConfiguration) : null;
        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;
                }
            }
        }

        if (overlayDirs != null) {
            for (String idmapPath : overlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }

        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPath(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }

        //Log.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        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();
        }
        r = new Resources(assets, dm, config, compatInfo);
       //....
            return r;
        }
    }
  

從這段代碼我們就可以知道,如果緩存里面有對應的Resources就直接返回,如果沒有就直接new出來,對應的資源路徑就是通過AssetManager.addAssetPath(String path)函數設置,也驗證了上面說的那句話,ResourcesAssetManager有關。

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) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

  private native final int addAssetPathNative(String path);

可以看到調用了一個native方法,從注釋我們可以知道,這個路徑對應的可以是一個目錄或者一個zip文件。我們看看AssetManager的構造方法

/**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * {@hide}
     */
    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

我們可以查看到addAssetPath這個方法和AssetManager的構造方法注釋上面都有{@hide},這個表示我們不能在應用層直接調用,如果我們還是要調用的話就需要用到反射:

            //創建一個AssetManager
            //AssetManager assetManager = new AssetManager(); hide的調用不了  只有用反射調用
            AssetManager assetManager = AssetManager.class.newInstance();
            //添加本地下載好的資源
            //  assetManager.addAssetPath(String path);// 也是hide的調用不了  繼續用反射執行該方法
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.invoke(assetManager, resPath);

通過前面的分析可知,Android系統中實際對資源的管理是AssetManager類.每個Resources對象都會關聯一個AssetManager對象,Resources將對資源的操作大多數委托給了AssetManager。當然有些源碼還有一層 ResourcesImpl 剛剛我們也看到了。
  另外還會存在一個native層的AssetManager對象與java層的這個AssetManager對象相對應,而這個native層AssetManager對象在內存的地址存儲在java層的AssetManager.mObject中。所以在java層AssetManager的jni方法中可以快速找到它對應的native層的AssetManager對象。
AssetManager的init()

  /**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * {@hide}
     */
    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

   // ndk的源碼路徑
   // frameworks/base/core/jni/android_util_AssetManager.cpp
   // frameworks/base/libs/androidfw/AssetManager.cpp
   private native final void init(boolean isSystem);
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
    if (isSystem) {
        verifySystemIdmaps();
    }
    //  AssetManager.cpp
    AssetManager* am = new AssetManager();
    if (am == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", "");
        return;
    }

    am->addDefaultAssets();

    ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
    env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}

bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    // framework/framework-res.apk  
    // 初始化的時候會去加載系統的framework-res.apk資源
    // 也就是說我們為什么能加載系統的資源如顏色、圖片、文字等等
    path.appendPath(kSystemAssets);

    return addAssetPath(path, NULL);
}

AssetManageraddAssetPath(String path)方法

bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
    asset_path ap;

    // 省略一些校驗代碼

    // 判斷是否已經加載過了
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = static_cast<int32_t>(i+1);
            }
            return true;
        }
    }

    // 檢查路徑是否有一個androidmanifest . xml
    Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
            kAndroidManifest, Asset::ACCESS_BUFFER, ap);
    if (manifestAsset == NULL) {
        // 如果不包含任何資源
        delete manifestAsset;
        return false;
    }
    delete manifestAsset;
    // 添加 
    mAssetPaths.add(ap);

    // 新路徑總是補充到最后
    if (cookie) {
        *cookie = static_cast<int32_t>(mAssetPaths.size());
    }

    if (mResources != NULL) {
        appendPathToResTable(ap);
    }

    return true;
}

bool AssetManager::appendPathToResTable(const asset_path& ap) const {
    // skip those ap's that correspond to system overlays
    if (ap.isSystemOverlay) {
        return true;
    }

    Asset* ass = NULL;
    ResTable* sharedRes = NULL;
    bool shared = true;
    bool onlyEmptyResources = true;
    MY_TRACE_BEGIN(ap.path.string());
    // 資源覆蓋機制,暫不考慮
    Asset* idmap = openIdmapLocked(ap);
    size_t nextEntryIdx = mResources->getTableCount();
    ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
    // 資源包路徑不是一個文件夾,那就是一個apk文件了
    if (ap.type != kFileTypeDirectory) {
        // 對于app來說,第一次執行時,肯定為0,因為mResources剛創建,還沒對其操作
        // 下面的分支 指揮在參數是系統資源包路徑時,才執行,
        // 而且系統資源包路徑是首次被解析的
        // 第二次執行appendPathToResTable,nextEntryIdx就不會為0了
        if (nextEntryIdx == 0) {
            // mAssetPaths中存儲的第一個資源包路徑是系統資源的路徑,
            // 即framework-res.apk的路徑,它在zygote啟動時已經加載了
            // 可以通過mZipSet.getZipResourceTable獲得其ResTable對象
            sharedRes = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTable(ap.path);
            // 對于APP來說,肯定不為NULL
            if (sharedRes != NULL) {
                // 得到系統資源包路徑中resources.arsc個數
                nextEntryIdx = sharedRes->getTableCount();
            }
        }
        // 當參數是mAssetPaths中除第一個以外的其他資源資源包路徑,
        // 比如app自己的資源包路徑時,走下面的邏輯
        if (sharedRes == NULL) {
            // 檢查該資源包是否被其他進程加載了,這與ZipSet數據結構有關,后面在詳細介紹
            ass = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTableAsset(ap.path);
            // 對于app自己的資源包來說,一般都會都下面的邏輯
            if (ass == NULL) {
                ALOGV("loading resource table %s\n", ap.path.string());
                // 創建Asset對象,就是打開resources.arsc
                ass = const_cast<AssetManager*>(this)->
                    openNonAssetInPathLocked("resources.arsc",
                                             Asset::ACCESS_BUFFER,
                                             ap);
                if (ass != NULL && ass != kExcludedAsset) {
                    ass = const_cast<AssetManager*>(this)->
                        mZipSet.setZipResourceTableAsset(ap.path, ass);
                }
            }
            // 只有在zygote啟動時,才會執行下面的邏輯
            // 為系統資源創建 ResTable,并加入到mZipSet里。
            if (nextEntryIdx == 0 && ass != NULL) {
                // If this is the first resource table in the asset
                // manager, then we are going to cache it so that we
                // can quickly copy it out for others.
                ALOGV("Creating shared resources for %s", ap.path.string());
                // 創建ResTable對象,并把前面與resources.arsc關聯的Asset對象,加入到這個ResTabl中
                sharedRes = new ResTable();
                sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
                sharedRes = const_cast<AssetManager*>(this)->
                    mZipSet.setZipResourceTable(ap.path, sharedRes);
            }
        }
    } else {
        ALOGV("loading resource table %s\n", ap.path.string());
        ass = const_cast<AssetManager*>(this)->
            openNonAssetInPathLocked("resources.arsc",
                                     Asset::ACCESS_BUFFER,
                                     ap);
        shared = false;
    }

    if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
        ALOGV("Installing resource asset %p in to table %p\n", ass, mResources);
        // 系統資源包時
        if (sharedRes != NULL) {
            ALOGV("Copying existing resources for %s", ap.path.string());
            mResources->add(sharedRes);
        } else {
            // 非系統資源包時,將與resources.arsc關聯的Asset對象加入到Restable中
            // 此過程會解析resources.arsc文件。
            ALOGV("Parsing resources for %s", ap.path.string());
            mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
        }
        onlyEmptyResources = false;

        if (!shared) {
            delete ass;
        }
    } else {
        mResources->addEmpty(nextEntryIdx + 1);
    }

    if (idmap != NULL) {
        delete idmap;
    }
    MY_TRACE_END();

    return onlyEmptyResources;
}

大家應該之前了解過這個文件resources.arsc, 如果沒了解過可以在網上找篇文章看一下。apk在打包的時候會生成它,我們解壓apk就應該能夠看到他。這里面基本都是存放的資源的索引,之所以不同的分辨率可以加載不同的圖片它可是個大功臣。

我們獲取到了assetManager其他的都好辦了,因為資源地址是由這個決定的,所以我們其他的都可以用當前app本身的

           //創建一個AssetManager
            //AssetManager assetManager = new AssetManager(); hide的調用不了  只有用反射調用
            AssetManager assetManager = AssetManager.class.newInstance();
            //添加本地下載好的資源皮膚
            //  assetManager.addAssetPath(String path);// 也是hide的調用不了  繼續用反射執行該方法
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath",         String.class);
             addAssetPathMethod.invoke(assetManager, resPath);
            Resources superResources = context.getResources();
            Resources  mResources = new Resources(assetManager, superResources.getDisplayMetrics(),                 superResources.getConfiguration());

這樣的話我們就可以通過自己創建Resources來加載非本身apk的資源了
`

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容