android setContentView()源码解析

Posted 史大拿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android setContentView()源码解析相关的知识,希望对你有一定的参考价值。

系统:mac

android studio: 4.1.3

kotlin version:1.5.0

gradle: gradle-6.5-bin.zip

看完本篇你讲学会什么?

  • setContentView() 如何解析View
  • LayoutInflater 什么时候初始化,在什么地方?
  • LayoutInflater 如何加载View
  • Factory2和Factory的作用
  • Factory2什么时候初始化?
  • appcompat1.2 和 appcompat1.3的区别
  • AppCompatViewInflater 如何改变View
  • AppCompat主题和Material主题对普通View的区别
  • 如何自己解析View(Activity / Fragment)
  • onCreate中不调用super.onCreate()为什么会报错

高温预警!

AppCompat主题material主题

1.setContentView() 如何解析View

这段源码在View生命周期中就有提到过,但是还不够细致,本篇带你完全理解!

从入口开始:

override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_custom_parse)
    

代码块1.1:

#AppCompatActivity.java

@Override
public void setContentView(@LayoutRes int layoutResID) 
    getDelegate().setContentView(layoutResID);


@NonNull
public AppCompatDelegate getDelegate() 
  if (mDelegate == null) 
    // 执行到了这里,创建AppCompatDelegate
    mDelegate = AppCompatDelegate.create(this, this);
  
  return mDelegate;

tips: 其实不会执行AppCompatDelegate.create() 因为此时mDelegate已经!=null了,后面会说在什么时候初始化的,这里就先以他会null来说

代码块1.2:

#abstract AppCompatDelegate.java
@NonNull
public static AppCompatDelegate create(@NonNull Activity activity,
        @Nullable AppCompatCallback callback) 
     // AppCompateDelegate 是一个抽象类
  	 // 实现类为 AppCompateDelegateImpl
    return new AppCompatDelegateImpl(activity, callback);

接着执行代码块1.1的setContentView(),

就会执行到AppCompateDelegateImpl.setContentView()方法

代码块1.3

# AppCompatDelegateImpl.java
@Override
public void setContentView(int resId) 
    // 解析主题属性等 并且调用Window#setContentView()方法
    ensureSubDecor();
  
    // android.R.id.content为screen_simple.xml中的id
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  
    contentParent.removeAllViews();
  
	  // 解析View
    LayoutInflater.from(mContext).inflate(resId, contentParent);
  
    // 空方法代表解析View完成
    mAppCompatWindowCallback.getWrapped().onContentChanged();

这段代码有很多博主直接进入LayoutInflater#inflate中解释,直接略过了最精彩的 ensureSubDecor()方法,现在来看看吧~

代码块1.4

# AppCompatDelegateImpl.java
private void ensureSubDecor() 
    if (!mSubDecorInstalled) 
        mSubDecor = createSubDecor();
				...
    

代码块1.5

# AppCompatDelegateImpl.java
Window mWindow;

private ViewGroup createSubDecor() 
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

    // 解析主题,设置样式
    if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false))
      else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) 
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false))
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) 
    a.recycle();
     .... 

    // 初始化DecorView
    mWindow.getDecorView();
    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;
	  if (!mWindowNoTitle) 
      if (mIsFloating) 
        ...
       else if (mHasActionBar) 
         // 会走这里.. 可以自行打断点看看.
         subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);
      
    
  
	  .... 省略了大量代码 ...
		// 初始化主界面
    mWindow.setContentView(subDecor);

    ...

    return subDecor;

这段代码前半部分主要是解析样式

期间会创建一个ViewGroup,这个ViewGroup的布局为:

这个东西是什么不重要,重要的是往**Window#setContentView()**中传的布局不是自己的布局

而是系用的布局!

接下来 主要代码是调用**Window#setContentView()**方法,来初始化主界面

众说周知,Window是PhoneWindow,在 Activity#attatch中初始化的

那么直接走到了PhoneWindow#setContentView()

代码块1.6

# PhoneWindow.java
  
@Override
public void setContentView(View view) 
  setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));


@Override
public void setContentView(View view, ViewGroup.LayoutParams params) 
    if (mContentParent == null) 
        // szj加载DecorView 初始化 mContentParent
        installDecor();
     
  ...
    mContentParent.addView(view, params);
  ...

这里要看仔细了, 传过来的是View,所以执行的是setContentView(View)

千万别跑到setContentView(id)上!

代码块1.7

# PhoneWindow.java
  
private void installDecor() 
    mForceDecorInstall = false;
    // 在代码块1.5中已经通过 mWindow.getDecorView();方法初始化过了,所以这里不执行
    if (mDecor == null) 
        // szj初始化DecorView
        mDecor = generateDecor(-1);
        ...
     else 
        mDecor.setWindow(this);
    
    
	  // szj初始化主界面布局
    if (mContentParent == null) 
        mContentParent = generateLayout(mDecor);
    

代码块1.8

# PhoneWindow.java
  
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

// 初始化主界面布局
protected ViewGroup generateLayout(DecorView decor) 
    TypedArray a = getWindowStyle();
    // 初始化Window样式等
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) 
      requestFeature(FEATURE_NO_TITLE);
     
    // 这里有很多初始化的方法..
  
    // 用来区分主界面布局
    int layoutResource;
  	if(...)else if(...)
    else 
      // 一般没有设置的情况下,主界面都是这一个
      layoutResource = R.layout.screen_simple;
    
    // 吧布局解析添加到DecorView上 
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
 	  return contentParent;

来看看 R.layout.screen_simple 布局是什么样子的:

Tips: R.layout.screen_simple需要下载android源码,我是下载android11的源码

那么终于可以找到了id为content的了

到此时,系统界面就初始化好了

接下来退回到起点,再来看看

代码块1.3

# AppCompatDelegateImpl.java
@Override
public void setContentView(int resId) 
    // 解析主题属性等 并且调用Window#setContentView()方法
    ensureSubDecor();
  
    // android.R.id.content为screen_simple.xml中的id
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  
    contentParent.removeAllViews();
  
	  // 解析View
    LayoutInflater.from(mContext).inflate(resId, contentParent);
  
    // 空方法代表解析View完成
    mAppCompatWindowCallback.getWrapped().onContentChanged();

在ensureSubDecor() 方法中,我们会初始化系统的布局

初始化完系统的布局后,我们获取到 R.id.content 也就是screen_simple.xml 中的FrameLayout

在由FrameLayout作为ViewGroup初始化我们的布局

开始执行 LayoutInflater.from(mContext).inflate(resId, contentParent);

走到这里先停一下, 我们先看LayoutInflater在什么时候初始化,然后在进行布局解析!

LayoutInflater 什么时候初始化,在什么地方?

代码块2.1:

// 通过form实例一个LayoutInflater
public static LayoutInflater from(Context context) 
    // szj 获取LayoutInflater实例
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) 
        throw new AssertionError("LayoutInflater not found.");
    
    return LayoutInflater;

这里通过context.getSystemService() 初始化LayoutInflater,

我们知道 context实现类为ContextImpl, 在ActivityThread.java 中初始化(这里就不展开了)

那么我们直接到ContextImpl去找getSystemService() 方法

代码块2.2:

# ContextImpl.java
@Override
public Object getSystemService(String name) 
    if (vmIncorrectContextUseEnabled()) 
        
    //szj走到了这里。。
    return SystemServiceRegistry.getSystemService(this, name);

代码块2.3:

# SystemServiceRegistry.java
public static Object getSystemService(ContextImpl ctx, String name) 
    if (name == null) 
        return null;
    
    // 在 SYSTEM_SERVICE_FETCHERS中找
    final ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    if (fetcher == null) return null;
  
    // 然后在ServiceFetcher中去找服务
    final Object ret = fetcher.getService(ctx);
    if (ret == null) return null;
    return ret;

代码块2.4:

那我们只要看看SYSTEM_SERVICE_FETCHERS是什么就OK了

# SystemServiceRegistry.java
private static final Map<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
        new ArrayMap<String, ServiceFetcher<?>>();

SYSTEM_SERVICE_FETCHERS是一个全局静态map,并且是不可改变的

那么直接在静态代码块中找即可

# SystemServiceRegistry.java
  
 static 
  // 注册LayoutInflater服务
  registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                  new CachedServiceFetcher<LayoutInflater>() 
                    @Override
                    public LayoutInflater createService(ContextImpl ctx) 
                      return new PhoneLayoutInflater(ctx.getOuterContext());
                    );
   
    // 注册window服务
   registerService(Context.WINDOW_SERVICE, WindowManager.class,
                   new CachedServiceFetcher<WindowManager>() 
                     @Override
                     public WindowManager createService(ContextImpl ctx) 
                       return new WindowManagerImpl(ctx);
                     );
  ......
 

private static <T> void registerService(@NonNull String serviceName,
            @NonNull Class<T> serviceClass, @NonNull ServiceFetcher<T> serviceFetcher) 
        SYSTEM_SERVICE_CLASS_NAMES.put(serviceName, serviceClass.getSimpleName());
    

可以看到所有的服务都在这里注册的.

那么就可以知道: LayoutInflater是在SystemServiceRegistry静态代码块初始化的

好了,对LayoutInflater有一个基本的了解后,就接着代码块1.3开始解析布局吧

代码块1.3

# AppCompatDelegateImpl.java
@Override
public void setContentView(int resId) 
    // 解析主题属性等 并且调用Window#setContentView()方法
    ensureSubDecor();
  
    // android.R.id.content为screen_simple.xml中的id
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  
    contentParent.removeAllViews();
  
	  // 解析View
    LayoutInflater.from(mContext).inflate(resId, contentParent);
  
    // 空方法代表解析View完成
    mAppCompatWindowCallback.getWrapped().onContentChanged();

LayoutInflater#.inflate(id, ViewGroup);

代码块1.3.1

# LayoutInflater.java
  
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) 
    return inflate(resource, root, root != null);

  • @param resource: 自己布局的id(R.layout.activity_main)
  • @param root: 系统的FrameLayout

代码块1.3.2

# LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 
    final Resources res = getContext().getResources();
   
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) 
        return view;
    
    // XmlResourceParser看名字就知道用来解析XML文件
    XmlResourceParser parser = res.getLayout(resource);
    try 
      	// 接着执行这里
        return inflate(parser, root, attachToRoot);
     finally 
        parser.close();
    

代码块1.3.3

# LayoutInflater.java
  
private static final String TAG_MERGE = "merge";
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 
    synchronized (mConstructorArgs) 
        ...
        // 保存传进来的view
        View result = root;

        try 
            // 找到root标签 如果找不到就直接报错
            advanceToRootNode(parser);

            // 获取到这个root标签name
            final String name = parser.getName();

            // 判断是否是 merge标签
            if (TAG_MERGE.equals(name)) 
                // 直接加载View,忽略merge标签
                rInflate(parser, root, inflaterContext, attrs, false);
             else 
                // 通过标签来解析View
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) 
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) 
                       // 设置ViewLayoutParams
                        temp.setLayoutParams(params);
                    
                
									
                // 解析childView
                rInflateChildren(parser, temp, attrs, true);

                // 最终解析完成后添加到root中
                if (root != null && attachToRoot) 
                    root.addView(temp, params);
                
            

          catch (Exception e) 
          ...
         

        return result;
    

这里都好理解,传过来一个view,通过 createViewFromTag()解析,

解析完成之后添加到View上就完成了

那么来看看createViewFromTag() 解析View的方法

代码块1.3.4

# LayoutInflater.java
  
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) 
    return createViewFromTag(parent, name, context, attrs, false);



View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                           boolean ignoreThemeAttr) 
        
        try 
            // 尝试创建View  
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) 
              // 判断是不是自定义View 
              // 1.自定义View在布局文件中是全类名
              // 2.而系统的View则不是全类名
              

以上是关于android setContentView()源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Android 通知 setContentView

Android 9 (API 28) 活动在启动时崩溃 (setContentView)

findViewById 返回 null - Android - setContentView

Android切换页面--setContentView

android setContentView

android setContentView()源码解析