android 皮肤包换肤之Resources加载(一)

news2024/11/24 13:50:49

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

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

09c0039d1d214fcf7d6ad96b0d1fab3e

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

image-20221228140514037

最终会走到LoadedApk#getResources()

image-20221228140825055

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

image-20221228142726400

最终

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

image-20221228153338250

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

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

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

image-20221228153810892

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

image-20221228160249480

走到这里Resources就创建好了

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

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

drawable 如何加载出来的

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

image-20221228161814337

 #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()

image-20221228174018899

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

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

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

image-20221228190511889

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

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

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

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

image-20221228193610264

这里只需要传入一个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来获取本身的资源了!

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

c098db24c17ece49174f4a828923d4d8

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

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

制作“皮肤包”

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

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

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

  1. 直接创建module

image-20221229133545490

  1. 创建lib

image-20221229134038339

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

image-20221229152422945

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

image-20221229140524397

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

image-20221229140954379

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

使用皮肤包

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

adb push apk路径 根目录

adb shell

ls sdcard

image-20221229145002285

加载皮肤包中的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

image-20221229150400147

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

image-20221229150657749

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

fd3b59c9b9ac337437283fefe5519ac9

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

git clone -b level-simple https://gitee.com/lanyangyangzzz/skin-demo.git

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

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

热门文章:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/126537.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python黑帽子】——搭建TCP端口扫描器

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门 创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座…

计算1到n的和(不用循环且逐步限制条件)

目录 一、题目简单描述 二、递归实现 1、if…else… 2、三目运算符 &#xff1f;&#xff1a; 3、逻辑与操作符 && 三、公式实现 四、C调用构造函数累加法 注&#xff1a;满足题目要求的解法有递归实现的第三种、公式实现、C调用构造函数累加法三种方法、 一、题目简…

死锁的成因以及解决方案

&#x1f388;专栏链接:多线程相关知识详解 目录 一.什么是死锁以及死锁的成因 Ⅰ.一个线程一把锁 Ⅱ.两个线程两把锁 Ⅲ.多个线程多把锁 二.死锁的解决方案 一.什么是死锁以及死锁的成因 死锁是一个线程加上锁了之后,解不开了 在多线程编程中&#xff0c;我们为了防止多…

【微服务】3、NACOS 的使用

&#x1f516; Eureka 可以做注册中心【https://github.com/Netflix/eureka】 &#x1f516; 但它的功能比较少&#xff0c;仅仅注册中心 &#x1f516; nacos 也可做注册中心&#xff0c;且功能更加丰富【https://nacos.io/】 一、了解 Nacos ✏️ Nacos 是阿里巴巴的产品&am…

【Python】PyQt拖动控件对齐到网格

实现如下需求&#xff1a; 在PyQt界面上有一个控件&#xff0c;实现其可任意拖动&#xff0c;且鼠标释放时自动对齐到网格。 目录1.控件任意拖动并对齐到网格2.进阶&#xff1a;双击控件使其移动到其他网格1.控件任意拖动并对齐到网格 如下按钮(尺寸100100)&#xff0c;可任意…

【K3s】第11篇 解决“1 Preemption is not helpful for scheduling”问题

目录 1、遇到问题 2、问题解决 1、遇到问题 sudo kubectl get pods -A sudo kubectl describe pods coredns-b96499967-q5lzw -n kube-system Events: Type Reason Age From Message ---- ------ ---- ---- …

YXC | ADAS自动驾驶四大模块选用晶振有何要求

近几年无人驾驶汽车&#xff08;ADAS&#xff09;热度非常高&#xff0c;不少汽企巨头纷纷入局&#xff0c;那么无人驾驶汽车需具备什么硬件设备呢&#xff1f; 自动驾驶汽车依靠人工智能&#xff08;AI&#xff09;、视觉计算、监控系统模块、雷达测距系统模块、和GPS全球定位…

SpringBoot 这两个配置文件有什么区别?

本文讲解了关于 SpringBoot 自动装配的两个配置文件spring.factories 和 spring-autoconfigure-metadata.properties有什么区别&#xff1f;点击上方“后端开发技术”&#xff0c;选择“设为星标” &#xff0c;优质资源及时送达读过上一片文章你可能会发现&#xff0c;在自动装…

第二个脚本——自动登录学习通

目录 本篇主要内容&#xff1a; 详细步骤&#xff1a; 第一步&#xff1a;对登入页面进行分析 第二步&#xff1a;模拟点击&#xff0c;表单填写和多边框操作原理介绍 模拟点击 表单填写 操作多选框: 第三步&#xff0c;实现自动登录 完整代码&#xff1a; 本篇主要内…

日百万流量网站励志一生被K

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 曾经每天小百万IP的网站、Z-blog流量最大的网址之一&#xff0c;励志一生这两周被百度K了&#xff0c;流量瞬间没有了&#xff0c;联盟广告收入估计日落千丈。这个网站有多牛可能很多人不清楚&#…

Linux内存管理:NUMA技术详解(非一致内存访问架构)

一.背景 所谓物理内存&#xff0c;就是安装在机器上的&#xff0c;实打实的内存设备&#xff08;不包括硬件cache&#xff09;&#xff0c;被CPU通过总线访问。在多核系统中&#xff0c;如果物理内存对所有CPU来说没有区别&#xff0c;每个CPU访问内存的方式也一样&#xff0c…

基于springboot+jpa 实现多租户动态切换多数据源 - 使用Flyway实现多数据源数据库脚本管理和迭代更新

多租户动态多数据源系列 1、基于springbootjpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表 2、基于springbootjpa 实现多租户动态切换多数据源 - 基于dynamic-datasource实现多租户动态切换数据源 3、基于springbootjpa 实现多租户动态切换多数据源 - 使用Fl…

Kafka Producer Acks机制

Kafka Producer Acks 设置ACK props.put("acks", "all");通过上述代码&#xff0c;配置kafka生产者发送消息后&#xff0c;是否等待Broker的回执信息。在集群环境下&#xff0c;该配置是kafka保证数据不丢的重要的参数之一&#xff0c;今天来学习一下&…

深入理解Elasticsearch分片

了解分片的基本原理&#xff0c;对Elasticsearch性能调优有帮助。 关系梳理 ES底层使用的是Lucene库&#xff0c;ES的分片&#xff08;shard &#xff09;是Lucene的索引&#xff0c;ES的索引是分片的集合&#xff0c;Lucene的索引是由多个段&#xff08;segment&#xff09;…

青岛OJ如何导入题库详细图示

打开你的后台管理 找到问题位置 增加题目是可以编辑题目&#xff0c;导入数据。 导入导出是用题目和数据直接导入的。 这个ID的话就是题目ID不能设置一样的 然后题目输入输出就都不说了 按照格式就可以了&#xff0c;这里说一下Tag是标签&#xff0c;每次都要设置&#xff…

shell-条件测试

1、编写一个 Shell脚本&#xff0c;程序执行时从键盘读入一个目录名&#xff0c;如果用户输入的目录不存在&#xff0c;则提示file does not exist&#xff1b;如果用户输入的不是目录则提示用户必须输入目录名&#xff1b;如果用户输入的是目录则显示这个目录下所有文件的信息…

小程序版 Three.js 框架下载及目录配置

1.库文件说明 由于微信官方提供的threejs适配库已经很久没有更新&#xff0c;而且开发者普遍反映使用起来很难用。 我这里分享的是独立的库文件&#xff0c;不需要npm安装&#xff0c;下载后将库文件放到项目中即可使用。 2.下载后的压缩包文件 3.解压后的文件夹结构 4.文件…

Vue2和Vue3的双向数据绑定原理

目录前言&#xff1a;vue2.x 是如何实现响应式系统的&#xff1a;defineProperty 的痛点&#xff1a;Object.defineProperty 代码的使用Proxy 方法的理解Proxy 代码的使用&#xff1a;总结&#xff1a;前言&#xff1a; 今天小编给大家讲解一下&#xff0c;Vue2和Vue3的双向数据…

CAPL学习之路-测试功能集函数(诊断测试)

TestCollectDiagEcuInformation 向诊断目标的诊断类下的所有诊断服务发送诊断请求,并将诊断响应写入测试报告中 testcase TCExample() {int status;status = TestCollectDiagEcuInformation( "Door", "Sessions");if( status == 0)TestStepPass( "EC…

javaweb项目接入CAS单点认证(含自身系统的三员过滤)

一、搭建cas server 1.下载war包 2.打开cmd窗口执行以下命令&#xff0c;命令如下(指定ip)&#xff1a; keytool -genkey -v -alias casbm -keyalg RSA -keystore D:\cas\keystore\casbm.keystore -ext SANIP:192.168.2.166 3.我们生成秘钥库后需要从秘钥库中导出证书&#x…