主頁 > 移動端開發 > 深入Android系統(八)Android的資源管理

深入Android系統(八)Android的資源管理

2020-12-20 12:10:13 移動端開發

Android的優勢之一是它幾乎能運行在任何尺寸的設備上,為了能讓同一個apk在不同設備上正常運行,Android設計了一套資源管理系統來完成目標,

Android并不是簡單地將UI布局和圖片進行擴大和縮小來匹配不同配置的設備,而是通過復雜的資源定義方式來保證每種設備都可以有對應的資源檔案,從而讓用戶體驗最佳,

資源系統簡介

Android 應用適應不同設備的方法是盡量為每種型別的設備提供一套資源,理論上雖然可以這樣做,但實際上卻行不通,我們只能為常見的幾種設備型別提供完整的資源,否則應用的占用空間會膨脹到無法接受的程度

常用術語和單位

大部分來自官網,應用資源概覽

Android中常用的單位:

  • dpi:螢屏密度,即每英寸的像素點數,160 dpi表示螢屏每英寸包含160個像素點
  • px:像素,1 px表示一個物理的像素點,px不被推薦使用,但是如果需要通過像素點來控制UI,也可以使用,
  • dpdp是一個虛擬像素單位,1 dp約等于中密度螢屏(160dpi;“基準”密度)上的1像素,對于其他每個密度,Android會將此值轉換為相應的實際像素數,
  • spsp多用于表示字體大小上,spdp概念相似,區別是Android在系統配置中定義一個scale值,spdp的換算關系是sp=dp*scale,通常scale值為1,(官方說法:默認情況下,sp單位與dp大小相同,但它會根據用戶的首選文本大小來調整大小,)

Android把螢屏尺寸歸為4類:

  • small:尺寸類似于低密度 VGA 螢屏的螢屏,小螢屏的最小布局尺寸約為 320x426 dp,例如,QVGA 低密度螢屏和 VGA 高密度螢屏,
  • normal:尺寸類似于中等密度 HVGA 螢屏的螢屏,標準螢屏的最小布局尺寸約為 320x470 dp,例如,WQVGA 低密度螢屏、HVGA 中等密度螢屏、WVGA 高密度螢屏,
  • large:尺寸類似于中等密度 VGA 螢屏的螢屏,大螢屏的最小布局尺寸約為 480x640 dp,例如,VGA 和 WVGA 中等密度螢屏,
  • xlarge:明顯大于傳統中等密度 HVGA 螢屏的螢屏,超大螢屏的最小布局尺寸約為 720x960 dp,在大多數情況下,螢屏超大的設備體積太大,不能放進口袋,最常見的是平板式設備,此項為 API 級別 9 中的新增配置,

Android把螢屏密度(dpi)分為:

  • ldpi:低密度螢屏;約為 120dpi,
  • mdpi:中等密度(傳統 HVGA)螢屏;約為 160dpi,
  • hdpi:高密度螢屏;約為 240dpi,
  • xhdpi:超高密度螢屏;約為 320dpi,此項為 API 級別 8 中的新增配置
  • xxhdpi:絕高密度螢屏;約為 480dpi,此項為 API 級別 16 中的新增配置
  • xxxhdpi:極高密度螢屏使用(僅限啟動器圖示);約為 640dpi,此項為 API 級別 18 中的新增配置
  • nodpi:可用于您不希望為匹配設備密度而進行縮放的位圖資源,
  • tvdpi:密度介于 mdpi 和 hdpi 之間的螢屏;約為 213dpi,此限定符并非指“基本”密度的螢屏,它主要用于電視,且大多數應用都不使用該密度 — 大多數應用只會使用 mdpi 和 hdpi 資源,而且系統將根據需要對這些資源進行縮放,
  • anydpi:此限定符適合所有螢屏密度,其優先級高于其他限定符,這非常適用于矢量可繪制物件,此項為 API 級別 21 中的新增配置

系統資源定義

對于下面的Layout資源定義:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
</LinearLayout>

上面的定義中有各種各樣的符號,例如LinearLayoutlayout_widthorientation等,這些符號在哪里定義的呢?語法規則優勢什么呢?

Android的資源系統并不想看上去那么簡單,Android利用了xml定義了一套完整的資源語言,我們來具體看下

定義屬性

Framework資源目錄frameworks/base/core/res/res/values的目錄下,有一個attrs.xml檔案,在這個檔案里,Android定義了資源的屬性值,以LinearLayout為例,相關的定義如下:

    <declare-styleable name="LinearLayout">
        <attr name="orientation" />
        <attr name="gravity" />
        <attr name="baselineAligned" format="boolean" />
        <attr name="baselineAlignedChildIndex" format="integer" min="0"/>
        <attr name="weightSum" format="float" />
        <attr name="measureWithLargestChild" format="boolean" />
        <attr name="divider" />
        <attr name="showDividers">
            <flag name="none" value="0" />
            <flag name="beginning" value="1" />
            <flag name="middle" value="2" />
            <flag name="end" value="4" />
        </attr>
        <attr name="dividerPadding" format="dimension" />
    </declare-styleable>

從檔案中不難看出,LinearLayout中所使用的各種屬性正是在這里定義的,不但定義了屬性的名稱,還有屬性值的格式也做了定義

對于UI的基本元素widget而言,最重要的是獲得預先定義好的各種屬性值,于是在attrs.xml檔案中,通過declare-styleable的方式定義了一套屬性的集合,我們再看下LinearLayout類的構造方法:

    public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        ......
        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
        int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
        if (index >= 0) {
            setOrientation(index);
        }
        ......
        a.recycle();
    }
  • 在建構式中,LinearLayout呼叫方法obtainStyledAttributes()來創建一個屬性集合
  • a.getInt()用來獲取屬性
    • 第一個引數是com.android.internal.R.styleable.LinearLayout,正是attrs.xml檔案中定義的屬性集合名稱
    • 還有一個引數是預設值-1,如果在撰寫layout檔案時必須給每個widget的所有屬性都賦值,這會變得很繁瑣,因此在讀取每個屬性時,代碼中都會給出一個預設值

給屬性賦值

LinearLayout的構造方法中有一個defStyleAttrdefStyleRes用來指定使用的stylestyle就是一些預定義的屬性值,例如:

    <style name="Widget.Button">
        <item name="background">@drawable/btn_default</item>
        <item name="focusable">true</item>
        <item name="clickable">true</item>
        <item name="textAppearance">?attr/textAppearanceSmallInverse</item>
        <item name="textColor">@color/primary_text_light</item>
        <item name="gravity">center_vertical|center_horizontal</item>
    </style>

這段xml定義了一個名為Widget.Buttonstyle,使用style的方式是在layout定義中加入下面的陳述句:

style="@style/Widget.Button"

對于style的命名,為什么要用.分割呢?Android用這種方式表示一種繼承關系,Widget.Button的含義是當前style繼承了Widget的所有屬性值

除了通過名字來表示繼承關系外,還可以通過<style/>標簽的parent屬性來指定,如:

    <style name="Widget.PopupMenu" parent="Widget.ListPopupWindow">
    </style>

主題Theme

theme就是所有UI屬性的集合,作為基礎定義的Theme本身是一個很龐大的style,而其他theme基本上都是通過Theme派生出來的,我們看下frameworks/base/core/res/res/values/themes.xml檔案定義:

    <style name="Theme">
        <item name="isLightTheme">false</item>
        <item name="colorForeground">@color/bright_foreground_dark</item>
        <item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item>
        ......
    </style>
    <!-- Variant of {@link #Theme} with no title bar -->
    <style name="Theme.NoTitleBar">
        <item name="windowNoTitle">true</item>
    </style>

資源型別

在Android應用的原始碼目錄下,通常有兩個和資源相關的目錄:assetsres目錄,這兩個目錄下的檔案都會被打包進APK檔案中

Android規定各類資源存放在res/目錄,目錄中支持的資源目錄表如下:

目錄資源型別
animator用于定義屬性影片的 XML 檔案,
anim用于定義漸變影片的 XML 檔案,(屬性影片也可保存在此目錄中,但為了區分這兩種型別,屬性影片首選 animator/ 目錄,)
color用于定義顏色狀態串列的 XML 檔案
drawable位圖檔案(.png、.9.png、.jpg、.gif)或編譯為以下可繪制物件資源子型別的 XML 檔案:位圖檔案、九宮格(可調整大小的位圖)、狀態串列、形狀、影片可繪制物件、其他可繪制物件
mipmap適用于不同啟動器圖示密度的可繪制物件檔案
layout用于定義用戶界面布局的 XML 檔案
menu用于定義應用選單(如選項選單、背景關系選單或子選單)的 XML 檔案
raw需以原始形式保存的任意文件,如要使用原始 InputStream 打開這些資源,請使用資源 ID(即 R.raw.filename)呼叫 Resources.openRawResource(),但是,如需訪問原始檔案名和檔案層次結構,則可以考慮將某些資源保存在 assets/ 目錄(而非 res/raw/)下,assets/ 中的檔案沒有資源 ID,因此您只能使用 AssetManager 讀取這些檔案,
values包含字串、整型數和顏色等簡單值的 XML 檔案,
xml可在運行時通過呼叫 Resources.getXML() 讀取的任意 XML 檔案,各種 XML 組態檔(如可搜索配置)都必須保存在此處,
font帶有擴展名的字體檔案(如 .ttf.otf.ttc),或包含 <font-family> 元素的 XML 檔案,

對于上面的animatoranimcolordrawablelayoutmenurawvaluesxml的9種目錄下存放的是預設資源,每種資源都可以有候選資源候選資源的存放格式類似<resources_name>-<qualifier>的目錄下:

  • <resources_name>就是前面的這9種預設資源的目錄名
  • <qualifier>是一些限定符的組合,用來區分候選資源型別,
  • 關于候選資源的命名規則和匹配演算法,在Android的官網上有著詳細描述,大家可以參考這里:官網傳送門

assets目錄

assets目錄下保存的檔案不能通過存取資源的方式在代碼中訪問到,訪問assets目錄下檔案的方式更像是打開一個檔案,例如:

AssetManager am = Context.getAssets();
InputStream is = am.open(filePath);

assets目錄下可以再建目錄,沒有限制,目錄下存放的檔案在編譯程序中不會被改動,會被原封不動的打包進APK中,

raw目錄

res目錄下有個raw目錄,放置在該目錄下的檔案也不會被Android改動,但是raw目錄下不能在創建子目錄,

訪問raw目錄下資源的方式和訪問其他資源是一致的:

InputStream is = getResources().openRawResource(R.id.filename);

同時,raw目錄下的資源也能像其它資源一樣有備選資源,能夠被overlay目錄中的資源覆寫掉,這也是assets目錄下的檔案不具備的特性,

Android 資源管理的實作原理

上面我們了解到Android中的資源是如此的紛繁復雜,那么

  • Android是如何加載、管理資源的呢?
  • 我們知道Android應用可以使用的資源有3中來源:本apk、系統Framework、其他apk,Android如何統一管理控制呢?
  • 不同apk間的資源包如何共享呢?
  • 對于資源加載,反復加載會浪費記憶體嗎?資源加載會有效率問題嗎?

關于這幾個問題的答案,我們需要從原理上來尋找!

Resources類的作用

Android資源的裝載是通過Resources類來完成的,

還記的Zygote行程的預加載么?
就是通過Resources.getSystem().startPreloading();來實作的,我們來看看Resources這個類到底做了什么

我們先看下核心的成員變數:

class Resources{
    // Resources的實體物件
    static Resources mSystem = null;
    // 真正實作類ResourcesImpl的實體物件
    private ResourcesImpl mResourcesImpl;
}
class ResourcesImpl{
    // 預加載相關資源
    private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
    private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
            = new LongSparseArray<>();
    private static final LongSparseArray<android.content.res.ConstantState<ComplexColor>>
            sPreloadedComplexColors = new LongSparseArray<>();
    // Drawable 和 ColorStateList 的快取
    private final DrawableCache mDrawableCache = new DrawableCache();
    private final DrawableCache mColorDrawableCache = new DrawableCache();
    // XML檔案的快取 XML_BLOCK_CACHE_SIZE=4,寫死的
    private int mLastCachedXmlBlockIndex = -1;
    private final int[] mCachedXmlBlockCookies = new int[XML_BLOCK_CACHE_SIZE];
    private final String[] mCachedXmlBlockFiles = new String[XML_BLOCK_CACHE_SIZE];
    private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[XML_BLOCK_CACHE_SIZE];
    // AssetManager實體的
    final AssetManager mAssets;
}

上面的代碼中,Resources類有兩個重要的變數:

  • Resources mSystem
    • Resources類的實體物件,用來管理系統資源
  • ResourcesImpl mResourcesImpl
    • 9.0版本的原始碼中,Resources類具體業務交給了ResourcesImpl來實作

我們下面具體來看下

mSystem的初始化

mSystem是一個參考Resources類自身實體的靜態變數,我們看下mSystem變數是如何初始化的:

    public static Resources getSystem() {
        synchronized (sSync) {
            Resources ret = mSystem;
            if (ret == null) {
                ret = new Resources();
                mSystem = ret;
            }
            return ret;
        }
    }

靜態方法getSystemmSystem為空的情況下會創建一個Resources物件的實體,我們看下這個無參構造:

    /**
     * class:Resources.java
     * Only for creating the System resources.
     */
    private Resources() {
        mClassLoader = ClassLoader.getSystemClassLoader();
        final DisplayMetrics metrics = new DisplayMetrics();
        metrics.setToDefaults();
        final Configuration config = new Configuration();
        config.setToDefaults();
        mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                new DisplayAdjustments());
    }
    /**
     * class:ResourcesImpl.java
     */
    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());
    }

這個兩個構造方法比較簡單,我們需要重點關注的是

  • Resource類私有構造方法的注釋:Only for creating the System resources.
  • 在創建ResourcesImpl實體時傳入的AssetManager物件,是通過靜態方法AssetManager.getSystem()來獲取的
    • 這里暫不深究,后面會單獨講解AssetManager類哈

我們接下來就來找找Resource.getSystem()方法最早是在什么地方呼叫的?

大家在frameworks目錄下grep一下就會發現,最早的函式呼叫其實也就是在Zygote行程初始化時:

/**
 * preloadResources 函式呼叫關系展示
 */
class ZygoteInit{
    public static void main(String argv[]) {
        preload();
    }
    static void preload(...) {
        preloadResources();
    }
    private static void preloadResources() {
        mResources = Resources.getSystem();
        mResources.startPreloading();
        ......
        preloadDrawables(ar);
        ......
        preloadColorStateLists(ar);
        ......
        mResources.finishPreloading();
    }
    private static int preloadDrawables() {
        ......
        mResources.getDrawable(id, null);
        ......
    }
    private static int preloadColorStateLists() {
        ......
        mResources.getColorStateList(id, null);
        ......
    }
}

由此,在Zygote行程初始化時mSystem就被賦值了,我們知道,所有的應用程式都是從Zygote行程fork而來的,這意味著所有行程中的mSystem參考的Resource實體在整個Android系統中是共享的,這部分其實也就是Framework的資源

getDrawable的程序

ZygotepreloadResources方法中得到系統的Resources實體后,會呼叫這個實體的getDrawablegetColorStateList方法去加載系統的Drawable資源和ColorStateList資源,

getDrawable(id,null)方法為例,我們來看下具體干了啥:

    public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme){
        // 請注意此處傳入的 density=0
        return getDrawableForDensity(id, 0, theme);
    }
    public Drawable getDrawableForDensity(...) {
        return mResourcesImpl.loadDrawable(this, value, id, density, theme);
    }

最后還是呼叫了具體實作類ResourcesImplloadDrawable方法,我們來看下整體流程:

    Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
            int density, @Nullable Resources.Theme theme)
            throws NotFoundException {
        // getDrawable 傳入的引數density = 0
        // 所以對于Zygote來說,useCache始終為true
        final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
        try {
            final boolean isColorDrawable;
            final DrawableCache caches;
            final long key;
            // 判斷要加載的 Drawable 型別,普通資源還是Color
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }
            // 非預加載階段才會走到這里
            // 檢查要加載的資源是否已經存在于cache中
            // 有的話直接回傳,沒有找到繼續檢查預加載相關的cache
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    ......
                    return cachedDrawable;
                }
            }
            ......
            // 檢查預加載相關的cache
            // ConstantState這是一個抽象類
            // 每個Drawable的子類都要實作它,可以用來生成對應的Drawable實體
            // 很經典的一個類
            final Drawable.ConstantState cs;
            if (isColorDrawable) {
                cs = sPreloadedColorDrawables.get(key);
            } else {
                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
            }
            ......
            Drawable dr;
            boolean needsNewDrawableAfterCache = false;
            if (cs != null) {
                // 在預加載cache中找到資源
                // 生成對應的Drawable實體
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                // 預加載cache中沒有找到資源
                // 直接創建Color類的資源,不是我們這次分析的重點哈
                dr = new ColorDrawable(value.data);
            } else {
                // 預加載cache中沒有找到資源 
                // 通過 loadDrawableForCookie 從XML中加載資源
                dr = loadDrawableForCookie(wrapper, value, id, density);
            }
            ......
            // 對Drawable實體進行簡單的配置作業
            if (dr != null) {
                dr.setChangingConfigurations(value.changingConfigurations);
                if (useCache) {
                    // 通過 cacheDrawable 進行資源快取
                    cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
                    ......
                }
            }
            return dr;
        } catch (Exception e) {
        }
    }

從上面代碼看,當快取中沒有找到需要的Drawable資源,會通過loadDrawableForCookie來加載資源,最后根據需要把資源快取到cache中,

先來看下loadDrawableForCookie的核心代碼如下:

    if (file.endsWith(".xml")) {
        // 加載XML型別的drawable資源
        final XmlResourceParser rp = loadXmlResourceParser(
            file, id, value.assetCookie, "drawable");
        dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
        rp.close();
    } else {
        // 加載圖片型別的drawable資源
        final InputStream is = mAssets.openNonAsset(
            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
        AssetInputStream ais = (AssetInputStream) is;
        dr = decodeImageDrawable(ais, wrapper, value);
    }
    return dr;

資源裝載分成了xmlimage兩種:

  • xml:通過loadXmlResourceParser()來加載,流程主要分為兩部分:
    • 檢查快取中是否已經存在:
    // First see if this block is in our cache.
    final int num = cachedXmlBlockFiles.length;
    for (int i = 0; i < num; i++) {
        if (cachedXmlBlockCookies[i] == assetCookie 
        && cachedXmlBlockFiles[i] != null
        && cachedXmlBlockFiles[i].equals(file)) {
            return cachedXmlBlocks[i].newParser();
        }
    } 
    
    • 快取中不存在,通過AssetManager來裝載檔案并快取
    // 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) {
        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
        mLastCachedXmlBlockIndex = pos;
        final XmlBlock oldBlock = cachedXmlBlocks[pos];
        if (oldBlock != null) {
            oldBlock.close();
        }
        cachedXmlBlockCookies[pos] = assetCookie;
        cachedXmlBlockFiles[pos] = file;
        cachedXmlBlocks[pos] = block;
        return block.newParser();
    }
    
  • image:通過mAssets.openNonAsset來加載二進制流,并通過decodeImageDrawable()來轉化為相應的Drawable物件

到這里,對于Resource類的作用我們就梳理的差不多了,主要的功能是快取DrawableColorStateListXML檔案等資源,而具體的加載細節,都和一個叫AssetManager的類有關系,

AssetManager等下再看,我們可以先總結一下:

  • 代表系統Framework資源的Resources實體系統只有一份,通過Resources類中的靜態成員變數mSystem參考,
  • 系統中預加載的Drawable資源和ColorStateList資源也是通過Resources類中的靜態成員變數參考,并且在整個系統中共享,不會浪費記憶體
  • 應用中裝載的DrawableColorStateListXML資源在Resources中會被快取,因此反復裝載這些資源的操作不會真正的重復加載,不會浪費額外的記憶體

AssetManager類的作用

AssetManager類是Android中實際管理資源的模塊,在Java層native層都有代碼,我們先看Java層AssetManager

Java層AssetManager

我們先看下AssetManager類中的變數:

    // Not private for LayoutLib's BridgeAssetManager.
    @UnsupportedAppUsage
    @GuardedBy("sSync") static AssetManager sSystem = null;
    @GuardedBy("sSync") private static ApkAssets[] sSystemApkAssets = new ApkAssets[0];
    @GuardedBy("sSync") private static ArraySet<ApkAssets> sSystemApkAssetsSet;
    
    // Pointer to native implementation, stuffed inside a long.
    @UnsupportedAppUsage
    @GuardedBy("this") private long mObject;
    // The loaded asset paths.
    @GuardedBy("this") private ApkAssets[] mApkAssets;
  • sSystem是和Resources類中mSystem對應的AssetManager物件,用來管理系統Framewrok的資源
    • 大家可以回顧上面的 mSystem的初始化 部分
  • sSystemApkAssetssSystemApkAssetsSet都是和系統資源相關的ApkAssets的集合,在Zygote初始化時就已經創建完成
    • 有沒有很好奇ApkAssets到底是啥?
      • 官方說明如下:
      /**
      * The loaded, immutable, in-memory representation of an APK.
      *
      * The main implementation is native C++ and there is very little API surface exposed here. The APK
      * is mainly accessed via {@link AssetManager}.
      *
      * Since the ApkAssets instance is immutable, it can be reused and shared across AssetManagers,
      * making the creation of AssetManagers very cheap.
      * @hide
      */
      public final class ApkAssets {
              @GuardedBy("this") private final long mNativePtr;
              @GuardedBy("this") private final StringBlock mStringBlock;
      }
      private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
          throws IOException {
          mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
          mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
      }
      public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
          return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/);
      }
      
      • ApkAssets主要是與native層進行聯系,通過各種native呼叫加載相關資源
      • ApkAssets還有一個功能是把native層加載字串資源保存到mStringBlock
    • sSystemApkAssets是一個ApkAssets的陣列,通常一個ApkAssets管理的是一個資源包中的資源,用陣列的話說明可能會管理多個資源包
  • mObject:用來存盤在native層創建的AssetManager物件指標
  • mApkAssets:記錄所有已經加載的asset資源,也包括sSystemApkAssets
    • 相關的實作程序可以參照AssetManagersetApkAssets()函式

還記得Resources初始化時呼叫的AssetManager.getSystem()函式么?我們來看下具體干了些啥?

注釋比較詳細:

    private AssetManager(boolean sentinel) {
        // nativeCreate()的作用是創建一個native層的AssetManager實體,并回傳物件指標
        // 從9.0的原始碼看,創建的是AssetManager2的實體
        mObject = nativeCreate();
    }
    public static AssetManager getSystem() {
        synchronized (sSync) {
            createSystemAssetsInZygoteLocked();
            return sSystem;
        }
    }
    private static void createSystemAssetsInZygoteLocked() {
        if (sSystem != null) {
            return;
        }
        final ArrayList<ApkAssets> apkAssets = new ArrayList<>();
        // 通過 ApkAssets 的 loadFromPath 來加載 Framework 資源
        // 最終是通過 nativeLoad() native函式來完成
        apkAssets.add(ApkAssets.loadFromPath("/system/framework/frmework-res.apk", true /*system*/));
        // 省略一些資源的額外操作
        ......
        // 分別賦值給 sSystemApkAssetsSet 和 sSystemApkAssets 集合
        sSystemApkAssetsSet = new ArraySet<>(apkAssets);
        sSystemApkAssets = apkAssets.toArray(newApkAssets[apkAssets.size()]);
        // 創建系統專用的AssetManager實體
        // 最終呼叫的是 nativeCreate 函式
        sSystem = new AssetManager(true /*sentinel*/);
        // 設定資源到已加載集合,并賦值給mApkAssets變數
        sSystem.setApkAssets(sSystemApkAssets, false /*invalidateCaches*/);
    }

關于AssetManager的創建我們比較清晰了,我們會發現大多數的程序都是通過native呼叫來完成的,這也是Java層AssetManager的主要作用:擔當通過native層的橋梁

我們以ResourcesgetString()方法,來追蹤下呼叫程序:

    // Resources.java
    public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();
    }
    public CharSequence getText(@StringRes int id) throws NotFoundException {
        // 呼叫的AssetManager的getResourceText函式
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        ......
    }
    // AssetManager.java
    CharSequence getResourceText(@StringRes int resId) {
        synchronized (this) {
            // 通過 TypedValue 物件來接收轉換資料
            final TypedValue outValue = mValue;
            if (getResourceValue(resId, 0, outValue, true)) {
                return outValue.coerceToString();
            }
            return null;
        }
    }
    boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        Preconditions.checkNotNull(outValue, "outValue");
        synchronized (this) {
            //通過 nativeGetResourceValue 函式查找resId資源對應ApkAssets的cookie值
            //并把相關資料更新到TypedValue物件中
            //資源cookie值映射的是Java層的已加載資源mApkAssets集合的index值
            final int cookie = nativeGetResourceValue(
                    mObject, resId, (short) densityDpi, outValue, resolveRefs);
            if (cookie <= 0) {
                return false;
            }
            ......
            if (outValue.type == TypedValue.TYPE_STRING) {
                // 當數值型別判斷正確后
                // 通過ApkAssets物件的getStringFromPool讀取資料
                // 需要注意的是outValue.data
                // 它表示字串在對應ApkAssets物件中的mStringBlock陣列的index值
                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
            }
            return true;
        }
    }
    // ApkAssets.java
    CharSequence getStringFromPool(int idx) {
        synchronized (this) {
            // 從mStringBlock獲取字串資料
            return mStringBlock.get(idx);
        }
    }

呼叫程序很清晰了哈!

可以看出AssetManager的功能實作依賴了很多的native函式,接下來我們就看看Androidnative層是怎么設計的吧!

native層的AssetManager

從上面我們知道,native層的AssetManager才是真正裝載資源的部分,

Android 資源管理這部分是Google爸爸折騰最多的地方,對比9.05.0的原始碼真滴是天差地別啊

5.0的書籍已經用不上了,差別太大,,,,,不過,從上面Java層的知識我們已經知道:

  • 裝載資源(就是加載framwork-res.apk)的入口是ApkAssets.loadFromPath()
  • 最后將資源整合到體系中的函式是AssetManager.setApkAssets()

我們就先從這兩個函式入手看看

通過ApkAssets裝載資源

ApkAssets.loadFromPath呼叫的是ApkAssets的私有建構式:

// ApkAssets.java
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
    throws IOException {
    mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
    mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}

ApkAssets的私有建構式通過nativeLoad()本地呼叫來實作真正的資源加載

nativeLoad()函式呼叫示意:

// android_content_res_ApkAssets.cpp
static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system, jboolean force_shared_lib, jboolean overlay) {
    .......
    // 呼叫了 native層的 ApkAssets 的 Load 函式
    apk_assets = ApkAssets::Load(path.c_str(), system);
    ......
    return reinterpret_cast<jlong>(apk_assets.release());
}
// ApkAssets.cpp
std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system) {
    // 奇怪的C++智能指標
    // 真正進行加載的是 LoadImpl() 函式
    return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/);
}

nativeLoad()函式呼叫了nativeApkAssets類的LoadImpl函式,從這個函式開始,就開始對resources.arsc檔案進行操作了,所以,我們先配上一幅結構圖再一步一步的看函式實作

描述資源的resources.arsc檔案

圖片來源于大神LinJW博客,受益匪淺:
image

resources.arsc是以一個個Chunk塊的形式組織的,Chunk的頭部資訊記錄了這個Chunk的型別、長度等資料,從整體上來看,其結構為:資源索引表頭部+字串資源池+N個Package資料塊

簡單了解即可,讓我們從LoadImpl函式開始跟蹤吧

ApkAssets::LoadImpl()函式
std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(......) {
    // 一個用來指向解壓后的資源檔案的指標
    ::ZipArchiveHandle unmanaged_handle;
    // 解壓資源檔案
    ::OpenArchive(path.c_str(), &unmanaged_handle);
    // 將解壓后的資源檔案包裝成ApkAssets物件
    // Wrap the handle in a unique_ptr so it gets automatically closed.
    std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path));
    
    // resources.arsc 就是我們apk中的資源索引表
    ::ZipString entry_name("resources.arsc");
    ::ZipEntry entry;
    // 從解壓后的apk資源中查找 resources.arsc
    result = ::FindEntry(loaded_apk->zip_handle_.get(), entry_name, &entry);
    if (result != 0) {
        // There is no resources.arsc, so create an empty LoadedArsc and return.
        loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty();
        // 對于一個Java coder,std::move真的是一個SAO操作
        return std::move(loaded_apk);
    }
    // Open the resource table via mmap unless it is compressed.
    // This logic is taken care of by Open.
    // 通過Open函式加載ARSC檔案到resources_asset_,英文注釋也就是方法的執行邏輯了
    loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER);
    // Must retain ownership of the IDMAP Asset so that all pointers to its mmapped data remain valid.
    // 其實這行代碼主要是給loadOverlay情況用的,其他情況下都相當于賦值為null
    // loadOverlay中已經包含了一個idmap,所以在這里需要保留下來原來的
    loaded_apk->idmap_asset_ = std::move(idmap_asset);
    // 將resources_asset_(加載完成的ASRC資料)打包成StringPiece物件
    const StringPiece data(reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),loaded_apk->resources_asset_->getLength());
    // 呼叫 LoadedArsc 的 Load 函式決議轉化為資源資料,我們等下重點看下
    loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library);
    ......
    // Need to force a move for mingw32.
    return std::move(loaded_apk);
}

上面的代碼注釋比較詳細哈,不再贅述,接下來重點關注下LoadedArsc::Load()函式,

LoadedArsc::Load()函式決議resources.arsc
std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data,
                                                   const LoadedIdmap* loaded_idmap, bool system,
                                                   bool load_as_shared_library) {
  // Not using make_unique because the constructor is private.
  // 創建 LoadedArsc 實體
  std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
  loaded_arsc->system_ = system;
  // 遍歷StringPiece物件(也就是加載好的arsc)
  ChunkIterator iter(data.data(), data.size());
  while (iter.HasNext()) {
    const Chunk chunk = iter.Next();
    switch (chunk.type()) {
        case RES_TABLE_TYPE:
            // 如果資料型別是 RES_TABLE_TYPE 
            // 通過 LoadTable 將對應資源加載到 table 中
            if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) {
                return {};
            }
        break;
    }
  }
  return std::move(loaded_arsc);
}

從上面可以看到,資源的加載處理呼叫到了LoadTable()函式,我們來看下

LoadedArsc::LoadTable()函式決議resources.arsc
bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
                           bool load_as_shared_library) {
  const ResTable_header* header = chunk.header<ResTable_header>();
  ......
  ChunkIterator iter(chunk.data_ptr(), chunk.data_size());
  while (iter.HasNext()) {
    const Chunk child_chunk = iter.Next();
    switch (child_chunk.type()) {
      case RES_STRING_POOL_TYPE:
        // Only use the first string pool. Ignore others.
        // 如果是字符常量池型別的資源
        // 資料保存到global_string_pool_中,這是全域字串
        // Java層ApkAssets中的mStringBlock指向的就是這部分資料
        status_t err = global_string_pool_.setTo(child_chunk.header<ResStringPool_header>(), child_chunk.size());
        break;
      case RES_TABLE_PACKAGE_TYPE: {
        // 如果是pacakge型別的資源
        // 使用 LoadedPackage::Load() 進行加載
        ......
        std::unique_ptr<const LoadedPackage> loaded_package =
            LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library);
        packages_.push_back(std::move(loaded_package));
      } break;
    }
  }
  ......
  return true;
}

從上面的代碼看,字串資源放到了global_string_pool_中,對于RES_TABLE_PACKAGE_TYPE型別的資源通過LoadedPackage::Load()函式

LoadedPackage::Load()函式決議resources.arsc
std::unique_ptr<const LoadedPackage> LoadedPackage::Load(......) {
  // 創建LoadedPackage實體
  std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage());
  ......
  // 設定是否為系統資源
  loaded_package->system_ = system;
  // 根據packageID 判斷是否為共享資源
  loaded_package->package_id_ = dtohl(header->id);
  if (loaded_package->package_id_ == 0 ||
      (loaded_package->package_id_ == kAppPackageId && load_as_shared_library)) {
    // Package ID of 0 means this is a shared library.
    loaded_package->dynamic_ = true;
  }
  ......
  while (iter.HasNext()) {
    const Chunk child_chunk = iter.Next();
    switch (child_chunk.type()) {
        case RES_STRING_POOL_TYPE: 
            // 這里會決議兩種字串池型別
            // 一種是資源型別字串池,比如:ainim、attr、bool、color等
            // 另一種是資源項名稱字串池,比如:app_name、activity_main等
            ......
        case RES_TABLE_TYPE_SPEC_TYPE: 
            ......
        case RES_TABLE_TYPE_TYPE: 
            ......
        case RES_TABLE_LIBRARY_TYPE: 
            const ResTable_lib_entry* const entry_end = entry_begin + dtohl(lib->count);
            for (auto entry_iter = entry_begin; entry_iter != entry_end; ++entry_iter) {
                ......
                // 添加package_name和packageId到loaded_package的dynamic_package_map_中
                loaded_package->dynamic_package_map_.emplace_back(std::move(package_name),dtohl(entry_iter->packageId));
            }
            ......
    }
  }
  ......
  return std::move(loaded_package);
}

這部分函式的核心就是把chunk按型別進行決議處理,然后回傳決議完成的loaded_package指標,

到這里一個res.apk的裝載程序基本就完成了,程序中忽略了很多細節,不要著急哈,讓我們先搞清整體流程,

資源裝載的整體流程

讓我們從ApkAssets開始來梳理下流程:

  • 呼叫時序圖:
    image
    • 按照上圖的呼叫流程走完
      • LoadedArsc物件中的global_string_poolpackage相關的資源已經完成加載
      • ApkAssets物件中的resource_asset_loaded_arsc_也加載完成
  • 關系類圖:
    image

這樣,一個res.apk的裝載程序我們清楚了,但是AssetManager是怎么組織它的呢?

我們來看下AssetManager.setApkAssets()函式

通過AssetManager.setApkAssets()添加資源

我們先看AssetManager.setApkAssets()函式:

    public void setApkAssets(@NonNull ApkAssets[] apkAssets, boolean invalidateCaches) {
        ......
        ApkAssets[] newApkAssets = new ApkAssets[sSystemApkAssets.length + apkAssets.length];
        // Copy the system assets first.
        System.arraycopy(sSystemApkAssets, 0, newApkAssets, 0, sSystemApkAssets.length);
        // Copy the given ApkAssets if they are not already in the system list.
        int newLength = sSystemApkAssets.length;
        for (ApkAssets apkAsset : apkAssets) {
            if (!sSystemApkAssetsSet.contains(apkAsset)) {
                newApkAssets[newLength++] = apkAsset;
            }
        }
        ......
        mApkAssets = newApkAssets;
        nativeSetApkAssets(mObject, mApkAssets, invalidateCaches);
    }

Java部分比較簡單:

  • setApkAssets()會先把sSystemApkAssets中的資源拷貝到一個全新陣列newApkAssets
  • 然后檢查sSystemApkAssets沒有的資源,并添加到陣列newApkAssets
  • 更新已加載資源集合mApkAssets
  • 呼叫nativeSetApkAssets處理native層的資源

我們看下NativeSetApkAssets函式做了啥:

static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr,
                               jobjectArray apk_assets_array, jboolean invalidate_caches) {
    // 根據Java層傳遞的數值長度,創建對應的ApkAssets集合
    const jsize apk_assets_len = env->GetArrayLength(apk_assets_array);
    std::vector<const ApkAssets*> apk_assets;
    apk_assets.reserve(apk_assets_len);
    // 將 Java 層保存的ApkAssets指標取出并添加到集合中
    for (jsize i = 0; i < apk_assets_len; i++) {
        jobject obj = env->GetObjectArrayElement(apk_assets_array, i);
        jlong apk_assets_native_ptr = env->GetLongField(obj, gApkAssetsFields.native_ptr);
        apk_assets.push_back(reinterpret_cast<const ApkAssets*>(apk_assets_native_ptr));
    }
    ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
    // 呼叫 AssetManager2 的 SetApkAssets 函式
    assetmanager->SetApkAssets(apk_assets, invalidate_caches);
}

NativeSetApkAssets()函式對Java層的資料進行簡單處理后,呼叫了AssetManager2SetApkAssets函式,函式如下:

bool AssetManager2::SetApkAssets(const std::vector<const ApkAssets*>& apk_assets,
                                 bool invalidate_caches) {
  // 將ApkAssets賦值給apk_assets_
  apk_assets_ = apk_assets;
  // 構建動態資源參考表
  BuildDynamicRefTable();
  // 重新構建符合當前設備的資源,即 filtered_configs_
  RebuildFilterList();
  ......
  return true;
}

真的簡潔,,,,,,核心函式也很突出,為了方便理解接下來的BuildDynamicRefTable()函式,我們先來看AssetManager2類的相關定義

AssetManager2的關鍵定義
class AssetManager2 {
    //用來存盤該AssetManager2已經加載的所有APK包
    std::vector<const ApkAssets*> apk_assets_;
    /**
     * 用來將apk_assets_分組,主要還是用來處理Runtime Resources Overlay的
     */
    std::vector<PackageGroup> package_groups_;
    /**
     * 它的key表示APK包也就是ApkAssets的id
     * 它的value表示APK包也就是ApkAssets所在的PackageGroup在package_groups_中的索引
     */
    std::array<uint8_t, std::numeric_limits<uint8_t>::max() + 1> package_ids_;
    //表示設備當前的配置資訊
    ResTable_config configuration_;
    /**
     * 用來快取資源的Bag
     * 它的key表示一個資源的id,比如一個style,一個array
     * 它的value 表示已經從resources.arsc中決議出來了的,該資源的所有Bag
     */
    std::unordered_map<uint32_t, util::unique_cptr<ResolvedBag>> cached_bags_;
}
  // A collection of configurations and their associated ResTable_type that match the current
  // AssetManager configuration.
  struct FilteredConfigGroup {
    // 該ResTable_typeSpec中符合設備當前配置的所有的config
    std::vector<ResTable_config> configurations;
    // 該ResTable_typeSpec中符合設備當前配置的所有的ResTable_type
    std::vector<const ResTable_type*> types;
  };

  // Represents an single package.
  struct ConfiguredPackage {
    // A pointer to the immutable, loaded package info.
    const LoadedPackage* loaded_package_;
    // 我們在獲取資源的時候,要根據設備的當前配置資訊,去選擇最合適的資源項
    // 這個程序要經過match、isBetterThan、isMoreSpecificThan 等比較的程序
    // 現在為了加快獲取資源的速度,在加載完資源后,系統就會先選出匹配設備當前配置的資源存放在filtered_configs_中,
    // 當我們獲取資源的時候,就可以節省篩選步驟
    // filtered_configs_中的每一項代表一個ResTable_typeSpec中符合設備當前配置的所有ResTable_type
    ByteBucketArray<FilteredConfigGroup> filtered_configs_;
  };
  
  using ApkAssetsCookie = int32_t;
  // Represents a logical package, which can be made up of many individual packages. Each package
  // in a PackageGroup shares the same package name and package ID.
  // 大家留意上面的注釋
  struct PackageGroup {
    // 相同loaded_package name的集合,包括target package和overlay package
    // 如果一個Package沒有overlay package,那么它應該獨占一個PackageGroup
    std::vector<ConfiguredPackage> packages_;

    // The cookies associated with each package in the group. They share the same order as
    // packages_.
    // 表示 ConfiguredPackage 所代表的 ApkAssets 在 AssetManager2 的 apk_assets_ 集合中的位置
    // cookies_集合的存放順序與packages_的順序一一對應
    std::vector<ApkAssetsCookie> cookies_;

    // A library reference table that contains build-package ID to runtime-package ID mappings.
    // 一個用來描述資源共享庫的編譯時id和運行時id的映射關系表
    DynamicRefTable dynamic_ref_table;
  };

結合上面的定義,我們來看下BuildDynamicRefTable()干了啥

BuildDynamicRefTable()構建動態資源參考表
void AssetManager2::BuildDynamicRefTable() {
  // 簡單的初始化操作
  package_groups_.clear();
  package_ids_.fill(0xff);

  // 0x01 is reserved for the android package.
  int next_package_id = 0x02;
  const size_t apk_assets_count = apk_assets_.size();
  // 遍歷處理apk_assets_集合
  for (size_t i = 0; i < apk_assets_count; i++) {
    // 獲取已經決議加載完成的arsc資源,即loaded_arsc
    const LoadedArsc* loaded_arsc = apk_assets_[i]->GetLoadedArsc();
    // 從loaded_arsc中取出決議完成的package資源,即loaded_package
    for (const std::unique_ptr<const LoadedPackage>& package : loaded_arsc->GetPackages()) {
      // Get the package ID or assign one if a shared library.
      int package_id;
      if (package->IsDynamic()) {
        // 如果是共享資源,分配一個特殊的package id
        package_id = next_package_id++;
      } else {
        // 不是共享資源,使用原有的package id
        package_id = package->GetPackageId();
      }
      // Add the mapping for package ID to index if not present.
      uint8_t idx = package_ids_[package_id];
      if (idx == 0xff) {
        // 0xff 說明package_ids還未記錄
        package_ids_[package_id] = idx = static_cast<uint8_t>(package_groups_.size());
        // 先向package_groups_的尾部添加一個新的PackageGroup物件
        package_groups_.push_back({});
        // 然后獲取物件動態資源參考表的指標
        DynamicRefTable& ref_table = package_groups_.back().dynamic_ref_table;
        // 賦值 package_id,干啥用的后面看看再說
        ref_table.mAssignedPackageId = package_id;
        // 設定當前是否為共享資源,常量值 0x7F 值得研究研究
        ref_table.mAppAsLib = package->IsDynamic() && package->GetPackageId() == 0x7f;
      }
      // 取出PackageGroup物件,開始進行資料填充
      PackageGroup* package_group = &package_groups_[idx];

      // Add the package and to the set of packages with the same ID.
      // 設定 packages_ 和 cookies_
      package_group->packages_.push_back(ConfiguredPackage{package.get(), {}});
      // 留意下這個變數i,其實就是當前ApkAsset在apk_assets_中的陣列下標
      package_group->cookies_.push_back(static_cast<ApkAssetsCookie>(i));
      // Add the package name -> build time ID mappings.
      for (const DynamicPackageEntry& entry : package->GetDynamicPackageMap()) {
        // 決議loaded_package中的 dynamic_package_map_ 一個packageName:packageID格式的集合
        String16 package_name(entry.package_name.c_str(), entry.package_name.size());
        // 并設定到 package_group 中的動態資源參考表中
        package_group->dynamic_ref_table.mEntries.replaceValueFor(
            package_name, static_cast<uint8_t>(entry.package_id));
      }
    }
  }
  // 到這里,ApkAssets集合中的資料就被決議的差不多了
  // package_groups_和package_ids_集合中的資料也被填充的差不多了
  // Now assign the runtime IDs so that we have a build-time to runtime ID map.
  // 接下來的這部分是將資源的 Build ID 與 runtime ID 關聯起來
  // 還記得前面生成的 mAssignedPackageId 么,在這里就起到作用了
  const auto package_groups_end = package_groups_.end();
  for (auto iter = package_groups_.begin(); iter != package_groups_end; ++iter) {
    const std::string& package_name = iter->packages_[0].loaded_package_->GetPackageName();
    for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) {
        // 跟蹤addMapping函式就會發現,這部分的操作是:
        // 根據 package_name 查找的 build packageID
        // 然后把 build packageID 作為索引,關聯runtimeID,也就是 mAssignedPackageId
      iter2->dynamic_ref_table.addMapping(package_name, iter->dynamic_ref_table.mAssignedPackageId);
    }
  }
}

BuildDynamicRefTable()函式基本上把AssetManager2定義的關鍵資料都填充完了,整體流程注釋的比較詳細,不再補充啦

這里還是要吐槽一下,Android資源相關的struct嵌套有點多,短時間理解起來還是有難度的,好在資源加載部分的業務邏輯應該很少遇到改動需求,慶幸一下下先

到這里整個framework資源的加載流程就差不多完成了,我們簡單總結下

AssetManager加載程序的簡單總結

資源管理部分還有比較重要的一個知識點是RRO,就不在這里介紹了,AssetManager2中的很多資料定義都是為了RRO而設計的,需要的話可以先從Google官網-RRO了解先

我們還是先通過呼叫時序圖來看下資源加載程序,從AssetManager.getSystem()開始,流程如下:

image

資源加載完后的整體結構,如下圖:

image

看完這兩張圖后思路有木有更清晰了呢,哈哈哈

AssetManager的資源查找

了解了AssetManager的資源加載程序,查找程序就比較容易了,AssetManager的資源查找入口可以簡單分為兩種:

  • open方式:像open()openNonAsset()
  • getResource方式:像getResourceText()getResourceArray()

我們來簡單看下

open方式

對于open類的介面,基本上處理的都是一些xml或者檔案形式的資料,從函式呼叫來看,最后都走到了native層的OpenNonAsset函式,這部分處理比較簡單,原始碼如下:

std::unique_ptr<Asset> AssetManager2::OpenNonAsset(const std::string& filename,
                                                   Asset::AccessMode mode,
                                                   ApkAssetsCookie* out_cookie) const {
  for (int32_t i = apk_assets_.size() - 1; i >= 0; i--) {
    std::unique_ptr<Asset> asset = apk_assets_[i]->Open(filename, mode);
    if (asset) {
      if (out_cookie != nullptr) {
        *out_cookie = i;
      }
      return asset;
    }
  }
  ......
}
std::unique_ptr<Asset> AssetManager2::OpenNonAsset(const std::string& filename,
                                                   ApkAssetsCookie cookie,
                                                   Asset::AccessMode mode) const {
    ......
    // 從這里就可以看出 ApkAssetsCookie 只是一個下標索引
    // 如果之前查找過對應的資源,就記錄下來對應的陣列下標
    // 下次獲取時就不需要像上面一樣用for回圈去查找了
    return apk_assets_[cookie]->Open(filename, mode);
}

函式根據cookie需求做了一個簡單的多載,但是最終都是呼叫到了ApkAssetsOpen()函式,

這個函式我們在資源加載時遇到過,作用就是加載資源,如果是壓縮資料會進行相應的解壓處理,

這里面還涉及到了Asset類,大家感興趣可以深入了解下

getResource方式

跟蹤getResource型別的函式呼叫,我們最終找到的native層介面是AssetManager2::FindEntry(),簡要流程如下:

ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
                                         bool /*stop_at_first_match*/,
                                         FindEntryResult* out_entry) const {
  ......
  // 根據resid決議出對應的pacakge資訊 
  const uint32_t package_id = get_package_id(resid);
  const uint8_t type_idx = get_type_id(resid) - 1;
  const uint16_t entry_idx = get_entry_id(resid);
  const uint8_t package_idx = package_ids_[package_id];
  if (package_idx == 0xff) {
    // 資源對應的package為空,回傳
    LOG(ERROR) << base::StringPrintf("No package ID %02x found for ID 0x%08x.", package_id, resid);
    return kInvalidCookie;
  }
  // 查找到資源對應的 PackageGroup 物件,PackageGroup 物件中包含著一個一個的loaded_pacakge
  const PackageGroup& package_group = package_groups_[package_idx];
  // 記錄 PackageGroup 物件中的loaded_package_數量
  const size_t package_count = package_group.packages_.size();
  ......
  // 開始從資源對應的 PackageGroup 中查找最合適的資源
  for (size_t pi = 0; pi < package_count; pi++) {
    ......
    // If desired_config is the same as the set configuration, then we can use our filtered list
    // and we don't need to match the configurations, since they already matched.
    // 如果獲取資源時的配置資訊和資源load時的配置資訊一致的話,可以使用快速查找邏輯
    const bool use_fast_path = desired_config == &configuration_;
    // 獲取最適合當前配置的資源集合
    const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx];
    if (use_fast_path) {
      // 快速查找,直接使用filtered_configs_中的配置資源即可
    } else {
      // This is the slower path, which doesn't use the filtered list of configurations.
      // Here we must read the ResTable_config from the mmapped APK, convert it to host endianness
      // and fill in any new fields that did not exist when the APK was compiled.
      // Furthermore when selecting configurations we can't just record the pointer to the
      // ResTable_config, we must copy it.
      // 需要查找資源對應的所有型別,從中找出匹配的資源,程序中還會涉及到記憶體拷貝,比較緩慢
      ......
    }
  }
  ......
}

原函式還有很多細節沒有展示,不過整體流程已經梳理出來了,再詳細的部分就涉及到ARSC中的資料結構了,為了避免篇幅過大就先到這里啦!

感覺要想真正搞懂資源管理這部分還是要去熟悉arsc檔案的格式,以及加載后的記憶體模型

學到這里算是找到了進入Android資源管理世界入口和鑰匙了,后面再來深入學習研究吧(挖坑ING…)

結語

對于Android資源管理這部分的學習來說,進度比以往慢了很多,書中5.0的資源管理與當前系統差異著實不小,少了一個好的向導,學習起來迷茫了一些,好在對比著原始碼磕磕絆絆的理清了,

總結下來,還只是剛剛入門,像資源適配RRO等知識還未詳細涉及,但為了不拖延大局,只能先到此為止了,等完成Android系統的整體學習后再來補充吧

先附上參考鏈接以供分享學習之用:

  • 大神博客-resources.arsc相關知識
  • 大神博客-Android 資源管理框架
  • Android官網-RRO
  • Android官網-螢屏兼容性

下一篇學習SystemServer行程

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/237679.html

標籤:其他

上一篇:vue 配置axios 封裝介面

下一篇:多層RecyclerView進行嵌套的一個開源框架

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more