Android APK資源加載流程

概述

我們在Activity中訪問資源(圖片,字符串,顏色等)是非常方便的,只需要getResources()獲取一個Resources對象,然後就可以訪問各種資源了,那這些資源到底是怎麼被加載的呢?下面我們就分析一下資源加載機制

App啓動流程

首先我們回顧一下App啓動流程,還不瞭解的可以看我之前寫的這篇文章

  • 首先是點擊App圖標,此時是運行在Launcher進程,通過ActivityManagerServiceBinder IPC的形式向system_server進程發起startActivity的請求
  • system_server進程接收到請求後,通過Process.start方法向zygote進程發送創建進程的請求
  • zygote進程fork出新的子進程,即App進程
  • 然後進入ActivityThread.main方法中,這時運行在App進程中,通過ActivityManagerServiceBinder IPC的形式向system_server進程發起attachApplication請求
  • system_server接收到請求後,進行一些列準備工作後,再通過Binder IPC向App進程發送scheduleLaunchActivity請求
  • App進程binder線程(ApplicationThread)收到請求後,通過Handler向主線程發送LAUNCH_ACTIVITY消息
  • 主線程收到Message後,通過反射機制創建目標Activity,並回調ActivityonCreate

首先我們看第四步,attachApplication方法,最終會調用thread#bindApplication然後調用ActivityThread#handleBindApplication方法,我們從這個方法開始看

ActivityThread#handleBindApplication


private void handleBindApplication(AppBindData data) {
    ...
    final InstrumentationInfo ii;
    ...
    // 創建 mInstrumentation 實例
    if (ii != null) {
        final ApplicationInfo instrApp = new ApplicationInfo();
        ii.copyTo(instrApp);
        instrApp.initForUser(UserHandle.myUserId());
        final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
                appContext.getClassLoader(), false, true, false);
        final ContextImpl instrContext = ContextImpl.createAppContext(this, pi);

        try {
            final ClassLoader cl = instrContext.getClassLoader();
            mInstrumentation = (Instrumentation)
                cl.loadClass(data.instrumentationName.getClassName()).newInstance();
        } catch (Exception e) {
            ...
        }
        ...
    } else {
        mInstrumentation = new Instrumentation();
    }
    ...
    Application app;
    ...
    // 創建 Application 實例
    try {
        ...
        app = data.info.makeApplication(data.restrictedBackupMode, null);
        mInitialApplication = app;
        ...
        try {
            mInstrumentation.callApplicationOnCreate(app);
        } catch (Exception e) {
            ...
        }
    } finally {
        ...
    }
    ...
}



// http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/LoadedApk.java#959
public Application makeApplication(boolean forceDefaultAppClass,
        Instrumentation instrumentation) {
    ...

    try {
        ...
        //註釋1
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        appContext.setOuterContext(app);
    } catch (Exception e) {
        ...
    }
    ...
    return app;
}

 static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        return new ContextImpl(null, mainThread,
                packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
    }

這個方法我們只留下了最核心的內容,我們看下注釋1, ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);這個方法會直接new一個新的ContextImpl


    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
          ....
        //LoadApk賦值
        mPackageInfo = packageInfo;
        mResourcesManager = ResourcesManager.getInstance();

       ...
        //通過LoadApk.getResources獲取Resources對象
        Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {

                if (container != null) {
                    // This is a nested Context, so it can't be a base Activity context.
                    // Just create a regular Resources object associated with the Activity.
                    resources = mResourcesManager.getResources(
                            activityToken,
                            packageInfo.getResDir(),
                            packageInfo.getSplitResDirs(),
                            packageInfo.getOverlayDirs(),
                            packageInfo.getApplicationInfo().sharedLibraryFiles,
                            displayId,
                            overrideConfiguration,
                            compatInfo,
                            packageInfo.getClassLoader());
                } else {
                    // This is not a nested Context, so it must be the root Activity context.
                    // All other nested Contexts will inherit the configuration set here.
                    resources = mResourcesManager.createBaseActivityResources(
                            activityToken,
                            packageInfo.getResDir(),
                            packageInfo.getSplitResDirs(),
                            packageInfo.getOverlayDirs(),
                            packageInfo.getApplicationInfo().sharedLibraryFiles,
                            displayId,
                            overrideConfiguration,
                            compatInfo,
                            packageInfo.getClassLoader());
                }
            }
        }
        //爲mResources變量賦值
        mResources = resources;

       ...
    }

packageInfo.getResources,packageInfoLoadApk類型的,我們看下這個方法

LoadApk#getResources

 public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
        }
        return mResources;
    }

其中調用了ActivityThreadgetTopLevelResources方法,我們繼續看一下

ActivityThread#getTopLevelResources

 Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
            String[] libDirs, int displayId, LoadedApk pkgInfo) {
        return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
                displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
    }
    

繼續調用了mResourcesManagergetResources方法,我麼繼續跟下去

ResourcesManager#getResources

  public @NonNull Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
    

  private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        ...

        // 創建ResourcesImpl
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        ....

            final Resources resources;
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
            }
            return resources;
        }
    }
    

 private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }
    

  protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        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 (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
            }
        }

        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    throw new Resources.NotFoundException(
                            "failed to add split asset path " + splitResDir);
                }
            }
        }

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

        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return assets;
    }
    
    
     private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
            @NonNull ResourcesImpl impl) {
        // Find an existing Resources that has this ResourcesImpl set.
        final int refCount = mResourceReferences.size();
        for (int i = 0; i < refCount; i++) {
            WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
            Resources resources = weakResourceRef.get();
            if (resources != null &&
                    Objects.equals(resources.getClassLoader(), classLoader) &&
                    resources.getImpl() == impl) {
                if (DEBUG) {
                    Slog.d(TAG, "- using existing ref=" + resources);
                }
                return resources;
            }
        }

        // Create a new Resources reference and use the existing ResourcesImpl object.
        Resources resources = new Resources(classLoader);
        resources.setImpl(impl);
        mResourceReferences.add(new WeakReference<>(resources));
        if (DEBUG) {
            Slog.d(TAG, "- creating new ref=" + resources);
            Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
        }
        return resources;
    }

首先調用createResourcesImpl,創建ResourcesImpl,我們看下這個方法內部創建了AssetManager assets = new AssetManager();,然後調用assets.addAssetPath添加資源地址,最後返回final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);,最後查看是否有緩存,如果有則返回緩存的resources,如果沒有就重新構建Resources,然後返回

平時使用

 Resources resources = getResources();
 resources.getString();
 resources.getAssets();
 resources.getColor();
 resources.getDrawable()

這個就是我們平時使用的代碼,通過resources獲取資源,其中getResources方法返回的就是上方ContextImpl創建的mResources變量,然後我們分析一下getString方法的實現

## Resources.java

 public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();
    }
    
  @NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                + Integer.toHexString(id));
    }
    

最後交給了mResourcesImpl.getAssets().getResourceText(id);,我們繼續看下這個方法

## ResourcesImpl.java

public AssetManager getAssets() {
        return mAssets;
    }
    
 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變量處理,mAssets變量就是創建ResourcesImpl時傳入的AssetsManager,其實最後就是委託給了AssetsManager去處理

總結

我們發現Apk的資源是通過AssetManager.addAssetPath方法來完成加載,那麼我們就可以通過反射構建自己的AssetManager對象,然後把調用addAssetPath加載自己的資源,然後把自己構建的AssetManager通過反射設置給mAssets變量,這樣下次加載資源就是用的我們AssetManager,也就是用的我們更換後的資源,這個就是熱修復的資源修復的原理

參考:http://www.apkbus.com/blog-822415-78080.html

https://blog.csdn.net/singwhatiwanna/article/details/24532419

發佈了100 篇原創文章 · 獲贊 5 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章