Android减少布局层次--有关Activity根视图DecorView的思考

男儿欲遂平生志,五经勤向窗前读。这篇文章主要讲述Android减少布局层次--有关Activity根视图DecorView的思考相关的知识,希望能为你提供帮助。
1 Android应用图层
       
    一直觉得有关DecorView还是有些问题没有搞清楚,今天在看了一点有关SurfaceFlinger的内容以后,顿时突发奇想,想到之前的问题,之前的思考是:

Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


    虽然可以将DecorView作为Activity布局的父View,也就是只存在   DecorView----> ActivityLayout两层,但是经过试验还是会存在Title Bar,或者说是现在的Action Bar,尝试如下:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. protected  void  onCreate(BundlesavedInstanceState)  {             
  2.               super.onCreate(savedInstanceState);            
  3.               ViewGroup  group  =(ViewGroup)getWindow().getDecorView();    
  4.             LayoutInflater.from(this).inflate(R.layout.activity_main,  group,  true);    
  5.               ViewServer.get(this).addWindow(this);    
  6.         }   
一个简单的APP运行出来看到的View结构图就是下面这样:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


    图中左右两边相同颜色的区域是对应的,下面是详细的View tree结构图:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


可以看到即使像上面那样写,在DecorView下面有3个子View,放大看一下:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


DecorView里面包含:
(1)ActionBarOverklayLayout :  ActionBar带来的一个布局,遍布了除了第三层最上端View的所有区域
(2)RelativeLayout : Activity的Layout(上述的activity_main布局)
(3)View : 屏幕最上端状态栏的背景
    可以看到activity_main中的根布局RelativeLayout和ActionBarOverklayLayout  并列的,虽然减少了Activity中布局的层次,但是还是存在ActionBarOverklayLayout 这个我们实际上可能不需要的布局。
 
2 改进--减少层次
    上面的改法有很多的问题,首先DecorView是一个FrameLayout,里面的所有的内容简单的叠在了一起,上面的RelativeLayout里面其实放了一个TextVIew,显示“Hello world”,但是都被其余层重叠了,所以看不到(仔细看勉强能看到),顺理成章想到DecorView作为一个Viewgroup,自然能够进行View的add和remove操作,于是想把其余的都remove删掉,不就只剩下Activity的Layout,于是改了一下代码:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. protected  void  onCreate(BundlesavedInstanceState)  {             
  2.         super.onCreate(savedInstanceState);          
  3.         ViewGroup  group  =(ViewGroup)getWindow().getDecorView();    
  4.         group.removeAllViews();    
  5.       LayoutInflater.from(this).inflate(R.layout.activity_main,  group,  true);    
  6.         ViewServer.get(this).addWindow(this);    
  7. }   
结果崩溃了,信息如下:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片



    看信息好像是和ActionBar有关,但是还是没有什么头绪,于是逐行加log看看到底是在onCreate的哪一行出现这样的问题,其实看上面的崩溃信息的调用栈就知道和onCreate没有关系,应该是在onCreate的后面哪一步调用的时候出现了问题,但是还是尝试了一下:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. protected  void  onCreate(BundlesavedInstanceState)  {             
  2.               super.onCreate(savedInstanceState);    
  3.               System.out.println("onCreate0");              
  4.               ViewGroup  group  =(ViewGroup)getWindow().getDecorView();              
  5.               System.out.println("onCreate1");              
  6.               group.removeAllViews();              
  7.               System.out.println("onCreate2");              
  8.             LayoutInflater.from(this).inflate(R.layout.activity_main,  group,true);              
  9.               System.out.println("onCreate3");              
  10.               ViewServer.get(this).addWindow(this);    
  11.               System.out.println("onCreateover");    
  12. }   
    运行以后结果如下:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


    onCreate已经完整调用结束,但是比较常规使用方法,就是少了一个setContentView函数,于是先看一下getWindow().getDecorView(),根据前面的学习知道这里的getWindow()返回的是一个PhoneWindow对象,于是看一下PhoneWindow.getDecorView():
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. public  final  View  getDecorView()  {   
  2.         if  (mDecor  ==  null)  {   
  3.                 installDecor();    
  4.         }   
  5.         return  mDecor;    
  6. }   
    这里就是调用installDecor(),这个函数会调用PhoneWindow.generateDecor()和PhoneWindow.generateLayout(DecorView)函数分别创建一个DecorView对象、为DecorView加载一个系统布局,再看一下Activity.setContentView函数:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. public  void  setContentView(int  layoutResID){   
  2.       getWindow().setContentView(layoutResID);    
  3.         initWindowDecorActionBar();    
  4. }   
  先看一下这里的getWindow().setContentView(layoutResID)函数,同样这里的getWindow()返回的是一个PhoneWindow对象,看
PhoneWindow.setContentView:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. public  void  setContentView(intlayoutResID)  {   
  2.                 //  Note:  FEATURE_CONTENT_TRANSITIONSmay  be  set  in  the  process  of  installing  the  window   
  3.                 //  decor,  when  theme  attributes  and  thelike  are  crystalized.  Do  not  check  the  feature   
  4.                 //  before  this  happens.   
  5.                 if  (mContentParent  ==  null)  {   
  6.                         installDecor();    
  7.                 }  else  if  (!hasFeature(FEATURE_CONTENT_TRANSITIONS)){                   
  8.                     //FEATURE_CONTENT_TRANSITIONS这个属性以后可能会用到,   
  9.                     //这里先标记一下,注意看上面的注释   
  10.                         mContentParent.removeAllViews();    
  11.                 }   
  12.                 if(hasFeature(FEATURE_CONTENT_TRANSITIONS))  {   
  13.                         final  Scene  newScene  =   
  14. Scene.getSceneForLayout(mContentParent,layoutResID,  getContext());    
  15.                         transitionTo(newScene);    
  16.                 }  else  {   
  17.                       mLayoutInflater.inflate(layoutResID,  mContentParent);    
  18.                 }   
  19.                 final  Callback  cb  =  getCallback();    
  20.                 if  (cb  !=  null  & & !isDestroyed())  {   
  21.                         cb.onContentChanged();    
  22.                 }   
  23.         }   
  里面没有什么特别的流程,基本上也就只有installDecor()和实例化布局对象这两个调用,这里的installDecor调用和上面的效果完全是一样的,所以基本上可以排除问题不在这里。
  再往回看,Acyivity.setContentView函数除了调用了PhoneWindow.setContentView以外还调用了Acyivity.initWindowDecorActionBar函数,看函数名称这个函数调用好像是和ActionBar有关系的,刚刚好刚才的问题也是和ActionBar有关系的,问题应该就是在这里,看一下Acyivity.initWindowDecorActionBar:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


    先看注释:创建一个新的ActionBar对象,定位和实例化ActionBar对应的View,并用这个View来初始化ActionBar对象,最后将ActionBar对象的引用存储在mActionBar中。同样这里的Window是一个PhoneWindow,于是再看一下下面的几个测试条件:
(1)isChild(): 这个很简单就是判断当前的Activity有没有Parent,这个目前还没有遇到Activity有Parent,所以这一个应该是false,这个直接在Acticity.onCreate里面就可以测试
(2)window.hasFeature(Window.FEATURE_ACTION_BAR):判断当前的Activity有没有指定FEATURE_ACTION_BAR这个Feature,后面会再细说到这个,一般情况不做设置,这一个是true
(3)mActionBar: 一开始自然是null
    到这里基本上已经可以解决这个问题了,只要不创建这个WindowDecorActionBar,就不会出现与ActionBar有关的问题,只要让上面的三个判断的其中之一失效就可以直接return了,也就不会创建WindowDecorActionBar对象了,比较一下最合适的应该是window.hasFeature(Window.FEATURE_ACTION_BAR)这个判断,那现在要做的就是要让window.hasFeature(Window.FEATURE_ACTION_BAR)返回false即可,先分析一下这个Window.FEATURE_*****它的值是什么?先看一下Window.Java:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. /**  Flag  for  the  "options  panel"feature.    This  is  enabled  by  default.  */   
  2. public  static  final  intFEATURE_OPTIONS_PANEL  =  0;    
  3. /**  Flag  for  the  "no  title"feature,  turning  off  the  title  at  the  top  of  the  screen.  */   
  4. public  static  final  int  FEATURE_NO_TITLE  =1;    
  5. /**  Flag  for  the  progress  indicator  feature*/   
  6. public  static  final  int  FEATURE_PROGRESS  =2;    
  7. /**  Flag  for  having  an  icon  on  the  leftside  of  the  title  bar  */   
  8. public  static  final  int  FEATURE_LEFT_ICON  =3;    
  9. /**  Flag  for  having  an  icon  on  the  rightside  of  the  title  bar  */   
  10. public  static  final  int  FEATURE_RIGHT_ICON=  4;    
  11. /**  Flag  for  indeterminate  progress  */   
  12. public  static  final  intFEATURE_INDETERMINATE_PROGRESS  =  5;    
  13. /**  Flag  for  the  context  menu.    This  is  enabled  by  default.  */   
  14. public  static  final  intFEATURE_CONTEXT_MENU  =  6;    
  15. /**  Flag  for  custom  title.  You  cannotcombine  this  feature  with  other  title  features.  */   
  16. public  static  final  intFEATURE_CUSTOM_TITLE  =  7;    
  17. /**Flag  for  enabling  the  Action  Bar.This  isenabled  by  default  for  some  devices.  The  Action  Bar  replaces  the  title  bar   
  18.   *  and  provides  an  alternate  location  foran  on-screen  menu  button  on  some  devices.*/   
  19. public  static  final  int  FEATURE_ACTION_BAR=  8;    
  20. /**   
  21.   *  Flag  for  requesting  an  Action  Bar  thatoverlays  window  content.   
  22.   *  Normally  an  Action  Bar  will  sit  in  thespace  above  window  content,  but  if  this   
  23.   *  feature  is  requested  along  with  {@link#FEATURE_ACTION_BAR}  it  will  be  layered  over   
  24.   *  the  window  content  itself.  This  is  usefulif  you  would  like  your  app  to  have  more  control   
  25.   *  over  how  the  Action  Bar  is  displayed,such  as  letting  application  content  scroll  beneath   
  26.   *  an  Action  Bar  with  a  transparentbackground  or  otherwise  displaying  a  transparent/translucent   
  27.   *  Action  Bar  over  application  content.   
  28.   */   
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. public  static  final  intFEATURE_ACTION_BAR_OVERLAY  =  9;    
  2. public  static  final  intFEATURE_ACTION_MODE_OVERLAY  =  10;    
  3. public  static  final  intFEATURE_SWIPE_TO_DISMISS  =  11;    
  4. public  static  final  intFEATURE_CONTENT_TRANSITIONS  =  12;    
  5. public  static  final  intFEATURE_ACTIVITY_TRANSITIONS  =  13;    
    目前一共是有13个FEATURE_**,各个FEATURE_**的作用可以在用到的时候看一下注释,这里面有2点很重要:
(1)通过Window.getFeatures函数可知,实际上所有的FEATURE_**最终会集中在mFeatures这个变量里面;
(2)再看下面的hasFeature函数,里面用的是位运算判断某一位是否为1,所以上述的FEATURE_**定义的值实际上是移位运算时候的位移量;
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. public  boolean  hasFeature(int  feature)  {   
  2.         return  (getFeatures()  &   (1  < < feature))  !=  0;    
  3. }   
  在Activity中获取Activity对应的Window的mFeatures变量的值,也就是PhoneWindow的mFeatures值,但是这个值定义在Window里面,下面利用反射:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. int  getFeature(){   
  2.               String  name  ="android.view.Window";    
  3.               try  {   
  4.                       Field  field  =Class.forName(name).getDeclaredField("mFeatures");    
  5.                       field.setAccessible(true);    
  6.                       returnfield.getInt(getWindow());    
  7.             }  catch  (Exception  e)  {   
  8.                       e.printStackTrace();    
  9.               }   
  10.               return  -1;    
  11.         }   
    这样就可以获取Window的mFeatures值了。找到了问题的原因,也找到了解决的办法,下面分析该怎么写代码。把上面的异常信息在分析一下,然后总结一下:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


问题:Activity里面所有默认的创建的ActionBar的操作都是从initWindowDecorActionBar里面开始的,看上面的异常信息  问题就是如何让initWindowDecorActionBar函数里面的
      window.hasFeature(Window.FEATURE_ACTION_BAR)判断返回false;
常见有两种方法:
方法一:
  调用一下requestWindowFeature(Window.FEATURE_NO_TITLE)就行了,为什么是这样?看一下Activity.requestWindowFeature:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


      然后跳转到PhoneWindow.requestWindowFeature,列出主要的部分:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. @Override   
  2.     public  boolean  requestFeature(intfeatureId)  {   
  3.             final  int  features  =getFeatures();       //获取mFeatures   
  4.             final  int  newFeatures  =  features  |  (1< <   featureId);    
  5.             if  ((newFeatures  &   (1  < < FEATURE_CUSTOM_TITLE))  !=  0  & & (newFeatures  & ~CUSTOM_TITLE_COMPATIBLE_FEATURES)  !=  0)  {   
  6.                     throw  newAndroidRuntimeException("You  cannot  combine  custom  titles  with  other  titlefeatures");    
  7.             }   
  8.    
  9.           /*   
  10.                   1  (features  &   (1  < < FEATURE_NO_TITLE))  !=  0  :  当前没有title   
  11.                   2  (features  &   (1  < < FEATURE_NO_TITLE))  !=  0   
  12.                                               & &   featureId  ==FEATURE_ACTION_BAR  :   
  13.                   当前没有title,这时候指定ACTION_BAR没有任何用处                   
  14.           */   
  15.             if  ((features  &   (1  < < FEATURE_NO_TITLE))  !=  0  & &   featureId  ==  FEATURE_ACTION_BAR)  {   
  16.                     return  false;   //  Ignore.  No  titledominates.   
  17.             }/*   
  18.                   1  (features  &   (1  < < FEATURE_ACTION_BAR))  !=  0  :  当前有ACTION_BAR   
  19.                   2  (features  &   (1  < < FEATURE_ACTION_BAR))  !=  0   
  20.                 & &   featureId  ==FEATURE_NO_TITLE  :   
  21.                   当前有ACTION_BAR,这时候指定NO_TITLE会去除ACTION_BAR         
  22.           */   
  23.             if  ((features  &   (1  < < FEATURE_ACTION_BAR))  !=  0   
  24.                                                   & &   featureId  ==FEATURE_NO_TITLE)  {   
  25.                     removeFeature(FEATURE_ACTION_BAR);    
  26.             }   
  27.             return  super.requestFeature(featureId);    
  28.     }   


    看上面的彩色部分,当原来指定了FEATURE_ACTION_BAR,并且当前指定FEATURE_NO_TITLE的话,就会从当前的mFeatures中的FEATURE_ACTION_BAR标志位去掉,所以下次调用到initWindowDecorActionBar来初始化ActionBar的时候都不会创建ActionBar。
方法二:
    利用Theme主题,上网一搜一大把,这些主题可能满足不了我们的需求,有时候不能单单的为了一个配置项去设置一个完全没有关系的Theme,可以通过查找Theme中关于属性值的设置,然后加到自己的定义的Theme里面就可以了,下面的这些Theme,他们的关键配置属性都能在sdk里面找到,路径在:
          android sdk根路径\platforms\android-xx\data\res\values
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. android:theme="@android:style/Theme.Dialog"将一个Activity显示为能话框模式   
  2. android:theme="@android:style/Theme.NoTitleBar"不显示应用程序标题栏   
  3. android:theme="@android:style/Theme.NoTitleBar.Fullscreen"不显示应用程序标题栏,并全屏   
  4. android:theme="Theme.Light"背景为白色   
  5. android:theme="Theme.Light.NoTitleBar"白色背景并无标题栏   
  6. android:theme="Theme.Light.NoTitleBar.Fullscreen"白色背景,无标题栏,全屏   
  7. android:theme="Theme.Black"背景黑色   
  8. android:theme="Theme.Black.NoTitleBar"黑色背景并无标题栏   
  9. android:theme="Theme.Black.NoTitleBar.Fullscreen"黑色背景,无标题栏,全屏   
  10. android:theme="Theme.Wallpaper"用系统桌面为应用程序背景   
  11. android:theme="Theme.Wallpaper.NoTitleBar"用系统桌面为应用程序背景,且无标题栏   
  12. android:theme="Theme.Wallpaper.NoTitleBar.Fullscreen"用系统桌面为应用程序背景,无标题栏,全屏   
  13. android:theme="Translucent"    透明背景   
  14. android:theme="Theme.Translucent.NoTitleBar"    透明背景并无标题   
  15. android:theme="Theme.Translucent.NoTitleBar.Fullscreen"    透明背景并无标题,全屏   
  16. android:theme="Theme.Panel"      面板风格显示   
  17. android:theme="Theme.Light.Panel"平板风格显示   
        找一下Theme.NoTitleBar需要的属性,android sdk根路径\platforms\android-xx\data\res\values\themes.xml:

 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. < !--  Variant  of  {@link  #Theme_Light}with  no  title  bar  -->    
  2. < stylenamestylename="Theme.Light.NoTitleBar">    
  3.         < itemnameitemname="android:windowNoTitle"> true< /item>    
  4. < /style>    
  5.    
  6. < !--  Variant  of  {@link  #Theme_Light}that  has  no  title  bar  and   
  7.           no  status  bar.    This  theme   
  8.           sets  {@linkandroid.R.attr#windowFullscreen}  to  true.  -->    
  9. < style  name="Theme.Light.NoTitleBar.Fullscreen">    
  10.         < itemnameitemname="android:windowFullscreen"> true< /item>    
  11.         < itemnameitemname="android:windowContentOverlay"> @null< /item>    
  12. < /style>    
这样就能知道:
  去掉TitleBar的关键属性是< itemname="android:windowNoTitle"> true< /item>
  全屏的关键属性是< item name="android:windowFullscreen"> true< /item>
 
3 再次思考
    上面给出的是一个三层状态,DecorView里面包含:
(1) ActionBarOverklayLayout:  ActionBar带来的一个布局,遍布了除了第三层最上端View的所有区域
(2) RelativeLayout : Activity的Layout
(3) View : 状态栏背景
    上面直观的认为是因为ActionBar导致了ActionBarOverklayLayout的创建,根据上面的方法一再做一次尝试:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. protected  void  onCreate(BundlesavedInstanceState)  {             
  2.             requestWindowFeature(Window.FEATURE_NO_TITLE);    
  3.               super.onCreate(savedInstanceState);    
  4.               ViewGroup  group  =(ViewGroup)getWindow().getDecorView();    
  5.             LayoutInflater.from(this).inflate(R.layout.activity_main,  group,  true);    
  6.               ViewServer.get(this).addWindow(this);    
  7.         }   
再看一下布局图:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


    依旧还是3层,再想一想为什么?因为我们上面只是指定了当前的Activity不需要ActionBar,但是回忆一下最上面的Activity的View层次图,系统首先会在DecorView上面加载一个初始布局,然后把Activity的布局放到这个初始布局上id为content的位置上,上面ActionBarOverklayLayout实际上是一个自带了ActionBar布局的初始布局,现在设置了FEATURE_NO_TITLE以后,系统加载的初始布局就是这里的LinearLayout,所以这一层布局依旧还是存在的,再结合上面的removeView修改一下:
 
[html] view plain copy
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
  1. protected  void  onCreate(BundlesavedInstanceState)  {             
  2.     requestWindowFeature(Window.FEATURE_NO_TITLE);    
  3.       super.onCreate(savedInstanceState);    
  4.       ViewGroup  group  =(ViewGroup)getWindow().getDecorView();    
  5.       group.removeAllViews();    
  6.     LayoutInflater.from(this).inflate(R.layout.activity_main,  group,  true);    
  7.       ViewServer.get(this).addWindow(this);    
  8. }   
    再看一下这时候的布局图:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片
 
    出来了,这下子少了一层并列的系统层了,下面的这个View是手机顶端的信号时间之类的状态信息,经过测试,在group.removeAllViews(); 的时候并没有这个View,具体什么时候创建的就不去深究了,但是借鉴
  http://chaosleong.github.io/blog/2015/11/28/Android-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD-layout-%E8%B5%84%E6%BA%90/
    这篇文章的内容,在inflate布局实例的时候,会调用ContextThemeWrapper.getResources()和ContextThemeWrapper.getTheme()函数(Activity继承了ContextThemeWrapper),根据当前的资源和主题来实例化对象,我们只要在Application的Theme中加一个全屏配置项:
    < item  name="android:windowFullscreen"> true< /item>
然后再看一下这时候的情况:
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


    就只有这一层了,再来对比一下一般情况下设置全屏模式的布局:   
Android减少布局层次--有关Activity根视图DecorView的思考

文章图片


  复杂程度可见一斑,但是去掉前面这两层目前还不知道有没有什么问题,还需要经过一些测试。根据目前Android系统事件的传输机制来看,应该是
没有什么问题的,通过上述方式可以将我们的布局直接放到根View里面去,减少了中间的两层系统布局。
 
4 完整的demo
      https://github.com/houliang/Android-DecorView
    里面用到了ViewServer类,该类的使用方法在注释里面说的很清楚了,加上这个类就可以利用sdk根路径\tools\hierarchyviewer.bat  观察Activity的布局
【Android减少布局层次--有关Activity根视图DecorView的思考】最后注明一点:ActionBar和Activity里面放到content里面的View是相同的等级关系,互不隶属。
















    推荐阅读