首页 > 编程语言 >Android 换肤之资源(Resources)加载源码分析(一)

Android 换肤之资源(Resources)加载源码分析(一)

时间:2023-06-19 10:33:42浏览次数:48  
标签:换肤 return AssetManager 源码 key Android null Resources 加载


本系列计划3篇:

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

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

  1. Resources在什么时候被解析并加载的
  1. Application#Resources
  2. Activity#Resources
  1. drawable 如何加载出来的
  2. 创建自己的Resources加载自己的资源
  3. 制作皮肤包"皮肤包"
  4. 加载“皮肤包”中的资源

tips:源码基于android-30

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

Android 换肤之资源(Resources)加载源码分析(一)_android

效果很简单,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()

Android 换肤之资源(Resources)加载源码分析(一)_开发语言_02

最终会走到LoadedApk#getResources()

Android 换肤之资源(Resources)加载源码分析(一)_java_03

然后会从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() 方法

Android 换肤之资源(Resources)加载源码分析(一)_加载_04

最终

  • 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种类型:

Android 换肤之资源(Resources)加载源码分析(一)_缓存_05

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

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

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

Android 换肤之资源(Resources)加载源码分析(一)_android_06

这个文件是打包的时候自动生成的,会存放一些资源下的信息,例如图中的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来管理

Android 换肤之资源(Resources)加载源码分析(一)_缓存_07

走到这里Resources就创建好了

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

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

drawable 如何加载出来的

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

Android 换肤之资源(Resources)加载源码分析(一)_缓存_08

#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);
 }
 
 public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
     final TypedValue value = obtainTempTypedValue();
     try {
        ...
         return loadDrawable(value, id, density, theme);
    } finally {
         releaseTempTypedValue(value);
    }
 }
 
 Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
             throws NotFoundException {
 /// 最终通过ResourcesImpl 来加载drawable
         return mResourcesImpl.loadDrawable(this, value, id, density, theme);
    }
#ResourcesImpl.java

 Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
             int density, @Nullable Resources.Theme theme)
             throws NotFoundException {

   ....
       Drawable dr;
     if (cs != null) {
       ....
    } else if (isColorDrawable) {
       dr = new ColorDrawable(value.data);
    } else {
       // szj走这里
       dr = loadDrawableForCookie(wrapper, value, id, density);
    }
 }
#ResourcesImpl.java
 private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
         int id, int density) {
    ....
     try {
        ....
         try {
             // 判断drawable是否是xml
             if (file.endsWith(".xml")) {
                 final String typeName = getResourceTypeName(id);
               /// 判断是否是颜色
                 if (typeName != null && typeName.equals("color")) {
                   /// 是颜色
                     dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
                } else {
                   // 加载xml
                     dr = loadXmlDrawable(wrapper, value, id, density, file);
                }
            } else {
                 // 是图片
 
                 // szj mAssets = AssetManager()
                 // 打开这张图片
               // 最终获取到的是stream
                 final InputStream is = mAssets.openNonAsset(
                         value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                 final AssetInputStream ais = (AssetInputStream) is;
                 dr = decodeImageDrawable(ais, wrapper, value);
            }
        } 
      ...
    } catch (Exception | StackOverflowError e) {
        ...
         throw rnf;

 
     return dr;
 }
  • 加载颜色:
#ResourcesImpl.java
 private Drawable loadColorOrXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
         int id, int density, String file) {
     try {
       /// 加载颜色
         ColorStateList csl = loadColorStateList(wrapper, value, id, null);
         return new ColorStateListDrawable(csl);
    } catch (NotFoundException originalException) {
         // 如果报错就尝试当作xml中的drawable加载
         try {
             return loadXmlDrawable(wrapper, value, id, density, file);
        } catch (Exception ignored) {
             // If fallback also fails, throw the original exception
             throw originalException;
        }
    }
 }
  • 加载xml中的drawable
#ResourcesImpl.java
 private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
         int id, int density, String file)
         throws IOException, XmlPullParserException {
     try (
             XmlResourceParser rp =
                     loadXmlResourceParser(file, id, value.assetCookie, "drawable")
    ) {
         return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
    }
 }
  • 是图片,通过AssetManager来打开图片,获取到输入流,并转换为图片
#ResourcesImpl.java
  final Drawable dr;
 
 final InputStream is = mAssets.openNonAsset(
         value.assetCookie, file, AssetManager.ACCESS_STREAMING);
 final AssetInputStream ais = (AssetInputStream) is;
 dr = decodeImageDrawable(ais, wrapper, value);
 
 
 /// 将输入流的内容转换为drawable
 private Drawable decodeImageDrawable(@NonNull AssetInputStream ais,
             @NonNull Resources wrapper, @NonNull TypedValue value) {
   ImageDecoder.Source src = new ImageDecoder.AssetInputStreamSource(ais,
                                                                     wrapper, value);
   try {
     return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
       decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
    });
  } catch (IOException ioe) {
     return null;
  }
 }

再来一波小结:

Resources其实做的事情很有限,基本就是操控ResourcesImpl来控制AssetManager来获取资源

AssetManager会通过ApkAssets来存储apk信息,包括路径,类型等

然后AssetManager会通过apk的地址, 找到具体apk的文件,调用nativeSetApkAssets() 去解析apk中的具体资源

当我们加载一个drawable的时候

Resources会调用ResourcesImpl#loadDrawable() 来加载图片

然后会判断加载的drawable是一张图片,还是自定义的xml,或者drawable是一个颜色

  • 如果是图片,就通过AssetManager#openNonAsset()来解析资源图片,获取到intputStream流,来解码成drawable
  • 如果是xml,那么就通过XmlResourceParser来解析,最终生成drawable [这里面还有些细节,都是些if判断,就没看了]
  • 如果是颜色,和xml类似,也是一点点解析

创建自己的Resources加载本地资源

正常我们加载资源是通过getResources().getDrawable() 来加载

现在想实现的是,用我自己的Resources,来加载我们自己的资源

那么首先就要获取到当前程序在手机内存中的路径

getApplicationContext().getPackageResourcePath()

Android 换肤之资源(Resources)加载源码分析(一)_开发语言_09

因为这是个隐藏文件夹,所以只能从这里看,在手机上是找不到的…

接下来创建一个AssetManager,用来解析apk中的资源等

在源码中,是通过AssetManager.Builder来构建AssetManager, 但是Builder类被隐藏掉了

Android 换肤之资源(Resources)加载源码分析(一)_android_10

并且构造方法都被隐藏掉了,所以只能通过反射来构建AssetManager

构建AssetManager时,需要通过AssetManager#nativeSetApkAssets() 来解析apk中的资源

这里我们选择反射 addAssetPath() 方法

通过addAssetPath调用 addAssetPathInternal 最终调用到nativeSetApkAssets()

Android 换肤之资源(Resources)加载源码分析(一)_java_11

这里只需要传入一个apk在手机的路径即可

这里需要注意的是不能直接反射addAssetPathInternal(),可以看到图中addAssetPathInternal()左侧有一把锁,反射不了.

当前代码:

try (
   // 创建AssetManager
   AssetManager assetManager = AssetManager.class.newInstance()
 ) {
   // 反射调用 创建AssetManager#addAssetPath
   Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
 
   // 获取到当前apk在手机中的路径
   String path = getApplicationContext().getPackageResourcePath();
   Log.i("szjPath", path);
 
   /// 反射执行方法
   method.invoke(assetManager, path);
 
   // 创建自己的Resources
   Resources resources = new Resources(assetManager, createDisplayMetrics(), createConfiguration());
 
   // 根据id来获取图片
   Drawable drawable = resources.getDrawable(R.drawable.ic_launcher_background, null);
 
   // 设置图片
   mImageView.setImageDrawable(drawable);
 
 } catch (Exception e) {
   e.printStackTrace();
 }
 
 // 这些关于屏幕的就用原来的就可以
 public DisplayMetrics createDisplayMetrics() {
     return getResources().getDisplayMetrics();
 }
 
 public Configuration createConfiguration() {
     return getResources().getConfiguration();
 }

这样一来,就可以用我们自己的Resources来获取本身的资源了!

效果没啥好说的,就是一上来就加载

Android 换肤之资源(Resources)加载源码分析(一)_加载_12

接下来我们尝试加载另一个apk中的资源

首先我们需要一个有一个apk让我们来加载,就是通常说的“皮肤包”

制作“皮肤包”

皮肤包就是一个只有资源文件的apk

可以新建一个项目,然后存放对应的资源即可

也可以在同目录下将lib改为application,为了好保管,我们就使用这种办法

  1. 直接创建module

Android 换肤之资源(Resources)加载源码分析(一)_java_13

  1. 创建lib

Android 换肤之资源(Resources)加载源码分析(一)_开发语言_14

  1. 直接输入名字创建即可

Android 换肤之资源(Resources)加载源码分析(一)_加载_15

  1. 将lib修改为application,并添加applicationId, 并且添加同名资源(制作皮肤包)

Android 换肤之资源(Resources)加载源码分析(一)_java_16

  1. 生成“皮肤包”(skin-pack-making-debug.apk)

Android 换肤之资源(Resources)加载源码分析(一)_java_17

此时,皮肤包我们就制作好了,skin-pack-making-debug.apk,我们将它放入到手机内存中尝试加载一下

使用皮肤包

为了测试方便,我们直接将“皮肤包”放入到根目录即可

adb push apk路径 根目录

adb shell

ls sdcard

Android 换肤之资源(Resources)加载源码分析(一)_开发语言_18

加载皮肤包中的apk

public static final String PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "skin-pack-making-debug.apk";
 
 try {
     AssetManager assetManager = AssetManager.class.newInstance();
 
     @SuppressLint("DiscouragedPrivateApi")
     Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
     method.setAccessible(true);
     /// 反射执行方法
     method.invoke(assetManager, PATH);
 
   // 创建自己的Resources
     Resources resources = new Resources(assetManager, createDisplayMetrics(), createConfiguration());
 
   /*
    * getIdentifier 根据名字拿id
    * name: 资源名
    * defType: 资源类型
    * defPackage: 所在包名
    * return:如果返回0则表示没有找到
    */
   /// 加载drawable
   int drawableId = resources.getIdentifier("shark", "drawable", "com.skin.skin_pack_making");
   // 加载string
   int stringId = resources.getIdentifier("hello_skin", "string", "com.skin.skin_pack_making");
   // 加载color
   int colorId = resources.getIdentifier("global_background", "color", "com.skin.skin_pack_making");
 
   mImageView.setImageDrawable(resources.getDrawable(drawableId, null));
   mTextView.setText(resources.getString(stringId));
   mTextView.setBackgroundColor(resources.getColor(colorId, null));
 } catch (Exception e) {
     e.printStackTrace();
 
     showDialog("出错了" + e.getMessage());
 }

需要注意的是,这里得通过名字来获取id

当我们加载一个drawable,id,color或者string的时候,在加载的时候都会替换成id

Android 换肤之资源(Resources)加载源码分析(一)_android_19

各个apk生成的id肯定是各不相同的,所以我们找的是皮肤包中的资源id,

Android 换肤之资源(Resources)加载源码分析(一)_android_20

最后再来看看今天完成的效果:

Android 换肤之资源(Resources)加载源码分析(一)_缓存_21

请下载level-simple分支:完整代码

git clone -b level-simple gitee.com/lanyangyang…

原创不易,您的点赞就是对我最大的支持!

下一篇:android setContentView() / LayoutInflater 源码解析

热门文章:

  • android MD 进阶[五] CoordinatorLayout 从源码到实战…
  • android View生命周期
  • android MD进阶[四] NestedScrollView 从源码到实战…
  • android 浅析RecyclerView回收复用机制及实战(仿探探效果)
  • Android进阶 -事件冲突与解决方案大揭秘

作者:史大拿

标签:换肤,return,AssetManager,源码,key,Android,null,Resources,加载
From: https://blog.51cto.com/u_16163480/6511333

相关文章

  • 【Android】一文读懂 Activity 的生命周期
    作为Android开发人,如果说连Activity生命周期都没搞懂,会走非常多的弯路,所以这篇文章我就对Activity生命周期的生命周期进行一个简单的总结。单Activity生命周期的整体流程首先,我们创建一个My_A_Activity,并且打印它的各个生命周期方法。classMy_A_Activity:AppCompatActivity(){......
  • Android代码检查规则Lint的自定义与应用
    前言:在日常的代码开发中,此处相信每个开发人员对代码质量都是高要求,有自己的一套代码规范,但是我们不是单独作战,往往大家都是团队作战,人是最大的变量,各人各异,如何保证团队的代码质量和代码规范呢?靠开发者自觉吗?也许有的团队有严格的CR机制,在MR阶段会进行CR,CR不通过的MR是不允许合入的......
  • 【车载开发】Android车载操作系统来了,前景非常乐观
    时间回到2014年6月26日,谷歌在GoogleI/O大会的开幕式主题演讲中正式发布了手机车机映射方案AndroidAuto,旨在通过中控屏幕来使用手机内置的服务,让软件的体验更适合车载场景。2016年,谷歌又在I/O大会上展示了使用Android系统接管玛莎拉蒂Ghibli的中控系统,包括空调和......
  • Android - 无法使用任何临时 SqlClient 版本(v2.1.4、v4.1.0、v5Preview)连接到 SQL Ser
    Aconnectionwassuccessfullyestablishedwiththeserver,butthenanerroroccurredduringthepre-loginhandshake.设法用证书和IP地址解决它。使用powershell为您的IP地址创建证书:New-SelfSignedCertificate-certstorelocationcert:\localmachine\my-dns......
  • Android 12 自定义底部导航栏
    1.修改配置文件 frameworks\base\packages\SystemUI\res\values\config.xml<!--Navbarbuttondefaultordering/layout--><stringname="config_navBarLayout"translatable="false">left[.5W];leftrotate,volume_sub,back,home,r......
  • 浅谈生活中常见的三大应用程序架构(PE、ELF、Mach-O)、五大操作系统(windows、linux、mac
    ·今天不聊复杂的技术,就是想做一下科普。我们生活中常见的操作系统,大致有5种分别是 电脑: Windows linux    macos手机 androidiosWindows手机操作系统没有发展起来,不同的操作系统间软件不能......
  • 给Nexus6p刷入lineage14.1(android 7.1)和 nethunter 2019.3
    本文依据kali教程编写https://build.nethunter.com/contributors/re4son/angler/INSTALLATION.txt写在前面的话你可能很奇怪,为什么有kali2020.3不用要刷入2019.3版本的。其实目的是使用安卓7,因为高版本安卓对某些软件的兼容性太差,刷入2019载手动升级到2020.3.Andrax在安卓7、9......
  • memcpy源码
    【调用栈】 【代码】 【glibc2.17和2.18性能的讨论】https://sourceware.org/bugzilla/show_bug.cgi?id=24872......
  • 【QCustomPlot】使用方法(源码方式)
    说明使用QCustomPlot绘图库辅助开发时整理的学习笔记。同系列文章目录可见《绘图库QCustomPlot学习笔记》目录。本篇介绍QCustomPlot的一种使用方法,通过包含源码的方式进行使用,这也是最常用的方法,示例中使用的QCustomPlot版本为Version2.1.1。目录说明1.下载源码2.......
  • 杰森气象——实况天气小程序(内附完整源码)
    项目介绍当今社会,天气的变化对我们的生活产生着越来越大的影响。为了更好地了解天气状况,越来越多的人开始使用天气查询小程序。今天,介绍的是一款实用的天气查询小程序——杰森气象。杰森气象是一款功能强大的天气查询小程序,它可以帮助我们随时了解天气状况,包括实时天气、预警信息、......