红橙Darren视频笔记 换肤框架1 获取其他apk中的资源

1.Android xml属性资源的加载

以ImageView的src为例 看看Android是如何找到图片资源的
我们通常使用image的代码如下

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/btn_back"/>

Android是如何加载到这个资源的呢
点进src的源码 这是Android源码的定义的文件 显然这种写法和我们使用

    <declare-styleable name="ImageView">
        <!-- Sets a drawable as the content of this ImageView. -->
        <attr name="src" format="reference|color" />
 		...
    </declare-styleable>

很明显 这里使用了下xml自定义属性 查看ImageView源码进一步证实这一点
我们在imageview的构造方法中发现了如下代码

        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);

        final Drawable d = a.getDrawable(R.styleable.ImageView_src);//重点跟踪
        if (d != null) {
            setImageDrawable(d);
        }

这是经典的读取自定义属性的代码 我们接着跟踪 看drawable是如何获取到的
TypeArray部分代码:

    @Nullable
    public Drawable getDrawable(@StyleableRes int index) {
        return getDrawableForDensity(index, 0);
    }

    public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }

        final TypedValue value = mValue;
        if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
            if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                throw new UnsupportedOperationException(
                        "Failed to resolve attribute at index " + index + ": " + value);
            }

            if (density > 0) {
                // If the density is overridden, the value in the TypedArray will not reflect this.
                // Do a separate lookup of the resourceId with the density override.
                mResources.getValueForDensity(value.resourceId, density, value, true);
            }
            return mResources.loadDrawable(value, value.resourceId, density, mTheme);//重点
        }
        return null;
    }

最终我们发现 图片资源由mResources进行解析
那么mResources是如何进行初始化的呢?

2.mResources的初始化

TypeArray中的mResources初始化不是很容易看懂,我们从其他方面入手,比如我们经常使用context.getResources()来获取string 颜色 图片等 那么 这里的getResources中的Resource是如何初始化的呢(API 28)

	getResources().getDrawable(-1,null);

	//ContextThemeWrapper
    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();//走这里
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

	//ContextWrapper
    @Override
    public Resources getResources() {
        return mBase.getResources();
    }

	// Context
    public abstract Resources getResources();

	// 追踪到实现类 ContextImpl
    @Override
    public Resources getResources() {
        return mResources;
    }

    void setResources(Resources r) {
        if (r instanceof CompatResources) {
            ((CompatResources) r).setContext(this);
        }
        mResources = r;
    }

            c.setResources(createResources(mActivityToken, pi, null, displayId, null,
                    getDisplayAdjustments(displayId).getCompatibilityInfo()));

    private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
            int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
        final String[] splitResDirs;
        final ClassLoader classLoader;
        try {
            splitResDirs = pi.getSplitPaths(splitName);
            classLoader = pi.getSplitClassLoader(splitName);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
        return ResourcesManager.getInstance().getResources(activityToken,//关键
                pi.getResDir(),
                splitResDirs,
                pi.getOverlayDirs(),
                pi.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfig,
                compatInfo,
                classLoader);
    }

	// ResourcesManager
    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);
        }
    }

	// ResourcesMananger
    // 创建Resources的有两个方法 getOrCreateResourcesForActivityLocked以及getOrCreateResourcesLocked
    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {

            if (activityToken != null) {
                ...
            } else {
                // Clean up any dead references so they don't pile up.
                ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);

                // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }

                // We will create the ResourcesImpl object outside of holding this lock.
            }

            // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }

            // Add this ResourcesImpl to the cache.
            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;
        }
    }
	//1 getOrCreateResourcesLocked
    private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
            @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
        // 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 = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : 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;
    }

	//2 getOrCreateResourcesForActivityLocked
    private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
            @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,
            @NonNull CompatibilityInfo compatInfo) {
        final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
                activityToken);

        final int refCount = activityResources.activityResources.size();
        for (int i = 0; i < refCount; i++) {
            WeakReference<Resources> weakResourceRef = activityResources.activityResources.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;
            }
        }

        Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : new Resources(classLoader);//重点
        resources.setImpl(impl);
        activityResources.activityResources.add(new WeakReference<>(resources));
        if (DEBUG) {
            Slog.d(TAG, "- creating new ref=" + resources);
            Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
        }
        return resources;
    }

	//两个创建resources的方法都类似 都使用了缓存机制 都是通过如下代码创建resources的
        Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : new Resources(classLoader);

	//而通过CompatResources创建的其实和直接用Resources创建的差不多
	// CompatResources.java
    public CompatResources(ClassLoader cls) {
        super(cls);//super是Resources
        mContext = new WeakReference<>(null);
    }

	//	Resources.java
    public Resources(@Nullable ClassLoader classLoader) {
        mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
    }

	// 也就是无论如何 都是通过 Resources resources = new Resources(classLoader)创建的Resources
	// 而实际上 Resources内部包含了ResourcesImpl对象 Resources利用ResourcesImpl来获取系统的一些资源
	// 如1.获取图片
    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 {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValueForDensity(id, density, value, true);
            return impl.loadDrawable(this, value, id, density, theme);
        } finally {
            releaseTempTypedValue(value);
        }
    }

	// 2.获取Dimension
    public float getDimension(@DimenRes int id) throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_DIMENSION) {
                return TypedValue.complexToDimension(value.data, impl.getDisplayMetrics());
            }
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        } finally {
            releaseTempTypedValue(value);
        }
    }
	// 3.获取color
    public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            if (value.type >= TypedValue.TYPE_FIRST_INT
                    && value.type <= TypedValue.TYPE_LAST_INT) {
                return value.data;
            } else if (value.type != TypedValue.TYPE_STRING) {
                throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                        + " type #0x" + Integer.toHexString(value.type) + " is not valid");
            }

            final ColorStateList csl = impl.loadColorStateList(this, value, id, theme);
            return csl.getDefaultColor();
        } finally {
            releaseTempTypedValue(value);
        }
    }
	// 4.获取String
    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));
    }

	// 我们可以直接创建ResourcesImpl或者创建Resources后通过其内部的ResourcesImpl来访问资源 这里我像视频一样选择后者
	// 其中要使得Resources和mResourcesImpl都进行初始化 最简单的调用时调用Resources的三个参数的构造方法
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }
	// 需要看一下assets metrics config是如何初始化的
    /**
     * Only for creating the System resources.
     */
    private Resources() {
        this(null);

        final DisplayMetrics metrics = new DisplayMetrics();
        metrics.setToDefaults();

        final Configuration config = new Configuration();
        config.setToDefaults();

        mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                new DisplayAdjustments());
    }

其中最重要的是参数AssetManager 上述例子使用的是系统的AssetManager 我们自己创建的时候需要使用自己path来创建AssetManager,具体灵感来自API 26的源码
AssetManager assets = new AssetManager();
assets.addAssetPath(resDir)// resDir apk的目录
API 28的相关代码如下

    /**
     * @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}
     * @hide
     */
    @Deprecated
    public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }

    private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
        Preconditions.checkNotNull(path, "path");
        synchronized (this) {
            ensureOpenLocked();
            final int count = mApkAssets.length;

            // See if we already have it loaded.
            for (int i = 0; i < count; i++) {
                if (mApkAssets[i].getAssetPath().equals(path)) {
                    return i + 1;
                }
            }

            final ApkAssets assets;
            try {
                if (overlay) {
                    // TODO(b/70343104): This hardcoded path will be removed once
                    // addAssetPathInternal is deleted.
                    final String idmapPath = "/data/resource-cache/"
                            + path.substring(1).replace('/', '@')
                            + "@idmap";
                    assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
                } else {
                    assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);
                }
            } catch (IOException e) {
                return 0;
            }

            mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
            mApkAssets[count] = assets;
            nativeSetApkAssets(mObject, mApkAssets, true);
            invalidateCachesLocked(-1);
            return count + 1;
        }
    }

有了以上的调查 我们就可以创建自己的Resource并取得图片 颜色 String等资源了

3.Demo 获取其他APK的资源

准备工作:

3.1.创建皮肤包

创建一个Android项目 里面仅仅放一个图片abc.png 生成一个APK 重命名为test.skin 将apk放置到手机内存根目录
/storage/emulated/0/test.skin
首先需要申请内存访问权限,之前就是因为没有申请权限 导致获取的id一直是0 结果调试了半天 才发现是没有内存的访问权限

3.2.申请内存访问权限

public class Util {
    public static void checkAndRequestReadSDCardPermission(Activity activity) {
        if (activity == null) {
            return;
        }
        if (activity.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            activity.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
        }
    }
}

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

3.3.创建Resource和AssertManager

    protected void initView() {
        Util.checkAndRequestReadSDCardPermission(this);
        loadImg();
    }

    private void loadImg() {
        try {
            ImageView img = findViewById(R.id.emptyImg);
            // 读取本地的一个 .skin里面的资源
            Resources superRes = getResources();
            // 创建AssetManager
            AssetManager assetManager = AssetManager.class.newInstance();
            // 寻找hide的方法
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            // method.setAccessible(true); 如果是私有的
            Log.e(TAG, "loadImg from : "+Environment.getExternalStorageDirectory().getAbsolutePath()+
                    File.separator + "test.skin");
            // 反射执行方法 assetManager指向指定的皮肤包 /storage/emulated/0/test.skin
            method.invoke(assetManager, Environment.getExternalStorageDirectory().getAbsolutePath()+
                    File.separator + "test.skin");

            Resources resource = new Resources(assetManager,superRes.getDisplayMetrics(),
                    superRes.getConfiguration());

            // 获取指定路径apk的packageName
            String packageName = "";
            if (this.getApplication() != null) {
                Log.e(TAG, "loadImage: getApplication!=null");
                if (this.getApplication().getPackageManager() != null) {
                    String myPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
                            File.separator + "test.skin";
                    Log.e(TAG, "loadImage: myPath ==="+myPath);
                    packageName = this.getApplication().getPackageManager().getPackageArchiveInfo(Environment.getExternalStorageDirectory().getAbsolutePath() +
                            File.separator + "test.skin", PackageManager.GET_ACTIVITIES).packageName;
                } else {
                    Log.e(TAG, "loadImage: getPackageManager==null");
                }
            }

            // 获取资源 id
            int drawableId = resource.getIdentifier("abc","drawable",packageName);
            Drawable drawable = resource.getDrawable(drawableId);
            img.setImageDrawable(drawable);
        } catch (Exception e) {
            Log.e(TAG, "loadImg: "+e.getStackTrace().toString());
            e.printStackTrace();
        }
    }

4.AssetManager的Android部分源码(API28)

我们之前已经知道资源的加载是通过Resources类来加载的,而实际上执行方法的是Resources的内部成员ResourcesImpl,这里我们继续往下深入
以Resources的getDrawableForDensity方法为例 其内部调用了ResourcesImpl的方法
loadDrawable(this, value, id, density, theme);

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

        try {
            if (TRACE_FOR_PRELOAD) {
                // Log only framework resources
                if ((id >>> 24) == 0x1) {
                    final String name = getResourceName(id);//获取预加载的Android原生的资源 内部调用的也是AssetManager的方法
                    if (name != null) {
                        Log.d("PreloadDrawable", name);
                    }
                }
            }

			...

            // First, check whether we have a cached version of this drawable
            // that was inflated against the specified theme. Skip the cache if
            // we're currently preloading or we're not using the cache.
            if (!mPreloading && useCache) {// 缓存机制
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    cachedDrawable.setChangingConfigurations(value.changingConfigurations);
                    return cachedDrawable;
                }
            }

            // 预加载资源
            // Next, check preloaded drawables. Preloaded drawables may contain
            // unresolved theme attributes.
            final Drawable.ConstantState cs;
            if (isColorDrawable) {
                cs = sPreloadedColorDrawables.get(key);
            } else {
                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
            }

            Drawable dr;
            boolean needsNewDrawableAfterCache = false;
            if (cs != null) {//加载Android原生资源的case
                if (TRACE_FOR_DETAILED_PRELOAD) {
                    // Log only framework resources 
                    if (((id >>> 24) == 0x1) && (android.os.Process.myUid() != 0)) {
                        final String name = getResourceName(id);
                        if (name != null) {
                            Log.d(TAG_PRELOAD, "Hit preloaded FW drawable #"
                                    + Integer.toHexString(id) + " " + name);
                        }
                    }
                }
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {//加载颜色类型的drawable
                dr = new ColorDrawable(value.data);
            } else {// 加载真正的drawable 重点!!!
                dr = loadDrawableForCookie(wrapper, value, id, density);
            }
            ...
        }
    }

	private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density) {
        ...
				if (file.endsWith(".xml")) {//加在xml类型的drawable
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                    rp.close();
                } else {// 加载png jpeg等类型的图片
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }
        ...
	}

我们知道加载是通过Resources类来加载的,而实际上执行方法的是Resources的内部成员,现在看了源码 其实真正查找资源的是AssetManager
我们看看AssetManager是如何初始化的吧
查看AssetManager的构造方法 有两个

    /**
     * Private constructor that doesn't call ensureSystemAssets.
     * Used for the creation of system assets.
     */
    @SuppressWarnings("unused")
    private AssetManager(boolean sentinel) {
        mObject = nativeCreate();
        if (DEBUG_REFS) {
            mNumRefs = 0;
            incRefsLocked(hashCode());
        }
    }

    /**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * @hide
     */
    public AssetManager() {
        final ApkAssets[] assets;
        synchronized (sSync) {
            createSystemAssetsInZygoteLocked();
            assets = sSystemApkAssets;
        }

        mObject = nativeCreate();
        if (DEBUG_REFS) {
            mNumRefs = 0;
            incRefsLocked(hashCode());
        }

        // Always set the framework resources.
        setApkAssets(assets, false /*invalidateCaches*/);
    }

方法的注释已经说明了区别 无参的构造方法是专门为了创建系统资源的 通过源码我们也知道无参的构造方法在createSystemAssetsInZygoteLocked中调用了包含boolean作为参数的构造方法。
createSystemAssetsInZygoteLocked方法中有个值得关注的点

apkAssets.add(ApkAssets.loadFromPath(FRAMEWORK_APK_PATH, true /*system*/));
private static final String FRAMEWORK_APK_PATH = "/system/framework/framework-res.apk";

这里加载了Android系统的资源apk 我猜测 小米的MIUI或者其他更改系统皮肤的手机制造商 是可以更改这个apk中的资源来达到控制UI式样的目的的
由于Android9.0中 AssetManager的源码变动较大,我能力有限,没有看懂在9.0中AssetManager如何在framework层初始化的
猜测可能的创建方式如下

	nativeCreate()
    // frameworks/base/core/jni/android_util_AssetManager.cpp
    static jlong NativeCreate(JNIEnv* /*env*/, jclass /*clazz*/) {
		// AssetManager2 needs to be protected by a lock. To avoid cache misses, we allocate the lock and
		// AssetManager2 in a contiguous block (GuardedAssetManager).
		return reinterpret_cast<jlong>(new GuardedAssetManager());
    }

	struct GuardedAssetManager : public ::AAssetManager {
		Guarded<AssetManager2> guarded_assetmanager;
	};

	//AssetManager2.cpp
	AssetManager2::AssetManager2() {
		memset(&configuration_, 0, sizeof(configuration_));
	}

最终创建的可能是AssetManager2
因为不是很懂c++ 再往下就分析不动了。。。
罗升阳的博客讲的比较清楚,不过他的Android版本可能是6.0版本的 Android9.0系统源码已经大变 不过还是有很大的参考价值
https://blog.csdn.net/luoshengyang/article/details/8791064
另外值得说一下的是 resources.arsc
这个是打包后的apk中的一个文件 是各种资源和id的映射,assetManager管理资源应该和这个文件关系很大 使用Android Studio打开apk并选择resources.arsc打开 可以看到详细的映射 我找到了系统的资源apk的resources.arsc 如图
在这里插入图片描述
打开我们之前使用皮肤包 还能看到我们放入的资源的id与图片名称的映射
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值