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]
这些问题我们留到后续的博客更新,敬请关注。
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories