转载: http://mobile.csdn.net/a/20120302/312675.html
我先简单自我介绍一下。我叫董红光,是北航本科毕业的,所以今天来到北航的地盘感觉信心爆棚了。我现在是在小米担任MIUI系统工程师,主要负责的是MIUI主题这块,就是被很多人称为最酷最绚这一块。很多人对如何换肤很感兴趣,可能还有更多深入可以挖掘的点,所以今天跟大家分享一下。然后希望能有人受到一些启发可以做出更酷更绚的产品。
我今天的主题是MIUI主题风格,这是一种Android系统换肤工程的设计思路。
说到主题首先大家要想到主题是什么?比如说QQ有QQ的主题,Windows可以换自己的主题,比如说QQ有什么红橙黄绿青蓝紫,各种各样的主题,甚至论坛有的普通主题、文艺主题等等。主题在手机是什么样的呢?我们先回顾一个系统。
这个是塞班系统S60的界面,我现在随便在网上下载的主题包截图,我们看风格完全不同的,这个是被很多人称为明日黄花的塞班系统。
我们现在回顾一下比较火的系统,一个就是Android,一个是iOS,这些系统他们支持不支持,首先说iOS答案就是不支持,如果你要越狱还是可以做到这一点。Android支持不支持?Android其实它标称是支持的,它是怎么支持的呢?它实际上再一个XML声明写一个Style,然后在应用程序里面制定你这个主题用到什么样的Style,这个是程序显示指定的样式,就是我这个程序用Android 4.0等等一些主题风格。它能更改哪些东西?一般来说可以更改字体、颜色、长宽、间距等等这些东西,实际上真正的主题不是这些。所以说这个地方看到其实Android原生提供的对于可替换的资源类型是十分有限的。
这两个截图就是它不同主题下的展现的方式。右面那个图已经参杂了我们MIUI的元素了,比如说上面这个图表,这因为是小米手机上截屏,这个是原生的,其实它一般来说就是换一些现代颜色、背景的颜色等等这些,其实图片都可以不换的。这个就是我们目前市场占有率第一大的操作系统可以做到的效果。
刚才我说到这个是不足够的,回过头来我们说一下主题是什么东西?主题我自己认为的一点就是说所有影响用户界面效果的属性的集合都称之为主题。比如说音效、图片、切换动画都算,这些东西当然了之前提到的字体、颜色、长宽、间距等等这些,另外最重要的就是图片,其实我们看到的一个程序给大家一个整体的感受,可能更多就是它的一些背景图,按钮的样式,所以说图片这个地方是非常重要的。当然很多人是做程序开发的,大家也都懂,我们做程序开发有一个通用的办法就是把一些图片资源和样式称之为资源,在Android中刚才友盟工程师讲的非常透彻,实际上Android也有资源,实际上在我们认为系统和程序称之为资源。之前Android的一些应用程序本身是由开发者去制定的,我要用4.0,我要长成一个白色的样子,或者是黑色的样子,实际上真正需求远远不止这些。比如说我是又宅又孵的,你设计出来是比较清新的风格,我可以换一个风格搭配我的使用。第二点就是即时生效,就是说我如果换一个主题,不能说等很久,比如说Android系统是待着没事儿杀进程,这样的用户体验是很不好的。
接下来问题来了,就是怎么做?这个怎么做不是指主题包本身怎么做,因为平时我跟大家介绍我的工作的时候可能大家都会问你在做什么,我说我做主题,他们说你不是学计算机的吗?怎么回过来做美工了呢?其实不是。怎么做?有两方面,一个是主题报告怎么做,我们怎么样画出漂漂亮亮的图片,我们怎么样有各种各样的风格。另外一个方面就是我们系统如何支持他用了这样的主题,在我们的系统之上我们就能把他的图片显示出来。
首先我们就要看一下Android的资源管理机制。其实我写这张PPT的时候也很纠结,因为我不知道在座的同学们Android相关的知识多与少,因为可能有的人相关的知识很多,我介绍这些其实还是挺浪费大家时间的,不过后来我发现原来在我之前还有一个讲师,就是友盟的讲师先给大家做了一个快速的入门,我就假设大家是五十分钟精通了Android资源管理机制了,我就简单过一下,可能一些我们主题可能涉及到的一些点。
首先第一个就是资源的类型,刚才也提到了,资源的类型比如说Drawable、layout、style、string、COLOR,通过R.什么什么东西,系统同志RES这个类拿到你需要使用的东西是个颜色还是一个图片等等的。平常我们用的时候就是用Context.getresources这一类的东西,实际上它的底层就是这个。
回到刚才的问题就是我们怎么去做?首先想到第一个思路,刚才说了这么一大堆东西,之前友盟工程师也花了将近一个小时的时间讲我们整套的机制,大家首先想到的是什么?就是复杂。相当的复杂和麻烦,我第一个解决方法可不可以绕过这套复杂的机制,答案是可以。就是直接读取外部资源文件,你这一面是用R又是用RES什么的,我不用这些东西,你给我提供了Drawable就可以读取,比如说你写了一个按钮,按钮的背景你需要运行初始化的时候去传一个图片或者是传一个颜色。其实这个缺点就出来了,就是说满屏幕你为主题、你为你的皮肤都要写类似的代码。
看一下它的特点,首先第一点就是主题包格式非常灵活,就是说这个格式完全你自己去定义,你愿意定义成一个文件夹放所有也没问题,你愿意定义成自己压缩的格式也都没有问题,反正解析的操作是你自己来做的。
第二点刚才我也提到了,就是你自己需要手动管理,你在你的程序中你要去设置。不过它也有优点,它的优点就是可控换肤。比如说搜狗的输入法是支持皮肤的,他的皮肤可能只允许你换一部分东西,如果不是这样的话,我把搜狗的LOGO换成百度了,主题应用完了之后就成了百度输入法,这肯定不是搜狗想看到的,所以这个有很大的优点,就是LOGO不允许你去换。
下面一个缺点是什么呢?就是没有办法给系统和其他的程序进行换肤,为什么这么说呢?就是因为所有的代码都是写在你自己的程序中,你不可能影响到系统,也不可能影响其他的应用程序。所以最终我们觉得这个东西只适合APP级别的换肤。
说了这样一个思路,至少我们已经打开了一点思路了,我们再回头分析一下它。我们看到这么多东西,有什么东西可以改进,首先一点我们看到这个手动解析资源,可能大家不是很明白这是什么意思,其实Android的那一套资源管理有自己的组织方式,然后它也提供了相应的解析逻辑,如果你自己去指定这个主题包,你自己要去解析这个逻辑,大家说没有问题,反正我自己定,但是有些问题,比如说Android最常见就是可以给不同的机型运行程序的,可能有的机型分辨率很高,有的机型分辨率很低,有的及其DPI很高,有的是DPI很低的,这样Android提供了一套机制就是说在Drawable下面挂了很多的目录,这些目录分别去放为每种分辨率设计的它的图片,因为你可能高分屏要设计质量比较高的图片,低分屏要设计质量比较低的图片这很正常。所以这一部分东西,Android会帮你去做这些事情。比如说你是在一个低分屏下,但是你的程序并没有给低分屏做图片,不代表它压根不显示这个图片,Android会给你算,会优先读这些资源,这些事情Android已经默默帮我们做了,我们想抛弃它没有这么简单的。
我就在想,能不能把手工解析这个地方去掉,我让Android还去帮我做这个事情,但是我不让他帮我管理加载哪些资源,这就是我们的思路二。说来简单,但是我们需要找到技术支持,搜遍了SDK的方法终于找到了一个,就是PackageManager,比如说购物桌面,我没有看到它的代码,我觉得它的机制是这样的,就是它可以读其他第三方的一些为它做的一些主题,而其他第三方都是一些APK格式,装到你的系统中,它会通过这个方式,把第三方的资源全都弄进来,他所用的就是RES,而且这个东西并不受签名的影响,因为它是可以跨不同的作者、不同的签名去访问的。
拿到了它之后,我们成功一半了,因为拿到了其他人的RES,我们可以拿GET Drawable等等的,接下来就是我们自己去做的,如果有主题文件那么我们就去那个里面去找,如果没有主题文件我们就从我们自己里去拿。它的相对于前一种思路的优点出来了,首先它的包就是APK包,它的缺点就是制作门槛比较高一点,因为APK还涉及到打包、签名一些操作,可能一般的平面、UI设计师他们不会做这些东西,他们最多打一个技术包,系统会帮你解析这些所有的资源。
下面这些特点其实和上面的差不太多,手动管理、可控换肤,而且它也没有办法给系统和其他换肤,所以最后这个东西只是适合给APP级别做换肤。
如何才能真正实现Android系统换肤?
其实我们努力了半天,想了两种办法都没有做到给系统级别或者给其他的APP换肤。首先想到的一个就是偷换概念,什么叫偷换概念呢?你帮我买一个小米一代,我就给你拎一袋小米回去。实际上他管我要的是某一个资源ID,但是我假装不知道,我给他是另外一个,这个地方大家可以很容易想到,为什么我们可以做?资源是有规律的。O1开头的都是系统的ID,这样我们就可以做一些事情了,就是说我发现如果是0X01开头,我把这位从1变成7,我自己做一个APK,然后里面它的ID和这个是一一对应的,只是前面这个开头是不一样的,这是可以做得到的,这样别人给我要这个资源ID我就给他返回另外一个。而这个是要和PackageManager合作的。它的特点就出来了。下面就是自动管理、自动换肤,这个时候你的应用程序不需要感知我怎么样拿皮肤相关的一些东西了,我只要管RES去要,我要某一个ID的RES,这个时候我自己去解析一个映射文件,我把他的ID映射到那个文件资源ID,所以可以实现自动换肤。
有了这样一个特点之后,就可以系统换肤了,其实我们改的也是系统代码,因为我们是做ROOM的,所以有了Android的代码还是可以改一些东西的,所以我们改的是RES。但是它也可以为其他的APP换肤,但是比较麻烦。还是回到R的机制上来说,R本身是由APP编出来的,系统的还好,系统在编的时候还会出现一个文件囊,他会把系统常用暴露出来的ID,这样你影射很好影射,但是如果你没有这样的ID,你增加资源和删除资源这个ID会变的,变了就映射错了,你明明是为0X01做的,但是这面0X01已经变了,这个有很大的风险。在资源ID变化的时候它的解决是很麻烦的。
另外一种就是资源缺失的时候,我们做了什么?就是把这个由1换成7,读完了以后重新返回一个数据,你原来的资源在新的主题包没有怎么办?你可能判断一下这个主题包有没有,有的话再返回这个,没有你就返回原生里面了,所以还要做一些额外的处理。
这个看起来是可行的,但是有很多麻烦的地方,而且为第三方APP换肤麻烦这是我们当时考虑到最重要一个缺点,所以说我们把它砍掉了。砍掉了之后怎么办呢?再往底层走。
再往底层走RES这一种方式不行,下一就是AssetManager,我们也看了AssetManager的代码,它有一个AddAssetPath,每一个程序启用的时候,就是用它路径实现过来,然后把自己的RES加进去,一般是这两个,当然额外也有别的情况。
比如说这样一个东西以后我们想到了,对于系统换肤来说,第一个特点这个主题包必须是APK的可以自动解析,可以自动管理,也可以实现自动换肤,这个最主要一点就是可以为系统和其他的APP换肤,就是我直接把原来的APK换掉了,任何一个系统或者是一个APP资源都是存在APK里面,我把APK替换了。
在底层整合过程中也许会出现一些问题,主题包资源缺失时怎么办?某些地方出现空白甚至图片无法显示时怎么办?
实际上两种办法,一种办法就是资源缺失在做主题的时候先把之前所有的这些全都解开,开开之后我需要哪一个就用哪一个,全部打包过去,这个对于主题包的负担很重的。第二个办法就是我在最底层做,在上层也要做,我一旦资源缺失了我再到新地方去找,这个改动是很大很大的,因为上层的话不会判断同样的ID怎么去处理了,因为你可能他俩虽然是不同的APK,但是他们两个同样一个资源共用一个ID,这个时候系统处理不了,我们还要进行各种各样的处理,很麻烦。这种办法也不是很好。有没有更好的办法呢?
刚才说的是对整个资源包进行一个重新设计,接下来就想我们可不可以为单独某一个文件进行设计,实际上我要的是一个小米手机,我回头其实给了你一个大米手机,你这面其实要是人的话肯定不行了,是你坑我,但是实际上程序是可以的,因为程序它是非常信任这个Framework,所以在这个地方我要还是要原来的东西。我要了原来的东西,我在RES这一层,我直接不读APK,我到一个新的地方去读,这是在思路三和思路四之间一个折中。思路三是我改你的资源ID,思路四就是我不改你的ID,我把底层全改掉,现在这个折中办法出来了,就是我不改你的ID,但是我对你的底层进行管理,你可以自己定一套格式,所以既支持手动又支持自动的解析资源。它的特点就是自动管理、自动换肤,同时也可以为系统和其他APP换肤,因为所有的APP都要通过RES这个入口去访问资源。这个时候资源一旦缺失的时候,它的解决方法非常简单,因为我们在RESGET的时候GET不到我就什么都不做就直接返回。
这个是我刚才提到五种思路,这个地方列了一个表,大概对比了一下。第一排就是主题包的格式,这三种是APK,这两种是任意,也可以用APK的方式,APP对显式管理,前两种是是,后两种是否。系统换肤前两者不支持,最后三种是支持。
在资源缺失时MIUI又会采取何种策略呢?
MIUI怎么做呢?其实刚才也分析了半天了,给了大家五种解决方法,我们采用思路5,重定向资源文件路径,就是把入口里面的逻辑改了,我们的入口就是RES类,我们更改它,截获所有资源的请求,首先在我自己里面判断,如果有就返回,如果没有交给系统去做。我们的主题包是自定义格式了,我们通过一些优化做一些处理。我们还是为了方便起见,我们每个安排APP一个资源包,主题包没有资源就返回原生资源。
这是很简单的一个结构图,APP通过RES这是我们改过的接口,现在这个地方加了一个HOOK调动我们自己的代码,背后相对有一套逻辑,但是这个地方只有一行,加进来我们去判断是不是存在,如果存在就这一个地方去找,找到就返回,如果不存在就交给原来的。
刚才说到主题包,分析一下这个主题包格式,主题包格式不是APK,但是实际上我们为了方便主题方式有点类似,我们也是用ZIP包的格式,但是不用签名也不需要变异相关的操作,很简单就是一个ZIP,ZIP里面是一个局部项目集合,这个局部项目很多了,有一方面是大家经常用到的一些东西,比如说图表、壁纸、来电铃声、开机动画和音乐这些东西,这些东西走的不是那些机制,这些东西都有自己独特的方式,如果有时间我可以继续给大家讲。其他的一些资源就是我们刚才所提到的比如说你给QQ换肤,这个是以APP为单元组织的,每个APP是一个小的主题包,这个主题包它也是一个ZIP格式,它里面包含了哪些呢?一方面就是Drawable,就是刚才说到的图片,它的层次结构也是一样的,Drawable下面如果你给高分屏做了一些东西,你就放在Drawable—HDPI,这个也是为了让我们通过Android一些东西去获取,不需要自己去算,考虑到这个问题。
刚刚提到颜色等等都统一在THEME values.XML,这个也是考虑到主题制作者对这一套结构完全不感冒,他们觉得怎么简单怎么来。很多人原来就没有编程的知识。
我们看一下THEME—Values.XML是什么东西。书写方式与Android的COLORS.XML dimens.XML Strings.XML,对于程序制作者来说是完全透明的,我们只是解析它做一个替换。
刚才提到这一个,可能如果要是做Android开发比较多的有一个问题就是说其实你这个东西局限性很大了,比如说你REFERENCE类型是不支持的,你也不支持多值属性,比如说我把一个ARROYS、style里面的数值替换掉这个也不支持。我想把自己设计一个风格支持不支持?不支持。我们考虑到主要是一个BAR原则,大部分APP视觉的元素和主题的元素大部分定义在刚才说到的这些地方,比如说COLOR这个是比较常用的,另外就是图片,所以我们做了一些取舍,反正这个地方也是可以做的,只是说这个地方的代码不能拿Android自己的代码了,我们可能还要自己重新写,重新解析。
大家说你不支持多值属性,layout你最得支持吧?我这个布局、大框架、上面是抬头、下面是按钮,这个我们也可以做到,但是我们不支持。为什么不支持呢?还是涉及到一个安全的考虑。比如说他随便调整了一个位置,他把确定和取消给换了一个位置,可能大家觉得没什么,但是使用者会觉得特别扭,你不小心点了弹出框架你习惯性点右边就不好了,所以这个地方利用不好就会有很多潜在的风险。比如说他改了一些拨号键盘的位置,1234567890完全错开了位置,程序就没法用了。
说到刚才主题包的格式以及它的局限性,接下来说主题包怎么解析。主题包解析两大部分刚才也提到了,第一部分就是独立的元素,图表、字体、壁纸、音效、开机动画等等,这些有Android的接口就可以像图表、壁纸、音效,但是有的得改更底层的代码,比如说字体和开机动画这些东西。
我们看其他的APP包,我们一次性解析THEME—VALUES的所有值,为什么一次性把所有东西都加进来,也可以做到你用什么就加什么,但是一个正常的主题包为一个APP设计的更改的样式不是特别多,所以我们一次性解析了可能提高效率。
接下来就是Drawable,图片不能一次性都解析,这个地方跟Android是一样的,它只有在使用的时候才去进行加载,加载之后Android的方式实际上在GETDrawable读一个图片的时候,它有一个缓存一个表,它会看一下这个地方是否之前已经加载过了,如果没有加载过他再去加载,实际上我们这个地方就是,如果没有加载过,我们先用我们的东西去加载,加载不到再用他的去加载。加载完了之后他会统一把这个加载之后的东西放在系统缓存中,下次再访问的时候就会很快。这是他的机制我们充分利用了他的机制。当然我们也可以在系统缓存之上自己去建立一套自己的缓存机制也是可以的,但是当时我们觉得没有必要的。
刚才说到的一点就是每个APP实际上每个都是ZIP包,ZIP包打开非常耗时,所以我们对于每个程序来说它其实用到APP包是有限的,用到的资源包是有限的,我们会有一个文件池,这个文件池会存他所有需要用到的APP包的距离,这个也是最近经常使用的方式这样一套逻辑,如果不常使用的东西就直接释放掉了。
刚才说到了那些东西大家可能觉得说我们整个主题的功能应该基本上可以满足了,其实也差不多可以满足大部分功能,但是还有一些点是需要考虑的,首先第一个点就是局部更改全局样式,这是什么意思呢?比如说短信里面经常用到系统的一些资源,它的列表它可能用系统的一些资源,但是我为短信专门做了一个皮肤,这个时候我不希望把系统整个所有的列表全都换掉,如果都换掉因为我只给短信做了这点东西,你把短信换了,其他程序就不搭了,我们引用了一个OVERLAY机制,还有主PACKAGE这个概念,主PACKAGE本身是一个主题包,这个主题包中如果有什么东西我是系统或者是其他第三方APP,但是我不希望他们用到,我直接定义在这里面,这里面是一个目录,然后用同样一个结构,然后我们会优先去看,他自己有没有这样一个资源,如果自己有这个资源我就优先加载这个,如果自己没有这个资源我再加载系统主题的图表,这个就是主PACKAGE的概念。
第二个相对更深入一点就是说家单说一点MIUI这边是分几块,一个是原生资源,就是Android给了我们这些图和样式,但是MIUI大家不知道用没用过,如果用过它和原生还是不太一样,我们修改很多东西,这个其实是我们定义到自己的资源里面去了。接下来就是用户他可能要去换这些主题,换这些图片等等这些东西,但是我们又不希望说他那个主题包没做到就去读原生,我们不希望做到这个,我们希望是如果他没有了读这个,所以这个地方引入了分层概念,就是主题包实际上分多层的,每次找的时候是从上到下一直找,如果没有就用原生。不同的层级可以指定接受资源类型,这个不详细展开了。
接下来刚才提到了有一点,怎么去更改,第一个是用户指定,我们刚才说到就是可以做到了,就是用户指定把一个主题包扔过来,你就帮我显示,另外一点就是即时生效,我怎么做到我应用之后它就会生效。回顾一下刚才五种思路,其实这五种思路都有一个共同存在的问题就是资源的刷新问题,实际上我换了这个资源我期待着系统是把这个图片给我换了,但是其实不是这样的,系统做了很多资源访问的优化,他有大量的缓存,还有不同层次的缓存,比如说APP级别还有预加载的资源,这些预加载的资源其实是不同进程共享的物理空间,还有大量的缓存怎么办?我们要清掉它,清掉它怎么办?一般通用的办法很简单就是杀进程,杀进程你起来以后会重读你的资源,但是这些值是适用于这个层次,而预加载这些东西是不变的,所以你再杀进程,这个进程起来以后读的东西还是来的。最后我们没有办法只能重启手机,我们MIUI当时主题风格最开始那版一直沿用的方法,因为没有找到太多太好的办法。当然了后来有一天我们找到了一个宝贝。叫Configuration,这个是什么呢?横屏竖屏我可能有不同的布局,横竖屏一切换就会帮你重新更改数据,或者是键盘显隐,这个就是在Configuration去实现。如果你们不去做这些东西,系统很简单,没什么别的,充气你的Activity,这个Activity是一个站。有了这样一个东西,其实我们当时就觉得黑夜中的一盏明灯,我们由此想到给主题定义一个新的Configuration类型,这个问题就来了,实际上大家写程序,当然我们是很希望以后大家都用MIUI的SDK开发,但是很可惜不是,大家一般写程序还是基于Android原生的开发,这样不会管我们新定义的Configuration,他也不会去注册,我们自己去维护这个Configuration,我们自己判断你换完了之后,比如说你现在在用短信,用到我这个新的主题包没有短信,我们不需要重启,也不需要管理了,如果你主题包有短信,没办法我重启你的Activity,这样就会重新读资源,当它去读的时候,这个时候由我们自己的代码去接管,这时候就可以读到新的资源了。这个地方就是我们资源刷新的机制。
两点第一点用户自己制定,第二点即时刷新。这个就是我们核心的两个功能点。最后大家可以看到我们可以实现五彩斑斓的主题风格。