您的位置:首页 > 移动开发 > Android开发

CyanogenMode主题模块解析(上)

2016-02-29 19:49 555 查看
   

MIUI有个主题模块,这块肥肉可是把我馋死了,奈何在问了度娘,谷爹后,均无大的成果,但是却也知道了MIUI的前

身是本文的主角CyanogenMode,于是我就sync了CyanogenMode的代码,发现果然有主题模块。这下可把我乐坏了,毛
爷爷说过:"自己动手,丰衣足食"。于是这系列的文章就出来了,旨在记录自己所走的路,也存了份共享的心思,好了废话不
多说,进入我们的主题。

既然要分析一个模块,得找一个突破口!习惯使然,我找了系统中的一个切换主题的DEMO。我们看看它是如何实现切

换主题的。

1.路径:packages/apps/ThemeChooser/src/org/cyanogenmod/theme/chooser/ChooserDetailFragment.java
public class ChooserDetailFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, ThemeChangeListener {
...
private ThemeManager mService;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
...
mApply = (Button) v.findViewById(R.id.apply);
mApply.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
ThemeChangeRequest request = getThemeChangeRequestForSelectedComponents();
mService.requestThemeChange(request, true);//④
mApply.setText(R.string.applying);
}
});
...

mService = (ThemeManager) getActivity().getSystemService(Context.THEME_SERVICE);//①
}

@Override
public void onResume() {
super.onResume();
if (mService != null) {
mService.onClientResumed(this);//③
}
refreshApplyButton();
}

@Override
public void onPause() {
super.onPause();
if (mService != null) {
mService.onClientPaused(this);
}
}

@Override
public void onDestroy() {
super.onDestroy();
if (mService != null) {
mService.onClientDestroyed(this);
}
}

private ThemeChangeRequest getThemeChangeRequestForSelectedComponents() {
// Get all checked components
ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
for (Map.Entry<String, CheckBox> entry : mComponentToCheckbox.entrySet()) {
String component = entry.getKey();
CheckBox checkbox = entry.getValue();
if (checkbox.isEnabled() && checkbox.isChecked()
&& !mAppliedComponents.contains(component)) {
builder.setComponent(component, mPkgName);//②
}
}
builder.setRequestType(RequestType.USER_REQUEST);
return builder.build();
}
...
}


设置一个新的Theme,主要有以下步骤:

[b]获取ThemeManager对象,如1.①
[/b]
[b]获取新的Theme的信息,主要有component(模块名),mPkgName(主题包名),如1.②
[/b]
[b]调用ThemeManager::onClientResumed设置监听Theme变化状态的ThemeChangeListener,如1.③
[/b]
[b]调用ThemeManager::requestThemeChange开始设置Theme,如1.④
[/b]

如此一来,突破口算是找到了,我们看到最后就是ThemeManager::requestThemeChange完成了切换主题操作的。

我尝试着,小心翼翼的"扯开"了ThemeManager的真面目。

从1.①可以看到,ThemeManager对象的获取跟Android系统原生的其他manager是如此的相似,那么它跟系统中其他

Manager的管理应该是相似的。直接杀到ContextImpl一看便知。

2.路径:framework/base/core/java/android/app/ContextImpl.java
static {
...
registerService(APPWIDGET_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
IBinder b = ServiceManager.getService(APPWIDGET_SERVICE);
return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b));
}});

registerService(THEME_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
IBinder b = ServiceManager.getService(THEME_SERVICE);
IThemeService service = IThemeService.Stub.asInterface(b);
return new ThemeManager(ctx.getOuterContext(),
service);//①
}});

...
}

...
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}


最后我们得到的是这样的一个实例:new ThemeManager(Context,ThemeService)。原来,ThemeManager包装了

ThemeService代理端。发往ThemeManager的所有的请求,都是通过ThemeService完成的。

3.路径:framework/base/core/java/android/content/res/ThemeManager.java
3.路径:framework/base/core/java/android/content/res/ThemeManager.java

public class ThemeManager {
...
public void addClient(ThemeChangeListener listener) {
synchronized (mChangeListeners) {
if (mChangeListeners.contains(listener)) {
throw new IllegalArgumentException("Client was already added ");
}
if (mChangeListeners.size() == 0) {
try {
mService.requestThemeChangeUpdates(mThemeChangeListener);
} catch (RemoteException e) {
Log.w(TAG, "Unable to register listener", e);
}
}
mChangeListeners.add(listener);
}
}

public void removeClient(ThemeChangeListener listener) {
synchronized (mChangeListeners) {
mChangeListeners.remove(listener);
if (mChangeListeners.size() == 0) {
try {
mService.removeUpdates(mThemeChangeListener);
} catch (RemoteException e) {
Log.w(TAG, "Unable to remove listener", e);
}
}
}
}

public void onClientPaused(ThemeChangeListener listener) {
removeClient(listener);
}

public void onClientResumed(ThemeChangeListener listener) {
addClient(listener);
}

...

public void requestThemeChange(ThemeChangeRequest request, boolean removePerAppThemes) {
try {
mService.requestThemeChange(request, removePerAppThemes);
} catch (RemoteException e) {
logThemeServiceException(e);
}
}

...

}


可以看到ThemeManager的所有请求都是通过ThemeService来完成的,如:

[b]ThemeManager::onClientResumed最后调用的是ThemeService::requestThemeChangeUpdates
[/b]
[b]ThemeManager::requestThemeChange最后调用的是ThemeService::requestThemeChange
[/b]

我们回头看看1.④:mService.requestThemeChange(request,true),这个请求,最终会被ThemeService接收并且处理掉,

这也是在这里完成系统主题的切换的。

在查看mService.requestThemeChange具体实现之前,我们有必要看下,一个主题包到底长什么样?

主题包解压后:





assets目录





overlays目录



下面是我切换某个主题时,1.②处会封装的一些参数:

ThemeChooser: component = mods_status_bar,mPkgName= com.lex.theme.lessugly.cm12

ThemeChooser: component =mods_navigation_bar,mPkgName = com.lex.theme.lessugly.cm12

ThemeChooser: component =mods_homescreen,mPkgName = com.lex.theme.lessugly.cm12

ThemeChooser: component =mods_overlays,mPkgName = com.lex.theme.lessugly.cm12

ThemeChooser: component =mods_fonts,mPkgName = com.lex.theme.lessugly.cm12

ThemeChooser: component =mods_ringtones,mPkgName = com.lex.theme.lessugly.cm12

结合主题包内容看,应该不难发现,我们切换一个主题时,会把该主题包支持的所有换肤模块的信息打包起来。比如如果主题

支持图标替换,那么就会封装如下信息:

Key: mods_icons value:themePkgName

并且在之后的解析过程中会将主题包的assets/icons目录下的图标解析并且打包到缓存目录中。那么这些信息会以什么样的形

式来封装呢?我们走进1.②来看下:

4.路径:framework/base/core/java/android/content/res/ThemeChangeRequest.java
public final class ThemeChangeRequest implements Parcelable {

public static class Builder {//一个建造者
Map<String, String> mThemeComponents = new HashMap<String, String>();
...
public Builder setComponent(String component, String pkgName) {
if (pkgName != null) {
mThemeComponents.put(component, pkgName);
} else {
mThemeComponents.remove(component);
}
return this;
}
...
}

}


主题包的信息都会以 模块名:主题包名 键值对的形式保存在一个mThemeComponentsHashmap中。随后ThemeService在切换主

题时,会从mThemeComponents取出可换肤的各个模块对应的主题包名,那么够参与主题切换的模块都有哪些呢? 它们都定义
ThemesContract.java中。

5.路径:framework/base/core/java/android/provider/ThemesContract.java
public class ThemesContract {
...
/**
* 1 if theme modifies the launcher/homescreen else 0
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_LAUNCHER = "mods_homescreen";

/**
* 1 if theme modifies the lockscreen else 0
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_LOCKSCREEN = "mods_lockscreen";

/**
* 1 if theme modifies icons else 0
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_ICONS = "mods_icons";

/**
* 1 if theme modifies fonts
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_FONTS = "mods_fonts";

/**
* 1 if theme modifies boot animation
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_BOOT_ANIM = "mods_bootanim";

/**
* 1 if theme modifies notifications
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_NOTIFICATIONS = "mods_notifications";

/**
* 1 if theme modifies alarm sounds
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_ALARMS = "mods_alarms";

/**
* 1 if theme modifies ringtones
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_RINGTONES = "mods_ringtones";

/**
* 1 if theme has overlays
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_OVERLAYS = "mods_overlays";

/**
* 1 if theme has an overlay for SystemUI/StatusBar
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_STATUS_BAR = "mods_status_bar";

/**
* 1 if theme has an overlay for SystemUI/NavBar
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_NAVIGATION_BAR = "mods_navigation_bar";

/**
* 1 if theme has a live lock screen
* <P>Type: INTEGER</P>
* <P>Default: 0</P>
*/
public static final String MODIFIES_LIVE_LOCK_SCREEN = "mods_live_lock_screen";

...
}


接下来我们回到1.④,看看ThemeService:: requestThemeChange。

6.路径:framework/base/services/core/java/com/android/server/ThemeService.java
public class ThemeService extends IThemeService.Stub {

...
private class ThemeWorkerHandler extends Handler {
private static final int MESSAGE_CHANGE_THEME = 1;
private static final int MESSAGE_APPLY_DEFAULT_THEME = 2;
private static final int MESSAGE_REBUILD_RESOURCE_CACHE = 3;

public ThemeWorkerHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_CHANGE_THEME:
Log.i(TAG,"RECEIVE MESSAGE_CHANGE_THEME");
final ThemeChangeRequest request = (ThemeChangeRequest) msg.obj;
doApplyTheme(request, msg.arg1 == 1);
break;
case MESSAGE_APPLY_DEFAULT_THEME:
doApplyDefaultTheme();
break;
case MESSAGE_REBUILD_RESOURCE_CACHE:
doRebuildResourceCache();
break;
default:
Log.w(TAG, "Unknown message " + msg.what);
break;
}
}
}

...
private class ResourceProcessingHandler extends Handler {
private static final int MESSAGE_QUEUE_THEME_FOR_PROCESSING = 3;
private static final int MESSAGE_DEQUEUE_AND_PROCESS_THEME = 4;

public ResourceProcessingHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_QUEUE_THEME_FOR_PROCESSING:
String pkgName = (String) msg.obj;
synchronized (mThemesToProcessQueue) {
if (!mThemesToProcessQueue.contains(pkgName)) {
if (DEBUG) Log.d(TAG, "Adding " + pkgName + " for processing");
mThemesToProcessQueue.add(pkgName);//③
if (mThemesToProcessQueue.size() == 1) {
this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME);
}
}
}
break;
case MESSAGE_DEQUEUE_AND_PROCESS_THEME:
Log.i(TAG,"RECEIVE MESSAGE_DEQUEUE_AND_PROCESS_THEME");
synchronized (mThemesToProcessQueue) {
pkgName = mThemesToProcessQueue.get(0);
}
if (pkgName != null) {
if (DEBUG) Log.d(TAG, "Processing " + pkgName);
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);
}

Log.i(TAG,"RECEIVE MESSAGE_DEQUEUE_AND_PROCESS_THEME END");
break;
default:
Log.w(TAG, "Unknown message " + msg.what);
break;
}
}
}

...

public void systemRunning() {
...

processInstalledThemes();//①

...
}

...

private void processInstalledThemes() {

final String defaultTheme = ThemeUtils.getDefaultThemePackageName(mContext);
Message msg;
// Make sure the default theme is the first to get processed!
if (!ThemeConfig.SYSTEM_DEFAULT.equals(defaultTheme)) {
msg = mHandler.obtainMessage(
ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING,
0, 0, defaultTheme);
mResourceProcessingHandler.sendMessage(msg);
}
// Iterate over all installed packages and queue up the ones that are themes or icon packs
List<PackageInfo> packages = mPM.getInstalledPackages(0);
for (PackageInfo info : packages) {
if (!defaultTheme.equals(info.packageName) &&
(info.isThemeApk || info.isLegacyIconPackApk)) {
msg = mHandler.obtainMessage(
ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING,
0, 0, info.packageName);
mResourceProcessingHandler.sendMessage(msg);
}
}
...

@Override
public boolean processThemeResources(String themePkgName) throws RemoteException {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_THEME_MANAGER, null);
try {
mPM.getPackageInfo(themePkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
// Package doesn't exist so nothing to process
return false;
}
// Obtain a message and send it to the handler to process this theme
Message msg = mResourceProcessingHandler.obtainMessage(
ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING, 0, 0, themePkgName);
mResourceProcessingHandler.sendMessage(msg);
return true;
}

...
@Override
public void requestThemeChange(ThemeChangeRequest request, boolean removePerAppThemes)
throws RemoteException {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_THEME_MANAGER, null);
Message msg;

/**
* Since the ThemeService handles compiling theme resource we need to make sure that any
* of the components we are trying to apply are either already processed or put to the
* front of the queue and handled before the theme change takes place.
*
* TODO: create a callback that can be sent to any ThemeChangeListeners to notify them that
* the theme will be applied once the processing is done.
*/
synchronized (mThemesToProcessQueue) {
Map<String, String> componentMap = request.getThemeComponentsMap();
for (Object key : componentMap.keySet()) {
if (ThemesColumns.MODIFIES_OVERLAYS.equals(key) ||
ThemesColumns.MODIFIES_NAVIGATION_BAR.equals(key) ||
ThemesColumns.MODIFIES_STATUS_BAR.equals(key) ||
ThemesColumns.MODIFIES_ICONS.equals(key)) {
String pkgName = (String) componentMap.get(key);
if (mThemesToProcessQueue.indexOf(pkgName) > 0) {//②
mThemesToProcessQueue.remove(pkgName);
mThemesToProcessQueue.add(0, pkgName);
// We want to make sure these resources are taken care of first so
// send the dequeue message and place it in the front of the queue
msg = mResourceProcessingHandler.obtainMessage(
ResourceProcessingHandler.MESSAGE_DEQUEUE_AND_PROCESS_THEME);
mResourceProcessingHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
}
msg = Message.obtain();
msg.what = ThemeWorkerHandler.MESSAGE_CHANGE_THEME;
msg.obj = request;
msg.arg1 = removePerAppThemes ? 1 : 0;
mHandler.sendMessage(msg);
}

...

}


在切换主题时,首先会检查被切换的主题包有没有被解析,如果没有被解析,这该主题包会被加入到负责主题包解析的

ResourceProcessingHandler的队列中,并且会放在队列的头部,以便该主题包能够第一时间被解析。随后就会通知负责
主题切换的ThemeWorkerHandler切换主题包。

这里,6.②处的判断一般都会是false,所以请求ThemeService切换主题时,一般都是直接通知ThemeWorkerHandler去切

换主题的,为什么这么说?如果这个时候不解析主题,那么主题又是什么时候被解析的呢?主题又是怎么被解析的呢?

先回答第一个问题:

主题包解析时机有两个:1.在系统启动完毕时,会加载所有的主题包 2.当安装一个主题包时,安装过程中就会解析该包。

开机解析主题的代码如下:

mActivityManagerService.systemReady(new Runnable() {
@Override
public void run() {
…
try {
// now that the system is up, apply default theme if applicable
if (themeServiceF != null) themeServiceF.systemRunning();
ThemeConfig themeConfig =
ThemeConfig.getBootTheme(context.getContentResolver());
String iconPkg = themeConfig.getIconPackPkgName();
mPackageManagerService.updateIconMapping(iconPkg);
} catch (Throwable e) {
reportWtf("Icon Mapping failed", e);
}
…
}
});


安装过程中主题包解析代码如下:
public class PackageManagerService extends IPackageManager.Stub {
...
class PackageHandler extends Handler {
...
void doHandleMessage(Message msg) {
...
POST_INSTALL: {
...
if(res.pkg.mIsThemeApk || res.pkg.mIsLegacyIconPackApk) {
processThemeResourcesInThemeService(res.pkg.packageName);
}
...
}
...
}
...
}
...

private void processThemeResourcesInThemeService(String pkgName) {
ThemeManager tm =
(ThemeManager) mContext.getSystemService(Context.THEME_SERVICE);
if (tm != null) {
tm.processThemeResources(pkgName);
}
}

...
}


第二个问题:一个主题包是怎么被解析的呢?

我们先看下,解析后的主题包长什么样子(以某个主题包解析完毕后的输出作为例子):

缓存目录(/data/resource-cache/themePkgName/)




icons目录



app主题目录,以com.android.systemui为例:



从一个被解析的主题包内容中我们可以看到,一个图标资源被解析后,会生成hash文件和一个资源apk,一个app主题包被

解析后,会生成一个idmap文件以及一个资源apk。这些都是可以帮组我们分析代码的。

前面提到ResourceProcessingHandler专门负责主题包的解析的,我们看到6.⑤,最后的解析是交给pm来做的。我们看看

PackageManagerService::processThemeResources的实现。

@Override
public int processThemeResources(String themePkgName) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_THEME_MANAGER, null);
PackageParser.Package pkg = mPackages.get(themePkgName);
if (pkg == null) {
Log.w(TAG, "Unable to get pkg for processing " + themePkgName);
return 0;
}
//解析图标资源
// Process icons
if (isIconCompileNeeded(pkg)) {
try {
ThemeUtils.createCacheDirIfNotExists();
ThemeUtils.createIconDirIfNotExists(pkg.packageName);
compileIconPack(pkg);
} catch (Exception e) {
uninstallThemeForAllApps(pkg);
deletePackageX(themePkgName, getCallingUid(), PackageManager.DELETE_ALL_USERS);
return PackageManager.INSTALL_FAILED_THEME_AAPT_ERROR;
}
}

// Generate Idmaps and res tables if pkg is a theme
Iterator<String> iterator = pkg.mOverlayTargets.iterator();
while(iterator.hasNext()) {
String target = iterator.next();
Exception failedException = null;
try {
compileResourcesAndIdmapIfNeeded(mPackages.get(target), pkg);
} catch (IdmapException e) {
failedException = e;
} catch (AaptException e) {
failedException = e;
} catch (Exception e) {
failedException = e;
}

if (failedException != null) {
Slog.w(TAG, "Unable to process theme " + pkg.packageName + " for " + target,
failedException);
// remove target from mOverlayTargets
iterator.remove();
}
}

return 0;
}


这段代码首先会判断是否需要解析图标资源,通过检查该主题对应的缓存目录中icons目录中hash文件存储的hashcode是

否等于主题包的hashcode。如果不相等或者hash文件读取出错,则表示需要解析图标资源。随后,会调用compileIconPack
去解析图标资源,解析会输出一些文件,比如hash文件以及资源索引文件。之后就开始解析所有的app的主题包,每个app
的主题包的解析都是通过compileResourcesAndIdmapIfNeeded完成的,最终会输出一些文件,比如idmap文件以及资源
索引文件(存放在resource.apk里面)。

资源的解析过程,其实就是aapt的一个打包过程,已经有大牛解析过了,我这里推荐大家可以看下老罗的文章,来看看aapt

到底是如何打包资源文件的。-- [b]Android应用程序资源的编译和打包过程分析[/b]

至此,ThemeService对资源包的解析就完成了,现在有如下问题抛出来:

[b]系统是什么时候以及怎么加载图标资源的?
[/b]
[b]启动一个Activity后,如何启用主题包的资源?
[/b]
[b]ThemeService是怎么完成切换主题的操作的?[/b]

这些问题我们留到后续的博客更新,敬请关注。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息