android 皮肤包换肤之Resources加载

Posted 史大拿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 皮肤包换肤之Resources加载相关的知识,希望对你有一定的参考价值。

android 换肤之资源(Resources)加载(一)

本系列计划3篇:

  1. Android 换肤之资源(Resources)加载(一) — 本篇
  2. setContentView() / LayoutInflater源码分析(二)
  3. 换肤框架搭建(三)

看完本篇你可以学会什么?

  1. Resources在什么时候被解析并加载的

    1. Application#Resources
    2. Activity#Resources
  2. drawable 如何加载出来的

  3. 创建自己的Resources加载自己的资源

  4. 制作皮肤包"皮肤包"

  5. 加载“皮肤包”中的资源

tips:源码基于android-30

阅读源码后本篇实现的效果:

效果很简单,2个按钮

  • 换肤
  • 还原

效果很简单,重点是换肤的时候是加载“皮肤包”中的资源

Resources在什么时候被解析并加载的

Application#Resources

众所周知,java程序都是由main方法开始的,所以我们就从ActivityThread#main()方法开始阅读源码

在ActivityThread#main()方法中,我们经常会说到一些关于Looper,handler的逻辑代码,本篇不展开说Looper

 #ActivityThread.java
 public static void main(String[] args) 
    ....// looper
     Looper.prepareMainLooper();// szj 创建 activityThread
     ActivityThread thread = new ActivityThread();
     thread.attach(false, startSeq);.....
     Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");
 

本篇重点不是Looper, 来看看 thread.attach(false, startSeq); 方法

 #ActivityThread.java
 private void attach(boolean system, long startSeq) 
    if (!system) 
      ...
    else 
      try 
        // 很关键的一个类,用来分发activity生命周期
        mInstrumentation = new Instrumentation();
        mInstrumentation.basicInit(this);// szj 创建Application Context
        ContextImpl context = ContextImpl.createAppContext(
          this, getSystemContext().mPackageInfo);// szj 反射创建 application
        mInitialApplication = context.mPackageInfo.makeApplication(true, null);// 执行application的onCreate() 方法
        mInitialApplication.onCreate();
       catch (Exception e) 
        throw new RuntimeException(
          "Unable to instantiate Application():" + e.toString(), e);
      
    
 
  • 通过ContextImpl.createAppContext() 创建Context
  • 通过反射创建application
  • 创建好application后会调用 Application#onCreate()方法

接着执行ContextImpl.createAppContext()

最终会走到LoadedApk#getResources()

然后会从LoadedApk#getResources() 执行到 ResourcesManager#getResources()

最终在ResourcesManager中创建Resources

这段源码我们知道:

  • 在程序运行到main方法的时候,我们会在ActivtyThread.#attach()中创建Context,创建Application,并且执行Application#onCreate()

  • 然后会执行到LoadedApk.getResources() 去解析获取Resources()

    • LoadedApk.java 从类名我们就知道这个类是用来对apk信息解析的
  • 最终解析Resources的任务交给了 ResourcesManager#createResources()

好了,读到这里就可以了,来看看Activity#Resources是如何解析并加载的


Activity#Resources

源码分析从 ActivityThread#performLaunchActivity()开始

为什么要从这里开始? 写完换肤之后开始framework系列,到时候具体聊~

 #ActivityThread.java
 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) 
    .... 省略部分代码
 ​
     // szj 创建 activity 的上下文
     ContextImpl appContext = createBaseContextForActivity(r);
     Activity activity = null;
     try 
         java.lang.ClassLoader cl = appContext.getClassLoader();
         // 通过反射创建 activity 的实例
         activity = mInstrumentation.newActivity(
                 cl, component.getClassName(), r.intent);
        
        
     catch (Exception e) 
        .....
    try 
         if (activity != null) // szj 创建 PhoneWindow,设置windowManager等操作
             activity.attach(appContext, this, getInstrumentation(), r.token,
                     r.ident, app, r.intent, r.activityInfo, title, r.parent,
                     r.embeddedID, r.lastNonConfigurationInstances, config,
                     r.referrer, r.voiceInteractor, window, r.configCallback,
                     r.assistToken);
 ​
             activity.mCalled = false;
             // szj 分发 onCreate() 事件
             if (r.isPersistable()) 
                 mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
             else 
                 mInstrumentation.callActivityOnCreate(activity, r.state);
            
             // 判断是否调用super.onCreate() 方法
             if (!activity.mCalled) 
                 throw new SuperNotCalledException(
                     "Activity " + r.intent.getComponent().toShortString() +
                     " did not call through to super.onCreate()");
            
        
        ...  catch (Exception e) 
        ...
    return activity;
 

在performLaunchActivity()这段代码中有几个重点:

  • createBaseContextForActivity() 创建ContextImpl
  • mInstrumentation.newActivity(,); 通过反射创建Activity实例
  • 然后会调用Activity#attach() 方法绑定window等操作
  • 绑定了window之后会立即调用Activity#onCreate()进行页面初始化

本篇重点是Context,其他的先不关注,先来看看createBaseContextForActivity() 代码

 # ContextImpl.java
 @UnsupportedAppUsage
 static ContextImpl createActivityContext(ActivityThread mainThread,
         LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
         Configuration overrideConfiguration) 
    ..../// szj创建Context
     ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null,
             activityInfo.splitName, activityToken, null, 0, classLoader, null);
    ...final ResourcesManager resourcesManager = ResourcesManager.getInstance();/// szj 通过ResourcesManager创建Resources
     context.setResources(resourcesManager.createBaseTokenResources(activityToken,
             packageInfo.getResDir(),
            ....));
     return context;
 

最终会调用到 ResourcesManager.getInstance().createBaseTokenResources() 方法

最终

  • activity创建Resurces
  • application创建Resurces

都是调用到ResourcesManager#createResources()来创建Resources

这里还用到了一个类:ResourcesKey 这个类主要作用就是来存储数据,以及做一些校验等

ResourcesManager#createResources()源码分析

 #ResourcesManager.java
   
 private @Nullable Resources createResources(@Nullable IBinder activityToken,
         @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) 
     synchronized (this) //szj 从缓存中找 ResourcesImpl 如果不存在就创建
   代码1:  ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
         if (resourcesImpl == null) 
             return null;
        if (activityToken != null) 
             // 创建Resources
             return createResourcesForActivityLocked(activityToken, classLoader,
                     resourcesImpl, key.mCompatInfo);
         else 
             // 直接创建Resources对象
             return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
        
    
 

先来看findOrCreateResourcesImplForKeyLocked(key);

 #ResourcesManager.java
   
 private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
         @NonNull ResourcesKey key) 
     // szj查找与ResourcesImpl匹配的缓存资源
     ResourcesImpl impl = findResourcesImplForKeyLocked(key);
     if (impl == null) 
         // szj 创建ResourcesImpl
         impl = createResourcesImpl(key);
         if (impl != null) 
             // 加入到缓存中
             mResourceImpls.put(key, new WeakReference<>(impl));
        
    
     return impl;
 

这段代码很简单,做了一些缓存,通过createResourcesImpl() 创建了ResourcesImpl

 #ResourcesManager.java
   
 private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) 
     final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
     daj.setCompatibilityInfo(key.mCompatInfo);// szj创建 AssetManager
     final AssetManager assets = createAssetManager(key);
     if (assets == null) 
         return null;
    final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
     final Configuration config = generateConfig(key, dm);
     // 根据assetManager 创建一个ResourceImpl
     // 其实找资源是 Resources -> ResourcesImpl -> AssetManager
     final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);...
     return impl;
 

关键点又来了:

创建ResourcesImpl需要4个参数:

  • 参数一: AssetManager 具体资源管理(重要)

  • 参数二: DisplayMetrics 屏幕的一些封装

    • 通过getResources().getDisplayMetrics().density 获取过屏幕的密度
    • 通过getResources().getDisplayMetrics().widthPixels 获取过屏幕的宽度等
  • 参数三: Configuration 一些配置信息[对本篇来说不重要]

  • 参数四: DisplayAdjustments 资源的兼容性等 [对本篇来说不重要]

createAssetManager方法:

 #ResourcesManager.java
   
 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) 
     // szj 创建AssetManager对象
     final AssetManager.Builder builder = new AssetManager.Builder();// key.mResDir 就是apk在手机内存中的的完整路径
     if (key.mResDir != null) 
         try 
             builder.addApkAssets(loadApkAssets(key.mResDir, false, false));
         catch (IOException e) 
             return null;
        
    ....if (key.mLibDirs != null) 
       /// 循环lib中的资源
         for (final String libDir : key.mLibDirs) 
             // .apk
             /// 只有.apk文件中才有资源,所以只要有资源的地方
             if (libDir.endsWith(".apk")) 
                 try 
                     builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
                             false /*overlay*/));
                 catch (IOException e) 
                
            
        
    ...return builder.build();
 

这段代码通过Builder设计模式,将多个资源文件下的资源都保存起来

多个资源指的是一个项目中的多个lib

来看看单个资源是如何加载的的(loadApkAssets):

 #ResourcesManager.java
  
 // path 表示当前apk在手机中的的完整路径
 private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
         throws IOException 
 ....
     // We must load this from disk.
       /// 从磁盘加载apk资源
     if (overlay) 
         apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path), 0 /*flags*/);
     else 
         apkAssets = ApkAssets.loadFromPath(path, sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
    ....
     return apkAssets;
 

最终通过静态方法创建ApkAssets:

 # ApkAssets.java
 public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath,
         @PropertyFlags int flags) throws IOException 
     return new ApkAssets(FORMAT_IDMAP, idmapPath, flags, null /* assets */);
 public static @NonNull ApkAssets loadFromPath(@NonNull String path, @PropertyFlags int flags)
             throws IOException 
   return new ApkAssets(FORMAT_APK, path, flags, null /* assets */);
 

创建ApkAssets的时候就是通过

  • 一个变量来标记当前是什么文件
  • 并且保存文件路径

这个变量一共有4种类型:

  • FORMAT_APK 标记为apk文件
  • FORMAT_IDMAP 标记为idmap文件
  • FORMAT_ARSC 标记为 resources.arsc文件
  • FORMAT_DIR 标记为是一个目录

默认都是标记为apk文件,因为默认加载的就是.apk文件

这里着重提一下 resources.arsc 文件

这个文件是打包的时候自动生成的,会存放一些资源下的信息,例如图中的id等等,全部资源都可以在这里面找到!

OK,回到主题,这里就不扯了

当解析了apk之后,就会调用 AssetManager.Builder#build()方法

 #ResourcesManager.java
 ​
 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) final AssetManager.Builder builder = new AssetManager.Builder();
     if (key.mResDir != null) 
       try 
         /// 上面代码将apk路径都解析好了
         builder.addApkAssets(loadApkAssets(key.mResDir, false, false));
       catch (IOException e) 
         return null;
      
    
 ​
 ​
 ...
 // 现在执行build()
 return builder.build();
 
 #AssetManager.Builder.java
   
 public AssetManager build() 
    ....
     final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];....
     final AssetManager assetManager = new AssetManager(false /*sentinel*/);
 
   // 最终交给 nativeSetApkAssets() 来管理
     AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
             false /*invalidateCaches*/);
     assetManager.mLoaders = mLoaders.isEmpty() ? null
            : mLoaders.toArray(new ResourcesLoader[0]);return assetManager;
 

最终通过AssetManager.Builder 来创建了AssetManager

并且由ApkAssets保存了apk的一些信息,例如路径,文件类型等

最终创建好AssetManager交给ResourcesImpl来管理

 #ResourcesManager.java
   
 private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) 
     final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
     daj.setCompatibilityInfo(key.mCompatInfo);/// 刚才通过AssetManager.Builder() 来创建的AssetManager
     final AssetManager assets = createAssetManager(key);
     if (assets == null) 
         return null;
    
 // 交给ResourcesImpl 来管理
     final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);return impl;
 

在退回到最外层:

 #ResourcesManager.java
 ​
 private @Nullable Resources createResources(@Nullable IBinder activityToken,
         @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) 
     synchronized (this) /// 刚才走的这创建的ResourcesImpl
         ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
         if (resourcesImpl == null) 
             return null;
        if (activityToken != null) 
             // 创建Resources
             return createResourcesForActivityLocked(activityToken, classLoader,
                     resourcesImpl, key.mCompatInfo);
         else 
             // 直接创建Resources对象
             return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
        
    
 

通过findOrCreateResourcesImplForKeyLocked() 中找或者创建 ResourcesImpl

最终将ResourcesImpl交给Resources来管理

走到这里Resources就创建好了

这里有很多角色来捋一下:

  • ResourcesManager 用来创建Resources
  • ResourcesImpl 用来创建AssetManager,Resources的具体实现,用来具体读取资源
  • AssetManager 管理apk,解析app/多个lib 下的资源
  • ApkAssets 用来记录apk信息
  • Resources 用来管理ResourcesImpl

drawable 如何加载出来的

相信大家在开发中经常写这种代码,这一小节来看看他是如何加载出来的

 #Context.java
 ​
 public final Drawable getDrawable(@DrawableRes int id) 
     return getResources().getDrawable(id, getTheme());
 
 #Resources.java
   
 public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
         throws NotFoundException 
     return getDrawableForDensity(id, 0, theme);
 
 ​
 <

以上是关于android 皮肤包换肤之Resources加载的主要内容,如果未能解决你的问题,请参考以下文章

android 换肤框架搭建及使用 (3 完结篇)

android 换肤框架搭建及使用 (3 完结篇)

LibGDX 默认皮肤未加载

Android 资源加载Resources源码分析(8.0)

Android 插件化“ 插桩式 “ 插件化框架 ( 获取插件入口 Activity 组件 | 加载插件 Resources 资源 )

Android实现apk插件方式换肤