简述:
一直希望有个机会可以好好研究一下android手机的多主题功能,借此机会将自己所能分析到的内容记录一下,防止以后遗忘。
目前大多数厂商的手机都具备的切换主题的功能,以一个apk的形式将所有资源打包,切换主题时会动态提醒每个应用的资源管理器,将需要使用的资源信息添加进去并重新刷新界面,使其能够使用已经替换过的资源,从而达到整个ROM UI风格的更新。
目前所有的学习都是基于魔趣OS的多主题功能框架代码,先看下一个主题 apk中主要目录结构:
- Samsung_theme.apk
从上图可以看出所有资源都在assets目录下,该APK安装后会同时修改壁纸、铃声、app皮肤、锁屏壁纸和应用图标等,接下是围绕着多主题中’ Icon ‘资源的分析。
学习目的:
通过研究多主题框架代码,进一步了解AAPT工具打包apk流程。
整体架构:
分析完整个编译流程和查找资源的流程,画了张图:
上图为本人对整个多主题-Icon资源的理解,暂时先这样,可能画的不够详细,有不对的地方还望大侠们指出。
通过上图可知,Icon资源包的创建可分为两步:
第一步:安装主题APK,路径: ’ /data/data/包名/base.apk ‘;
第二步:主题APK安装完毕时PMS
会接受到广播,紧接着通过AAPT工具为刚才安装的’ 主题apk ‘打包一个”resource.apk”,我把它简称为“索引apk”。
资源的查找这边不花时间赘述,可以通过上图了解到下大体的查找流程。
打包流程:
由于对 c/c++ 代码不熟,百度了一点知识(呵呵哒)。在走到 c++ 流程时记得尽量详细一些,错误的地方还请大拿们指出。先放出整个资源打包的流程图,流程对我而言是相当复杂繁琐,而打包的流程是有两个入口:
1. 当主题包安装完成时,PMS
会对主题包进行二次打包处理,为资源创建新的资源索引包;
2. 当开机完成,系统服务准备就绪,PMS
会立即检查当前应用中的主题,如有必要会重新通过aapt工具打包。
下面的流程图是从安装主题包完成时开始分析(图太大,需要单独查看)。
一、主题包安装完成
step 1 - 3 :
先列出这部分所有类的位置 :
- source\frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java (简称 PMS
)
- source\packages\appsThemeManagerService\src\com\gome\themeservice\ThemeManagerService.java (简称 TMS
)
- source\frameworks\base\services\core\java\org\cm\platform\internal\ThemeManagerServiceBroker.java (简称 TMSBroker
)
当主题包安装完成,PMS
会接受到一个’ POST_INSTALL ‘的 Handle
消息,在接下的方法中先判断当前 apk 包安装成功与否,再接着对当前的包信息进行判断,如果当前 apk 是主题包就进入特殊处理。PMS
会将主题包的包名传递给TMS
服务,从而开始进行下一步处理。
(TMS
服务并非是系统服务,坐落于app层。TMSBroker
为系统服务,是 TMS
的在 framework 层的服务代理,所有与app层的交互都是通过 TMSBroker
来代理)
private void handlePackagePostInstall(PackageInstalledInfo res, boolean grantPermissions,
boolean killApp, String[] grantedPermissions,
boolean launchedForRestore, String installerPackage,
IPackageInstallObserver2 installObserver) {
if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
...
// if this was a theme, send it off to the theme service for processing
if(res.pkg.mIsThemeApk || res.pkg.mIsLegacyIconPackApk) {
processThemeResourcesInThemeService(res.pkg.packageName);
}
...
}
private void processThemeResourcesInThemeService(String pkgName) {
IThemeService ts = IThemeService.Stub.asInterface(ServiceManager.getService(
MKContextConstants.MK_THEME_SERVICE));
if (ts == null) {
Slog.e(TAG, "Theme service not available");
return;
}
try {
ts.processThemeResources(pkgName);
} catch (RemoteException e) {
/* ignore */
}
}
step 4 - 7 :
TMS
拿到包名后会重新调用PMS
的 processThemeResources
方法继续走打包流程,如果编译成功TMS
会将这些信息收集起来并做其他处理。此时上层主题的操作交由TMS
的来处理,而具体打包流程由PMS
来进行。TMS
这块本章不做任何介绍,接来下看看PMS
如何工作。
private class ResourceProcessingHandler extends Handler {
...
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
...
case MESSAGE_DEQUEUE_AND_PROCESS_THEME:
...
if (pkgName != null) {
String name;
try {
PackageInfo pi = mPM.getPackageInfo(pkgName, 0);
name = getThemeName(pi);
} catch (PackageManager.NameNotFoundException e) {
name = null;
}
//走打包流程
int result = mPM.processThemeResources(pkgName);
if (result < 0) {
postFailedThemeInstallNotification(name != null ? name : pkgName);
}
//根据打包结果处理上层逻辑
sendThemeResourcesCachedBroadcast(pkgName, result);
synchronized (mThemesToProcessQueue) {
mThemesToProcessQueue.remove(0);
if (mThemesToProcessQueue.size() > 0 &&
!hasMessages(MESSAGE_DEQUEUE_AND_PROCESS_THEME)) {
this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME);
}
}
//提醒打包完成
postFinishedProcessing(pkgName);
}
break;
}
}
}
二、为索引 apk 的打包作准备
step 8 - 15 :
这部分主要根据当前主题包名来判断是否需要进一步的打包处理,判断依据就是读取对应目录下’ hash ‘值,并与当前主题包 ’ versionCode ‘作对比,一致则不需要再次编译。当一个主题包首次安装时,对应目录下是不存在这些缓存文件的。而在每次开机时会动态检查缓存,如果之前不慎删除了缓存,此时才会重新打包。
缓存目录(以 Samsung_theme.apk 为例) : ” /data/resource-cache/com.wsdeveloper.galaxys7/icons/ ”
该目录下就存在两个文件,’ hash ‘文件用来检验主题包的合法性,而’ resources.apk ‘才是这次需要重点研究的对象,可以参考上面的资源架构图来理解。
从下面的代码可知,如果需要编译索引包,就先根据包名创建对应的缓存目录。
@Override
public int processThemeResources(String themePkgName) {
...
// Process icons
if (isIconCompileNeeded(pkg)) {
try {
ThemeUtils.createCacheDirIfNotExists();
ThemeUtils.createIconDirIfNotExists(pkg.packageName);
//开始准备创建“icon”目录下的两个文件
compileIconPack(pkg);
} catch (Exception e) {
//出现异常就立即将主题包卸载
uninstallThemeForAllApps(pkg);
deletePackageX(themePkgName, getCallingUid(), PackageManager.DELETE_ALL_USERS);
return PackageManager.INSTALL_FAILED_THEME_AAPT_ERROR;
}
}
// 以下是关于 overley 资源的编译流程,暂不记录
....
return 0;
}
这个方法主要为了创建零时的’ Manifest ‘文件和 ’ hash ‘文件,’ resources.apk ‘的打包流程接着往下分析。
private void compileIconPack(Package pkg) throws Exception {
if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, " Compile resource table for " + pkg.packageName);
OutputStream out = null;
DataOutputStream dataOut = null;
try {
//创建临时 AndroidManifest.xml 文件
createTempManifest(pkg.packageName);
//创建"icon/hash"文件
int code = pkg.mVersionCode;
String hashFile = ThemeUtils.getIconHashFile(pkg.packageName);
out = new FileOutputStream(hashFile);
dataOut = new DataOutputStream(out);
dataOut.writeInt(code);
//准备编译 resources.apk
compileIconsWithAapt(pkg);
} finally {
IoUtils.closeQuietly(out);
IoUtils.closeQuietly(dataOut);
cleanupTempManifest();
}
}
准备使用aapt命令打包资源,为了能够适配多主题,CM修改了aapt工具的部分流程,使之适应主题索引包的编译。工具打包时,上层传入的参数主要有四个:
- 主题资源包路径(pkg.baseCodePath): /data/app/com.wsdeveloper.galaxys7-1/base.apk
- 索引包路径(resPath):/data/resource-cache/com.wsdeveloper.galaxys7/icons
- 资源前缀(APK_PATH_TO_ICONS): assets/icons/
- 图标资源包 package id(Resources.THEME_ICON_PKG_ID) : 98
描述一下,在查找主题资源时,’ package id ’ 用于定位索引包位置;索引包路径用于获取单一资源的’ 相对路径 ‘(这里只讨论图片资源);此时资源管理器会通过 ’ 主题资源包路径 ’ 来解压’ base.apk ‘,并通过’ 资源前缀+相对路径 ‘得到资源在’ base.apk ‘中的绝对位置,得到包中的所有文件资源,从而获取到资源返回给上层的Resources
。
private void compileIconsWithAapt(Package pkg) throws Exception {
String resPath = ThemeUtils.getIconPackDir(pkg.packageName);
final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid);
try {
if (mInstaller.aapt(pkg.baseCodePath, APK_PATH_TO_ICONS, resPath, sharedGid,
Resources.THEME_ICON_PKG_ID,
pkg.applicationInfo.targetSdkVersion,
"", "") != 0) {
throw new AaptException("Failed to run aapt");
}
} catch (InstallerException ignored) {
}
}
step 16 - 20:
先列出这部分所有类的位置 :
- source\frameworks\base\services\core\java\com\android\server\pm\Installer.java
- source\frameworks\base\core\java\com\android\internal\os\InstallerConnection.java
- source\frameworks\native\cmds\installd\installd.cpp
具体代码不全贴了,都是逻辑上的处理,简要概括下:Installer
重组 aapt
参数接着传给 InstallerConnection
来继续工作,而InstallerConnection
负责与 installd
服务进程 进行通讯,将aapt
命令跨进程传送 installd
服务,由native
层来执行命令。可以通过下面大神的博客详细了解一下native
层的installd
服务进程:
http://blog.csdn.net/yangwen123/article/details/11104397
installd
服务进程的启动时在Android启动脚本’ init.rc ‘中通过服务配置的,而PMS
是通过套接字的方式访问 installd
服务,在以下代码中可以了解大概流程,如果没有连接则先尝试连接socket
,再将aapt
指令通过socket
发送给installd
服务。
public synchronized String transact(String cmd) {
...
//尝试连接 installd 服务的socket,
if (!connect()) {
return "-1";
}
//发送cmd指令
if (!writeCommand(cmd)) {
if (!connect() || !writeCommand(cmd)) {
return "-1";
}
}
...
}
此时接受到来自PMS
传来的指令,接下来开始执行打包指令。
static int installd_main(const int argc ATTRIBUTE_UNUSED, char *argv[]) {
...
//自installd服务启动后,会一直等待来自PMS的 socket 数据流
for (;;) {
alen = sizeof(addr);
s = accept(lsocket, &addr, &alen);
for (;;) {
...
//buf 将装载 aapt 指令每个参数
if (execute(s, buf)) break;
}
}
}
step 21 - 25:
先列出这部分所有类的位置 :
- source\frameworks\base\services\core\java\com\android\server\pm\Installer.java
- source\frameworks\native\cmds\installd\commands.cpp
这边通过打印可以查看指令字串:
aapt /data/app/com.wsdeveloper.galaxys7-1/base.apk assets/icons/ /data/resource-cache/com.wsdeveloper.galaxys7/icons 50089 98 0
下面代码中将执行 cmdsinfo
中对应的函数,arg
数组保存在所有参数的地址,接下来看下cmdsinfo
数组中的各个位对应的函数。
/* Tokenize the command buffer, locate a matching command,
* ensure that the required number of arguments are provided,
* call the function(), return the result.
*/
static int execute(int s, char cmd[BUFFER_MAX])
{
...
for (i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
if (!strcmp(cmds[i].name,arg[0])) {
if (n != cmds[i].numargs) {
ALOGE("%s requires %d arguments (%d given)\n",
cmds[i].name, cmds[i].numargs, n);
} else {
//ALOGE("wangjian func arg + 1 :%d reply :%s "(arg + 1),reply.string());
ret = cmds[i].func(arg + 1, reply);
}
goto done;
}
}
多主题功能在 cmds
中添加了两个对应参数 aapt
和 aapt_with_common
,也就是说刚才输入的参数指令,会调用 cmds[19]
中对应的 do_aapt
函数。在do_aapt
函数也是做了很多判断和过滤,主要功能代码在run_aapt
函数中。
struct cmdinfo cmds[] = {
...
{ "aapt", 7, do_aapt },
{ "aapt_with_common", 8, do_aapt_with_common },
...
};
static int do_aapt(char **arg, char reply[REPLY_MAX] __unused)
{
return aapt(arg[0], arg[1], arg[2], atoi(arg[3]), atoi(arg[4]), atoi(arg[5]), arg[6], "");
}
函数中考虑情况太多,我只贴出用到的主干部分代码,这边打印出各个参数的值:
- source_apk : /data/app/com.wsdeveloper.galaxys7-1/base.apk
- internal_path : assets/icons/
- out_restable : /data/resource-cache/com.wsdeveloper.galaxys7/icons
- uid : 50089
- pkgId : 98
- min_sdk_version : 0
- app_res_path : null
- common_res_path : null
此时 app_res_path
和 common_res_path
值是为空的,因此下面函数中不走的代码都注释掉了。最后执行了 execl 函数,定义在 unistd.h
头文件。从代码中得知,最终会通过 execl 函数来启动 aapt tools 工具,具体 aapt tools 功能入口的代码在源码中的位置 : ’ /frameworks/base/tools/aapt/Main.cpp ‘。
static void run_aapt(const char *source_apk, const char *internal_path,
int resapk_fd, int pkgId, int min_sdk_version,
const char *app_res_path, const char *common_res_path)
{
static const char *AAPT_BIN = "/system/bin/aapt";
...
static const size_t MAX_INT_LEN = 32;
char resapk_str[MAX_INT_LEN];
char pkgId_str[MAX_INT_LEN];
char minSdkVersion_str[MAX_INT_LEN];
bool hasCommonResources = (common_res_path != NULL && common_res_path[0] != '\0');
bool hasAppResources = (app_res_path != NULL && app_res_path[0] != '\0');
if (hasCommonResources) {
...
} else {
// 执行"/system/bin/"目录下的aapt工具, 其功能代码在framework/base/tools/aapt/下
execl(AAPT_BIN, AAPT_BIN, "package",
"--min-sdk-version", minSdkVersion_str,
"-M", MANIFEST,
"-S", source_apk,
"-X", internal_path,
"-I", FRAMEWORK_RES,
"-r", resapk_str,
"-x", pkgId_str,
"-f",
hasAppResources ? "-I" : (char*)NULL,
hasAppResources ? app_res_path : (char*) NULL,
(char*)NULL);
}
ALOGE("execl(%s) failed: %s\n", AAPT_BIN, strerror(errno));
}
step 26 - 28:
先列出这部分所有类的位置 :
- source\frameworks\base\tools\aapt\Main.cpp
从这部分开始,主题的打包工作正式交由 aapt tools工具来执行。首先由入口函数 Main()
将打包的一系列参数全都封装进Bundle
中,结合 (step 21 - 25 )代码可以知道每个命令对应参数代表的意思。最后由 handleCommand()
函数来分配任务。
/*
* Parse args.
*/
int main(int argc, char* const argv[])
{
char *prog = argv[0];
Bundle bundle;
...
/* 设置默认的压缩方式 */
bundle.setCompressionMethod(ZipEntry::kCompressDeflated);
...
if (argv[1][0] == 'v')
...
// package 对应功能是打包资源的操作,kCommandPackage 对应 doPackage 函数
else if (argv[1][0] == 'p')
bundle.setCommand(kCommandPackage);
else {
goto bail;
}
/*
* Pull out flags. We support "-fv" and "-f -v".
*/
while (argc && argv[0][0] == '-') {
/* flag(s) found */
const char* cp = argv[0] +1;
while (*cp != '\0') {
switch (*cp) {
case '-':
if (strcmp(cp, "-debug-mode") == 0) {
...
} else if (strcmp(cp, "-min-sdk-version") == 0) {
...
bundle.setMinSdkVersion(argv[0]); //设置最小sdk版本
}
break;
case 'M':
...
bundle.setAndroidManifestFile(argv[0]);
break;
case 'S':
...
bundle.addResourceSourceDir(argv[0]); // 设置主题包资源 apk 的绝对路径
break;
case 'X':
...
bundle.setInternalZipPath(argv[0]); // 设置主题查找路径前缀
break;
case 'I':
bundle.addPackageInclude(argv[0]);// 设置原生资源包“framework-res.apk”的绝对路径
break;
case 'r':
...
bundle.setOutputResApk(argv[0]);// 设置主题“索引包”的绝对路径
break;
case 'x':
...
bundle.setExtendedPackageId(atoi(argv[0]));//设置主题包“索引包”的 package ID
break;
default:
goto bail;
}
...
bundle.setFileSpec(argv, argc);
result = handleCommand(&bundle);
return result;
}
/*
* Dispatch the command.
*/
int handleCommand(Bundle* bundle)
{
switch (bundle->getCommand()) {
...
case kCommandPackage: return doPackage(bundle);
...
default:
return 1;
}
}
三、aapt 工具打包
step 29 - 72:
先列出这部分所有类的位置 :
- source\frameworks\base\tools\aapt\Command.cpp
- source\frameworks\base\tools\aapt\AaptAssets.cpp
- source\frameworks\base\tools\aapt\AaptConfig.cpp
- source\frameworks\base\tools\aapt\Resource.cpp
- source\frameworks\base\tools\aapt\ResourceTable.cpp
- source\frameworks\base\tools\aapt\ApkBuilder.cpp
- source\frameworks\base\tools\aapt\Package.cpp
- source\frameworks\base\tools\aapt\ZipEntry.cpp
- source\frameworks\base\tools\aapt\OutputSet.cpp
- source\frameworks\base\tools\aapt\ZipEntry.cpp
- source\frameworks\base\tools\aapt\StringPool.cpp
- source\frameworks\base\libs\androidfw\AssetManager.cpp
前面所有的步骤都是在为这部分服务,用一句话概括下面要分析函数就是:打包索引apk。但是过程相当复杂,关键代码都需要分将近40个步骤来记录。
结合上部分代码可以了解到 Bundle
中保存的变量有哪些,也可以查看下图(红框框起来的参数不需要考虑)。即将要分析的是这些变量在打包过程中的作用,整个打包的流程全部都在函数 doPackage
中完成的。
在分析 doPackage
详细工作之前需要了解一下 索引apk 中有那些文件需要打包。从’ 图6 ’ 可以看出 apk 中存在三个资源文件,’ AndroidManifest.xml ’ 和 ’ appfilter.xml ’ 文件最终会以二进制方式打包进apk中,现在只分析 ’ resources.arsc ‘的生成过程,’ resources.arsc ‘可以理解为资源索引表,其表结构的描述可以提前看下下面大神的博客,具体生成过程还得看流程:
https://www.jianshu.com/p/3cc131db2002