AndroidQ RRO(Runtime Resource Overlay)机制(4)

概述

上篇文章说到目标应用的Overlay包路径被更新到了目标应用ApplicationInfo之后,就会将更新之后的ApplicationInfo传给APP进程,本篇继续来看APP进程的处理。

ApplicationThread.scheduleApplicationInfoChanged

      public void scheduleApplicationInfoChanged(ApplicationInfo ai) {
            mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai);
            sendMessage(H.APPLICATION_INFO_CHANGED, ai);
        }

ActivityThread.handleApplicationInfoChanged

    @VisibleForTesting(visibility = PACKAGE)
    public void handleApplicationInfoChanged(@NonNull final ApplicationInfo ai) {
        //LoadedApk用来描述APP进程中一个package的详细信息
        LoadedApk apk;
        LoadedApk resApk;
        
        //一个APP的LoadedApk通常是放在mPackages中,mResourcePackages通常存放为其他资源包应用创建的LoadedApk
        synchronized (mResourcesManager) {
            WeakReference<LoadedApk> ref = mPackages.get(ai.packageName);
            apk = ref != null ? ref.get() : null;
            ref = mResourcePackages.get(ai.packageName);
            resApk = ref != null ? ref.get() : null;
        }

        final String[] oldResDirs = new String[2];
        
        if (apk != null) {
            oldResDirs[0] = apk.getResDir();
            final ArrayList<String> oldPaths = new ArrayList<>();
            LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths);
            //将AMS传过来的,已经更新了的ApplicationInfo再更新到APP进程的LoadedApk
            apk.updateApplicationInfo(ai, oldPaths);
        }
         if (resApk != null) {
            oldResDirs[1] = resApk.getResDir();
            final ArrayList<String> oldPaths = new ArrayList<>();
            LoadedApk.makePaths(this, resApk.getApplicationInfo(), oldPaths);
            resApk.updateApplicationInfo(ai, oldPaths);
        }

        synchronized (mResourcesManager) {
            // Update all affected Resources objects to use new ResourcesImpl
            mResourcesManager.applyNewResourceDirsLocked(ai, oldResDirs);
        }

        ApplicationPackageManager.configurationChanged();

        Configuration newConfig = new Configuration();
        newConfig.assetsSeq = (mConfiguration != null ? mConfiguration.assetsSeq : 0) + 1;
        //回调onConfigurationChanged
        handleConfigurationChanged(newConfig, null);

        // 重启Activity使得Overlay资源生效,这里的重启不是整个应用被杀掉那种重启,preserveWindows代表是否保留窗口
        relaunchAllActivities(true /* preserveWindows */);
    }

APP这边拿到新的ApplicationInfo之后会将其更新到自己的LoadedApk中,之后会回调onConfigurationChanged方法,最后是重启所有Activity,使Overlay资源能够生效,我们看到这里只是针对Activity可以重启生效,但如果是对SystemUI这种只有window的应用是不行的,那SystemUI是怎么生效的呢? 依靠的就是onConfigurationChanged的回调,其回调入口就在SystemUIApplication中,接着重点看下ApplicationInfo的更新过程:

LoadedApk.updateApplicationInfo

public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
            @Nullable List<String> oldPaths) {
        //将ApplicationInfo保存到LoadedApk,主要是更新了mOverlayDirs
        //mOverlayDirs = aInfo.resourceDirs
        setApplicationInfo(aInfo);
        
        ......
        
        synchronized (this) {
                ......
                //重建目标应用的ResourcesManager
                mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                        splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                        Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                        getClassLoader());
            }
        }
        ..
    }

只关注上面代码两部分,一部分是将新的ApplicationInfo更新到LoadedApk,第二部分就是新ApplicationInfo中的数据然后重建ResourcesManager,对于RRO来说主要更新就是mOverlayDirs这个变量,它代表目标应用的Overlay包的路径,详见上一篇:

private void setApplicationInfo(ApplicationInfo aInfo) {
        ......
        mOverlayDirs = aInfo.resourceDirs;
       ......
    }

接着我们需要看的是ResourcesManager的重建:

ResourcesManager.getResources

public @Nullable 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);
        }
    }

上面的变量很多,我们关心的就两个,resDir代表目标应用的安装路径,overlayDirs代表Overlay资源包的安装路径,可以有多个Overlay,ResourcesKey用来保存这些信息。

ResourcesManager.getOrCreateResources

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {
            //activityToken为空
            if (activityToken != null) {
               ....
            } else {
               
                // 从缓存中去拿ResourcesImpl,这里面有条件的,并不是说缓存中有就一定会取,还会判断当前的ResourcesImpl
                //是不是最新的
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }

                
            }

            // 创建新的ResourcesImpl
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }

            // 缓存下来
            mResourceImpls.put(key, new WeakReference<>(resourcesImpl));

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

这里直接来看ResourcesImpl的创建流程:

ResourcesManager.createResourcesImpl

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
         ....

        final AssetManager assets = createAssetManager(key);
        
        ....
        
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }

这里我们只关注AssetManager的创建:

ResourcesManager.createAssetManager

 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        final AssetManager.Builder builder = new AssetManager.Builder();

        //应用自身的资源包
        if (key.mResDir != null) {
            try {
                builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
                        false /*overlay*/));
            } catch (IOException e) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }
        //拆分应用的资源包
        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                try {
                    builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
                            false /*overlay*/));
                } catch (IOException e) {
                    Log.e(TAG, "failed to add split asset path " + splitResDir);
                    return null;
                }
            }
        }
        //Overlay资源包
       if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                try {
                    builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
                            true /*overlay*/));
                } catch (IOException e) {
                    Log.w(TAG, "failed to add overlay path " + idmapPath);

                    // continue.
                }
            }
        }
        //共享资源包
        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.
                    try {
                        builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
                                false /*overlay*/));
                    } catch (IOException e) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");

                        // continue.
                    }
                }
            }
        }

        return builder.build();
    }

每个应用在创建createAssetManager时都会加载四种类型的资源包(如果有的话),第一个是应用自身,第二个是拆分的资源包(Android5.0之后,支持将一个应用拆分为多个包),第三个是Overlay资源包,第四个是共享资源包(其他应用共享给当前应用的资源,这个和Overlay有什么区别呢?主要区别就是共享资源包需要目标应用自己在代码中引入,并且明确表明需要使用,如引用某个共享资源包的资源需要加其包名前缀)。

上面四种资源包的加载流程都一样的,我们以Overlay包为例,loadApkAssets根据资源包的路径构造ApkAssets对象,并通过addApkAssets添加到AssetManager中:

ResourcesManager.loadApkAssets

private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
            throws IOException {
        final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
        ApkAssets apkAssets = null;
        //省略缓存相关的代码
        .....
        // We must load this from disk.
        if (overlay) {
            apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
                    false /*system*/);
        } else {
            apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
        }

        if (mLoadedApkAssets != null) {
            mLoadedApkAssets.put(newKey, apkAssets);
        }

        mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
        return apkAssets;
    }

loadApkAssets三个参数分别为资源包的安装路径,是否共享,是否为Overlay包,我们这里主要看的是目标应用对Overlay包的加载,所以后面都沿着Overlay流程去分析,overlayPathToIdmapPath这个方法相当重要,它会将Overlay包的安装路径进行转换:

private static String overlayPathToIdmapPath(String path) {
        return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap";
    }

比如我们Overlay包安装路径为"product/overlay/RROResource/RROResource.apk",转换之后的路径就为"/data/resource-cache/product@overlay@RROResource@RROResource.apk@idmap",这是个什么路径呢?前面
AndroidQ RRO(Runtime Resource Overlay)机制(2)
说过,这个路径就是Overlay包的Idmap文件的生成路径,这个文件中包含了目标应用与Overlay应用相同资源名称间的映射关系,通过命令adb shell idmap2 dump --idmap-path [file]可以查看:

[target res id] - > [overlay res id] [resource name]
0x01040151 -> 0x01050001 string/config_dozeComponent
0x01040152 -> 0x01050002 string/config_dozeDoubleTapSensorType
0x01040153 -> 0x01050003 string/config_dozeLongPressSensorType

继续看加载:

ApkAssets.loadOverlayFromPath

   public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
            throws IOException {
        return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
    }
     private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
            throws IOException {
        Preconditions.checkNotNull(path, "path");
        mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
        mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
    }

可以发现最终的加载都在native层实现,Java层ApkAssets会保存native层ApkAssets的引用,参数system表示是否为framework-res.apk,显然这里为false。

ApkAssets.nativeLoad

static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system,
                        jboolean force_shared_lib, jboolean overlay) {
  ScopedUtfChars path(env, java_path);
  if (path.c_str() == nullptr) {
    return 0;
  }
  ..
  std::unique_ptr<const ApkAssets> apk_assets;
  
  //不同类型的资源包有不同的加载方式
  if (overlay) {
    apk_assets = ApkAssets::LoadOverlay(path.c_str(), system);
  } else if (force_shared_lib) {
    apk_assets = ApkAssets::LoadAsSharedLibrary(path.c_str(), system);
  } else {
    apk_assets = ApkAssets::Load(path.c_str(), system);
  }
   ...
  return reinterpret_cast<jlong>(apk_assets.release());
}

我们关注的是Overlay:

ApkAssets::LoadOverlay

std::unique_ptr<const ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path,
                                                        bool system) {
  std::unique_ptr<Asset> idmap_asset = CreateAssetFromFile(idmap_path);
  if (idmap_asset == nullptr) {
    return {};
  }

  const StringPiece idmap_data(
      reinterpret_cast<const char*>(idmap_asset->getBuffer(true /*wordAligned*/)),
      static_cast<size_t>(idmap_asset->getLength()));
  std::unique_ptr<const LoadedIdmap> loaded_idmap = LoadedIdmap::Load(idmap_data);
  if (loaded_idmap == nullptr) {
    LOG(ERROR) << "failed to load IDMAP " << idmap_path;
    return {};
  }
  return LoadImpl({} /*fd*/, loaded_idmap->OverlayApkPath(), std::move(idmap_asset),
                  std::move(loaded_idmap), system, false /*load_as_shared_library*/);
}

这个方法里面全是对文件流的操作,CreateAssetFromFile会打开idmap_path,构造Assetidmap_data代表从Idmap文件读取到的数据,这些数据又被构造成LoadedIdmap对象,loaded_idmap->OverlayApkPath返回的是Overlay包的安装路径。

ApkAssets::LoadImpl

std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(
    unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset,
    std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library) {
  ::ZipArchiveHandle unmanaged_handle;
  int32_t result;
  //打开Overlay apk压缩包
  if (fd >= 0) {
    result =
        ::OpenArchiveFd(fd.release(), path.c_str(), &unmanaged_handle, true /*assume_ownership*/);
  } else {
    result = ::OpenArchive(path.c_str(), &unmanaged_handle);
  }

   .....
  //构造native层ApkAssets
  std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time));

  // kResourcesArsc = "resources.arsc"
  ::ZipString entry_name(kResourcesArsc.c_str());
  ::ZipEntry entry;
  //找到"resources.arsc"
  result = ::FindEntry(loaded_apk->zip_handle_.get(), entry_name, &entry);
  
     .....
  
  // 打开"resources.arsc"
  loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER);
   
   ...

  loaded_apk->idmap_asset_ = std::move(idmap_asset);

  const StringPiece data(
      reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),
      loaded_apk->resources_asset_->getLength());
      
  //加载"resources.arsc"的数据
  loaded_apk->loaded_arsc_ =
      LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library);
  
  .....

  return std::move(loaded_apk);
}

LoadImpl会通过OpenArchiveFd解压缩Overlay包(压缩算法为zip),接着构造native层ApkAssets对象,然后通过FindEntry找到Overlay包的resources.arsc,最后打开resources.arsc并读取其中的数据。

resources.arsc是一个二进制文件,是由Android的aapt打包工具生成的,它里面包含了当前应用的资源数据,其结构非常复杂,要理解resources.arsc首先必须熟悉其中的数据结构,关于resources.arsc解析不在本篇讨论范围中。

resources.arsc的加载由LoadedArsc来完成,我们来简单看看它是如何实现的:

LoadedArsc::Load

std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data,
                                                   const LoadedIdmap* loaded_idmap, bool system,
                                                   bool load_as_shared_library) {
 

  std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
  loaded_arsc->system_ = system;

  ChunkIterator iter(data.data(), data.size());
  while (iter.HasNext()) {
    const Chunk chunk = iter.Next();
    //根据Chunk类型解析
    switch (chunk.type()) {
      case RES_TABLE_TYPE:
        if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) {
          return {};
        }
        break;

      default:
        LOG(WARNING) << StringPrintf("Unknown chunk type '%02x'.", chunk.type());
        break;
    }
  }
  ....
  
  return std::move(loaded_arsc);
}

一个resources.arsc文件就是类型为RES_TABLE_TYPE的Chunk,所以核心代码在LoadTable中:

LoadedArsc::LoadTable

bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
                           bool load_as_shared_library) {
  
  ....

  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:
       
        if (global_string_pool_.getError() == NO_INIT) {
          status_t err = global_string_pool_.setTo(child_chunk.header<ResStringPool_header>(),
                                                   child_chunk.size());
       }
         ....
        break;

      case RES_TABLE_PACKAGE_TYPE: {
        ....

        std::unique_ptr<const LoadedPackage> loaded_package =
            LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library);
        if (!loaded_package) {
          return false;
        }
        packages_.push_back(std::move(loaded_package));
      } break;

      default:
        LOG(WARNING) << StringPrintf("Unknown chunk type '%02x'.", chunk.type());
        break;
    }
  } 
  ....
  return true;
}

resources.arsc文件中存放数据的Chunk大的类型分为两种,类型为RES_STRING_POOL_TYPE的全局字符串池,类型为RES_TABLE_PACKAGE_TYPE的包相关信息,RES_TABLE_PACKAGE_TYPE里面还有子Chunk,所以RES_TABLE_PACKAGE_TYPE还需要通过LoadedPackage进一步解析,LoadedPackageLoad函数同样又对Chunk分类型解析,它的内部的Chunk类型比较多,解析更为复杂,由于resources.arsc解析很复杂且不是本篇重点,感兴趣的可以自己去看看代码。

到此我们就知道了Overlay资源包的加载其核心就是解析apk的resources.arsc文件,将其中的资源数据加载进内存,方法目标应用获取。不仅是Overlay包的加载是这样,其他资源包的加载也是同样的流程。

对APP层的处理我们主要关注的就是这部分重建AssetManager的逻辑,接着回到ActivityThreadhandleApplicationInfoChanged方法中,资源包加载之后为了让Overlay的资源能够生效就会relaunchAllActivities,这里的重启Activity为了给用户好的体验会保留窗口的重启,重启之后应用加载资源时,Android资源管理框架就会去Overlay包中加载那些已经被Overlay的资源到达换肤目的。

最后附上一张流程图:
在这里插入图片描述资源管理框架的分析推荐博客:资源管理框架

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值