Open Theme

一、替换应用资源

1. 实现主题包apk中的资源替换原来apk

old.jpg

new.jpg
  • 主题包需要完成工作

(1). AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    android:versionCode="664" 
    android:versionName="3.00.13" 
    package="com.example.mcdulltheme" 
    platformBuildVersionCode="21" 
    platformBuildVersionName="5.0-1521886">
    <application android:allowBackup="false" android:hasCode="false"/>
    <overlay android:priority="1" android:targetPackage="com.example.oldtheme"/>
</manifest>

(2). 覆盖OldTheme.apk 的资源

 可以替换图片、颜色、字符串等,但不可以替换xml资源,比如布局文件。

(3). 杀OldTheme.apk进程重新启动,资源替换成功。

2. framework层实现原理

  • 主题包安装过程

    apk安装过程中会调用PackageManagerService.createIdmapForPackagePairLI,这个方法在资源替换过程中起着重要作用。

/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

    //参数:pkg 要覆盖的apk, 从AndroidManifest.xml 里面的targetPackage读到的
    //opkg 主题资源apk
    private boolean createIdmapForPackagePairLI(PackageParser.Package pkg,
            PackageParser.Package opkg) {
        if (!opkg.mTrustedOverlay) {
            Slog.w(TAG, "Skipping target and overlay pair " + pkg.baseCodePath + " and " +
                    opkg.baseCodePath + ": overlay not trusted");
            return false;
        }
        //mOverlays 以目标apk为键值,对应的所有主题包apk信息
        ArrayMap<String, PackageParser.Package> overlaySet = mOverlays.get(pkg.packageName);
        if (overlaySet == null) {
            Slog.e(TAG, "was about to create idmap for " + pkg.baseCodePath + " and " +
                    opkg.baseCodePath + " but target package has no known overlays");
            return false;
        }
        final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid);
        // TODO: generate idmap for split APKs
        try {
            //在Install守护进程创建Idmap,用于查找主题包资源
            mInstaller.idmap(pkg.baseCodePath, opkg.baseCodePath, sharedGid);
        } catch (InstallerException e) {
            Slog.e(TAG, "Failed to generate idmap for " + pkg.baseCodePath + " and "
                    + opkg.baseCodePath);
            return false;
        }
        PackageParser.Package[] overlayArray =
            overlaySet.values().toArray(new PackageParser.Package[0]);
        Comparator<PackageParser.Package> cmp = new Comparator<PackageParser.Package>() {
            public int compare(PackageParser.Package p1, PackageParser.Package p2) {
                return p1.mOverlayPriority - p2.mOverlayPriority;
            }
        };
        Arrays.sort(overlayArray, cmp);

        pkg.applicationInfo.resourceDirs = new String[overlayArray.length];
        int i = 0;
        for (PackageParser.Package p : overlayArray) {
            //将应用资源路径resourceDirs改为主题包路径
            pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
        }
        return true;
    }

createIdmapForPackagePairLI 主要完成了已下工作:

  1. 根据主题包apk和原apk路径创建idmap文件

    data/resource-cache/

    data@com.example.mcdulltheme@base.apk@idmap

    com.example.mcdulltheme是主题包apk包名

  2. 将主题包路径添加到原应用资源路径resourceDirs中

  • 资源加载过程
SequenceDiagram1.png

上图为应用启动创建Resources 、 AssetManager 以及资源加载过程

我们通常会在Activity里面会使用getResources().getString(R.string.AnyString)来得到字符串或者其他资源。
getResources()最后是调用ContextImpl.getResources,返回一个Resources对象,那么我们就从Resources对象创建开始看。

getResources返回mResources对象,mResources对象在ContextImpl构造函数中创建。
    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        ... ...
        Resources resources = packageInfo.getResources(mainThread);
        ... ...
        mResources = resources;
        ... ...
    }

/frameworks/base/core/java/android/app/LoadedApk.java

    public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
        }
        return mResources;
    }

/frameworks/base/core/java/android/app/ActivityThread.java

    Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
            String[] libDirs, int displayId, LoadedApk pkgInfo) {
        return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
                displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
    }

ResourcesManager.getResources会调用getOrCreateResources

    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        ... ...

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

        synchronized (this) {
            ... ...

            final Resources resources;
            //这里创建Resources对象
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
            }
            return resources;
        }
    }
    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);
        //创建AssetManager对象
        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        //Resources的实现类ResourcesImpl
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        return impl;
    }
    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir是apk路径,'android'包的resDir是null,因为AssetManager会自动加载framework资源
        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    Log.e(TAG, "failed to add split asset path " + splitResDir);
                    return null;
                }
            }
        }
        //添加overlay资源,前面主题包安装过程中,有添加applicationInfo.resourceDirs,这里就会将主题包的资源加载到AssetManager中
        if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }
        //添加lib库
        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.
                    if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return assets;
    }

createAssetManager 方法中主要完成了以下工作:

1.创建AssetManager对象
2.调用 AssetManager.addAssetPath添加应用程序路径到资源管理框架
3.调用 AssetManager.addOverlayPath添加主题包资源

回到createResourcesImpl方法,得到AssetManager对象assets后,用assets为参数创建ResourcesImpl对象,再以这个ResourcesImpl为参数创建Resources对象,返回给ContextImpl。

从以上流程可以看到,应用资源在应用启动时,ActivityThread.getTopLevelResources过程中将应用资源以及相应主题包资源加载完成,那么framework的资源什么时候加载的呢?继续看下AssetManager的构造函数。

/frameworks/base/core/java/android/content/res/AssetManager.java

   public AssetManager() {
       synchronized (this) {
           if (DEBUG_REFS) {
               mNumRefs = 0;
               incRefsLocked(this.hashCode());
           }
           init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

    private static void ensureSystemAssets() {
        synchronized (sSync) {
            //sSystem 用来访问系统资源的AssetManager对象,再Zygote进程中创建
            if (sSystem == null) {
                AssetManager system = new AssetManager(true);
                system.makeStringBlocks(null);
                sSystem = system;
            }
        }
    }

init 调到C层AssetManager.cpp 的addDefaultAssets函数

/frameworks/base/libs/androidfw/AssetManager.cpp

static const char* kSystemAssets = "framework/framework-res.apk";
static const char* kResourceCache = "resource-cache";
bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    path.appendPath(kSystemAssets);
    //addAssetPath添加framework资源路径
    return addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);
}
bool AssetManager::addAssetPath(
        const String8& path, int32_t* cookie, bool appAsLib, bool isSystemAsset)
{
    AutoMutex _l(mLock);

    asset_path ap;

    String8 realPath(path);
    if (kAppZipName) {
        realPath.appendPath(kAppZipName);
    }
    //讲传过来的资源apk path传给asset_path对象ap
    ap.type = ::getFileType(realPath.string());
    if (ap.type == kFileTypeRegular) {
        ap.path = realPath;
    } else {
        ap.path = path;
        ap.type = ::getFileType(path.string());
        if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) {
            ALOGW("Asset path %s is neither a directory nor file (type=%d).",
                 path.string(), (int)ap.type);
            return false;
        }
    }

    // Skip if we have it already.
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = static_cast<int32_t>(i+1);
            }
            return true;
        }
    }

    ALOGV("In %p Asset %s path: %s", this,
         ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());

    ap.isSystemAsset = isSystemAsset;
    //将ap放到mAssetPaths 容器中,mAssetPaths 类型是:Vector<asset_path> mAssetPaths
    mAssetPaths.add(ap);

    // new paths are always added at the end
    //cookie用来存放当前apk的asset_path 在mAssetPaths中的索引+1 ,然后作为参数返回
    if (cookie) {
        *cookie = static_cast<int32_t>(mAssetPaths.size());
    }

#ifdef __ANDROID__
    // Load overlays, if any
    asset_path oap;
    for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) {
        oap.isSystemAsset = isSystemAsset;
        mAssetPaths.add(oap);
    }
#endif
    //调用appendPathToResTable将ap添加到资源表ResTable中
    if (mResources != NULL) {
        appendPathToResTable(ap, appAsLib);
    }

    return true;
}

这里涉及AssetManager几个总要变量:

  • mAssetPaths:存放所有资源包路径

    定义: Vector<asset_path> mAssetPaths

  • mResources:存放所有资源包的ID和资源属性对应表

    定义:ResTable mResources

至此framework-res的资源加载到资源管理器中了,前面getTopLevelResources应用调用java层addAssetPath ,最后也是通过native 层addAssetPath将应用资源加载进来, 然后继续调用addOverlayPath将该应用对应的主题包资源加载进来。其主要实现在AssetManager.cpp中的addOverlayPath。

bool AssetManager::addOverlayPath(const String8& packagePath, int32_t* cookie)
{
    const String8 idmapPath = idmapPathForPackagePath(packagePath);

    AutoMutex _l(mLock);
    //如果已经存在直接返回cookie为当前索引+1
    for (size_t i = 0; i < mAssetPaths.size(); ++i) {
        if (mAssetPaths[i].idmap == idmapPath) {
           *cookie = static_cast<int32_t>(i + 1);
            return true;
         }
     }

    ... ...

    asset_path oap;
    //主题包路径
    oap.path = overlayPath;
    //主题包类型
    oap.type = ::getFileType(overlayPath.string());
    //生成的idmap路径
    oap.idmap = idmapPath;
    //主题包信息添加至mAssetPaths
    mAssetPaths.add(oap);
    *cookie = static_cast<int32_t>(mAssetPaths.size());
    //将该asset_path添加到资源表mResources中
    if (mResources != NULL) {
        appendPathToResTable(oap);
    }

    return true;
 }

addOverlayPath 添加主题包资源跟前面addAssetPath类似

到现在应用资源、framework资源、应用对应的主题包资源(只是targetPackage="应用报名")都被加载到AssetManager资源管理框架中了。

二、替换framework资源

oldf.jpg

newf.jpg

上图中第二个图片是安装了我自己制作的framework资源包后效果,资源包中只改了字体颜色资源。

Google 原生代码是不支持主题包中资源替换framework-res.apk 中资源的,所以需要稍作改动。

(1).创建idmap 文件

前面说到安装应用主题包时,会调用到createIdmapForPackagePairLI 来创建idmap.
我们可以模仿普通应用来创建framework-res.apk对应的idmap.

 private boolean createIdmapForPackagePairLI(PackageParser.Package pkg,
            PackageParser.Package opkg) {
    ... ...
    //主题包AndroidMenifest.xml中将targetPackage设为"frameworkres",其他的也可以
    if(opkg.mOverlayTarget.equalsIgnoreCase("frameworkres")) {
        String frameworkPath = "/system/framework/framework-res.apk";
        //framework-res.apk 包名"android"
        final int sharedGid = UserHandle.getSharedAppGid(mPackages.get("android").applicationInfo.uid);
        // TODO: generate idmap for split APKs
        try {
            mInstaller.idmap(frameworkPath, opkg.baseCodePath, sharedGid);
        } catch (InstallerException e) {
            Slog.e(TAG, "Failed to generate idmap for " + pkg.baseCodePath + " and "
                    + opkg.baseCodePath);
            return false;
        }
    }
    ... ...
 }

(2).添加主题包路径到 ApplicationInfo.resourceDirs

在ActivityThread.getTopLevelResources时或者之前,或者直接在createIdmapForPackagePairLI创建idmap之后添加主题包路径到 ApplicationInfo.resourceDirs

 if(opkg.mOverlayTarget.equalsIgnoreCase("frameworkres")) {
    for (String overlayTarget : mOverlays.keySet()) {
        ArrayMap<String, PackageParser.Package> map = mOverlays.get(overlayTarget);
        if (map != null) {
            map.put(opkg.packageName,opkg);
            PackageParser.Package[] overlayArray =
            map.values().toArray(new PackageParser.Package[0]);
            if (overlayTarget.equals("frameworkres")) 
                overlayTarget = "android";
            PackageParser.Package pkg = mPackages.get(overlayTarget);
                    pkg.applicationInfo.resourceDirs = new String[overlayArray.length];
            int i = 0;
            for (PackageParser.Package p : overlayArray) {
                //将应用资源路径resourceDirs改为主题包路径
                pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
            }
        }

    }
}

(3).调用AssetManager.addOverlayPath 添加主题包信息

mContext.getResources().getAssets().addOverlayPath(opkg.baseCodePath);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容