Android开发之Theme、Style探索及源码浅析

============

有了上面的应用使用基础,下面的源码简单浅析可能存在跳跃性和经验性,不会像之前博客那样系统性的从头到尾进行分析,而是分点点到为止,感兴趣的同学可以自行深入研读。

3-1 追根溯源Theme、Style等根源


在我们App开发中通常我们会在新建工程后的AndroidManifest.xml文件中看见工程默认引用了应用包下自定义的主题@style/AppTheme(用法完全符合上一大节的规则)。该主题在当前应用包的style.xml中定义如下:

看着木有,它活生生的继承了Theme.AppCompat.Light.DarkActionBar这个style,这玩意又在framework的support v7包下res的themes.xml文件中,具体如下:

哈哈,原来如此,这里的Theme.Light你应该十分熟悉了吧(这就是以前我们App用的不是Support包,而是默认的时候,theme默认就是这玩意哈),这玩意就在framework的base下的themes.xml中定义着呢(所以通过了android:进行引用,留意细节吧),具体如下:

到这里我们就很容易明白啦,Theme.Light的父类原来是Theme哇,也在这个文件中,如下:

看注释吧,这货有接近400多个item属性,这也就是我们Android关于Theme的开山鼻祖了,在我们自定义时其实来这看比去API查还方便呢(其实需要两个互相配合,一个查,一个看解释,哈哈),因为它里面定义了关于我们整个应用中文字样式、按钮样式、列表样式、窗体样式、对话框样式等,这些样式都是默认样式,它还有很多我们常用的扩展样式,譬如Theme.Light、Theme.NoTitleBar、Theme.NoTitleBar.Fullscreen等等,反正你要有需求来这里搞就行。当我们继承使用时只用在前加上android:即可,有些属性可能是找不到的。同理,我们所谓的style、attr等等也都是这么个框架,大致位置也类似主题Theme的,所以这里不再过多说明,自行脑补即可。

3-2 Theme、Style等res资源客户化流程


对于纯App开发来说这一个知识点可以忽略,因为本小节需要大致了解Android源码的结构和编译框架,对于固件等开发来说这个还是比较重要的,记得以前做TV盒子开发时很多系统资源需要替换及添加,也就是说会稍微涉及到修改System UI及FW的res,那时候好坑爹,虽然修改的地方不多,只是换几个图标和加几个资源,但是那时候自己还是蒙圈了一段时间才搞明白,所以说有必要啰嗦几句。

首先我们先要明白设备里系统目录下的这些常见jar与apk的来源,如下:

| 名字 | 解释 |

| — | — |

| am.jar | 执行am命令所需的java lib,对应FW的base/cmds/am目录,具体可以参考下面的Android.mk定义。 |

| framework-res.apk | Android系统资源库集合,对应FW的core/res目录,具体同理参见Android.mk定义。 |

| framework.jar | Android SDK核心代码,对应FW的base目录,具体可以参考目录下的Android.mk的MOUDLE定义。 |

| SystemUI.apk | 从Android2.2开始状态栏和下拉通知栏被分割出一个单独的SystemUI.apk,一般在system的app或者priv-app下(还有很多其他模块呢,譬如SettingProvider等,具体可以在设备下看看),对应的源码在FW的packages下的SystemUI中。 |

| Others | 其他的jar比较多,不做一一介绍,不同厂商可能还会不同定制,具体可在厂商设备的system下看看有哪些包,对应回去通过Android.mk文件寻找即可。 |

| android.jar | 切记这个特例,这货是make sdk生成的,多方整合,别以为也可以找到对应目录,木有的!还有就是这个jar很实用的,很多时候我们想用AS直接调运系统的hide API等,自己编译一个就能派上用场啦! |

有了上边这几个和我们本文相关的核心常识后我们简单说下怎么修改编译:

  1. 修改FW/base/XXX/下面需要修改的代码;

  2. 单独在XXX下mm编译生成XXX.jar(apk);

  3. 把编译的jar(apk)包(在out目录对应路径下)push到设备系统system的FW目录下;

  4. reboot重启设备验证;

不过这里有些坑大家要明白,我们在mm前最好每次都去清除对应out/obj目录下的中间文件,特别是资源文件更新时,否则容易被坑。还有就是切记添加系统API或者修改@hide的API或者添加资源(包含添加修改public.xml等)后,需要执行make update-api命令来同步base/api下的current.txt的修改,完事再make就行啦,这些编译文档都有介绍。

有了上面这些相信大家对于客户化资源也就有了一些认识啦,想想如果我们需要用到framework.jar的hide资源或者framework-res.apk中新加的资源时又不想用反射和源码下编译怎么办?当然是编译一个no hide的jar引入我们工程即可哇,要注意我们引入以后一定是Providered的模式,也就是该jar只编译不打包入该apk,还有就是依赖的先后优先级顺序,否则又用的是sdk默认的。还有就是万能的android.jar也是一种曲线救国的办法。当然啦,如果是SDK开发则完全可以复制一份自己搞,完事编译进系统即可,同时提供给App开发。

3-3 Theme、Style加载时机及加载源码浅析


前面我们介绍了Android的Theme、Style的定义及使用及Theme、Style等res的由来,这里我们来看看这些被使用的Theme的最终是何时、怎样被加载生效的。我们都知道对于Theme有两种方式来使用,具体如下(Style等attr在View的使用也比较同类,这里只分析Theme、其他的请在View等地自行分析脑补):

  • 在AndroidManifest.xml中<application>或者<activity>节点设置android:theme属性;

  • 在Java代码中调用setTheme()方法设置Activity的Theme(须在setContentView()前设置;

可以看见,这两种方式我们都比较常用,甚至有时候还会设置Window的一些属性标记,这些标记方法都在Window类中。我们平时在设置这些Theme时总是有很多疑惑,譬如为毛只能在setContentView()前设置等等,那么下面我们就来庖丁解牛一把。故事在开始之前可能还需要你自行脑补下《Android应用setContentView与LayoutInflater加载解析机制源码分析》《Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析》两篇文章,完事再来继续下面的内容。

关于Activity通过setContentView方法设置View的来源这里就不多说了,参考前面两篇即可,我们直接跳到PhoneWindow的setContentView方法来看下,如下:

public void setContentView(int layoutResID) {

if (mContentParent == null) {

installDecor();//每个Activity第一次进来必走

} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

mContentParent.removeAllViews();

}

}

我们接着来看下installDecor()方法,如下:

private void installDecor() {

if (mDecor == null) {

//仅仅new DecorView(getContext(), -1)而已,也就是FrameLayout

mDecor = generateDecor();

}

if (mContentParent == null) {

//生成我们布局的父布局

mContentParent = generateLayout(mDecor);

// Set up decor part of UI to ignore fitsSystemWindows if appropriate.

mDecor.makeOptionalFitsSystemWindows();

final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(

R.id.decor_content_parent);

}

}

接着我们继续看看generateLayout(mDecor);这个方法,如下:

protected ViewGroup generateLayout(DecorView decor) {

// Apply data from current theme.

//获取当前主题,重点!!!!!!!

TypedArray a = getWindowStyle();

//解析一堆主题属性,譬如下面的是否浮动window(dialog)等

mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);

// Inflate the window decor.

//依据属性获取不同的布局添加到Decor

int layoutResource;

int features = getLocalFeatures();

// System.out.println(“Features: 0x” + Integer.toHexString(features));

if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {

layoutResource = R.layout.screen_swipe_dismiss;

}

View in = mLayoutInflater.inflate(layoutResource, null);

decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

mContentRoot = (ViewGroup) in;

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

return contentParent;

}

一样喽,继续先看下getWindowStyle()方法是神马鬼,这个方法在其基类Window中,如下:

/**

  • Return the {@link android.R.styleable#Window} attributes from this

  • window’s theme.

*/

public final TypedArray getWindowStyle() {

synchronized (this) {

if (mWindowStyle == null) {

mWindowStyle = mContext.obtainStyledAttributes(

com.android.internal.R.styleable.Window);

}

return mWindowStyle;

}

}

哎,没啥好看的,没有逻辑,就是流程,继续跟吧,去Context类看看obtainStyledAttributes(com.android.internal.R.styleable.Window)方法吧,如下:

/**

  • Return the Theme object associated with this Context.

*/

@ViewDebug.ExportedProperty(deepExport = true)

public abstract Resources.Theme getTheme();

/**

  • Retrieve styled attribute information in this Context’s theme. See

  • {@link android.content.res.Resources.Theme#obtainStyledAttributes(int[])}

  • for more information.

  • @see android.content.res.Resources.Theme#obtainStyledAttributes(int[])

*/

public final TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) {

//获取当前Theme对应的TypedArray对象

return getTheme().obtainStyledAttributes(attrs);

}

哎呦我去,憋大招呢,急死人了!可以看见Context的getTheme()方法时一个抽象方法,那他的实现在哪呢,看过《Android应用Context详解及源码解析》一文的同学一定知道对于Activity来说他的实现类就是ContextThemeWapprer,那我们赶紧进去看看它到底搞了啥玩意,如下:

@Override

public Resources.Theme getTheme() {

//一旦设置有Theme则不再走后面逻辑,直接返回以前设置的Theme

if (mTheme != null) {

return mTheme;

}

//没有设置Theme则获取默认的selectDefaultTheme

mThemeResource = Resources.selectDefaultTheme(mThemeResource,

getApplicationInfo().targetSdkVersion);

//初始化选择的主题,mTheme就不为null了

initializeTheme();

return mTheme;

}

@Override

public void setTheme(int resid) {

//通过外部设置以后mTheme和mThemeResource就不为null了

if (mThemeResource != resid) {

mThemeResource = resid;

//初始化选择的主题,mTheme就不为null了

initializeTheme();

}

}

我勒个去,憋大招总算憋出来翔了,ContextThemeWapprer才是重头戏啊,总算看见了光明了。这里的getTheme方法有一个判断,没有设置过Theme(mTheme为空)则通过Resources.selectDefaultTheme获取默认主题,否则用setTheme设置的主题。那么我们就来先看下假设没有设置主题,使用默认主题的方法,Resources.selectDefaultTheme如下:

/**

  • Returns the most appropriate default theme for the specified target SDK version.

    • Below API 11: Gingerbread
    • APIs 11 thru 14: Holo
    • APIs 14 thru XX: Device default dark
    • API XX and above: Device default light with dark action bar
    • @param curTheme The current theme, or 0 if not specified.

    • @param targetSdkVersion The target SDK version.

    • @return A theme resource identifier

    • @hide

    • */

      public static int selectDefaultTheme(int curTheme, int targetSdkVersion) {

      return selectSystemTheme(curTheme, targetSdkVersion,

      com.android.internal.R.style.Theme,

      com.android.internal.R.style.Theme_Holo,

      com.android.internal.R.style.Theme_DeviceDefault,

      com.android.internal.R.style.Theme_DeviceDefault_Light_DarkActionBar);

      }

      /** @hide */

      public static int selectSystemTheme(int curTheme, int targetSdkVersion, int orig, int holo,

      int dark, int deviceDefault) {

      if (curTheme != 0) {

      return curTheme;

      }

      if (targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) {

      return orig;

      }

      if (targetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {

      return holo;

      }

      if (targetSdkVersion < Build.VERSION_CODES.CUR_DEVELOPMENT) {

      return dark;

      }

      return deviceDefault;

      }

      哎呀妈呀,这不就解释了我们创建不同版本的App时默认主题不一样的原因么,哈哈,原来如果我们没有设置主题Theme,系统会依据版本给我们选择一个默认的主题,也就是上面这段代码实现了该功能。

      我们回过头继续回到ContextThemeWapprer的getTheme方法,当我们已经设置了Theme该方法就直接返回了,恰巧设置Theme的方法也在ContextThemeWapprer中。那这个方法啥时候被调运的呢?这一小节一开始我们就说了Activity的Theme设置有两种方法,主动通过Java调运setTheme()和在AndroidManifest文件配置,AndroidManifest文件配置的Theme又是啥时候调运的呢?有了前面几篇博客的铺垫,我想你也一定能找到的,就在ActivityThread的performLaunchActivity()方法中,也就是我们通过startActivity()方法启动Activity时就调运了Activity的setTheme方法,这个就不多说了,感兴趣的自己进去看下就行了,也是流程憋大招,最终调用了activity.setTheme()完成了AndroidManifest文件的Theme获取。

      我们现在把目光回到ContextThemeWapprer的setTheme或者getTheme中调运的initializeTheme()方法中来看看,如下:

      protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {

      theme.applyStyle(resid, true);

      }

      //大招!!!!!!!

      private void initializeTheme() {

      //这就解释了为何setTheme必须在setContentView前调运,不多解释了,很明白了吧!!!!!!!!

      final boolean first = mTheme == null;

      if (first) {

      mTheme = getResources().newTheme();

      Resources.Theme theme = getBaseContext().getTheme();

      if (theme != null) {

      mTheme.setTo(theme);

      }

      }

      onApplyThemeResource(mTheme, mThemeResource, first);

      }

      这个方法就解释了为何setTheme必须在setContentView前调运。最终通过onApplyThemeResource调运Resources.Theme的方法进行了设置,如下:

      /**

      • Place new attribute values into the theme. The style resource

      • specified by resid will be retrieved from this Theme’s

      • resources, its values placed into the Theme object.

      • The semantics of this function depends on the force

      • argument: If false, only values that are not already defined in

      • the theme will be copied from the system resource; otherwise, if

      • any of the style’s attributes are already defined in the theme, the

      • current values in the theme will be overwritten.

      • @param resId The resource ID of a style resource from which to

      •          obtain attribute values.
        
      • @param force If true, values in the style resource will always be

      •          used in the theme; otherwise, they will only be used
        
      •          if not already defined in the theme.
        

      */

      public void applyStyle(int resId, boolean force) {

      AssetManager.applyThemeStyle(mTheme, resId, force);

      mThemeResId = resId;

      mKey.append(resId, force);

      }

      到此注释也说明了一些概念,关于AssetManager的应用又是另一个大话题了,这里先不展开讨论,我们只用知道到此一个Theme就选择完成了,还有就是一个Theme的是怎么被选择出来的,当然对于Dialog等Window的Theme也是一个样子,这里不多说明,感兴趣的自行脑补即可。

      到现在为止我们已经找到了Theme是怎么来的了,下来我们需要回到我们这一小节开头部分的源码分析,也就是Resources的obtainStyledAttributes()方法,还记得我们最终传递了com.android.internal.R.styleable.Window进行获取该style么。这货不就是FW中res的attr.xml中自定义的属性么,如下:

      可以看见,Style、Theme其实就是一组自定义的内置在Android系统资源中的属性集合,而这里唯一比较特殊的就是这些定义的属性没有声明format字段。其实在Android中如果某个自定义属性没有声明format属性则意味着该属性已经定义过,这里只是别名而已。在哪定义的呢?当然还是attr中哇,属性么,自然只能在这了,找找看发下如下:

    上一篇:Java中停止线程的3种方式


    下一篇:博客园美化过程