是Android的自定义View-进阶知识-Android中的View体系

前言

Android现有的View体系是一个十分庞大的结构体系,单凭这一篇文章肯定不可能面面俱到,但我会尽我的理解来尽可能地将直观的体系展现给大家。

View的体系

结构

目前,Android的View体系都基于View和ViewGroup两个大类,同时ViewGroup又是View的子类。其结构设计基于“组合模式”,ViewGroup是容器,View是叶子节点,这就意味着ViewGroup中既可以包含View,又可以包含ViewGroup,但是反之则不行,这就保证依赖关系的安全。

关于组合模式,本文不做展开,未来会在设计模式的系列文章中进行讲解,感兴趣的同学可以先做一个了解。

所以View的的树状结构所呈现的如下:

View树状结构

做过Android的同学都知道,我们在写布局文件时,ViewGroup和View都是层层嵌套的,那他们是怎么展示到我们屏幕上的呢?

可以看到View的结构是树状的,由于树的特性,View的展示过程是从上到下逐级绘制(实际过程当然很复杂),可以说每一个有视图Activity都包含一个View Tree用来描述呈现在屏幕上的View关系,这方便让开发者更好地定位到每一个View的位置。

同时,View的绘制起点并不是我们所定义的那一个xml视图文件,在其外层还有系统所提供的一层东西所包裹,那这层东西又是什么呢?

顶层布局

本小节是关于源码的分析,如果不感兴趣的同学可以先跳过,先直接给出结论:

结构图

我们都知道在Activity的onCreate方法中需要实现setContentView方法来绑定我们定义的布局,那绑定的布局又是绑定在什么地方呢?直接绑定在Activity上吗?

这些问题的答案自然需要到源码中去找了,本文所使用的源码是基于Android 10。

分析的入口

我们的编码一般都是从Activity开始,在Activity中都有onCreate方法用于进行初始化设置,还记得其中的setContentView(R.layout.activity_main);嘛?这就是我们设置xml布局的地方,也是我们分析的入口。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 分析的入口 --> 分析1.Activity 的 setContentView 方法
    setContentView(R.layout.activity_main);
}

创建过程

分析1.Activity 的 setContentView 方法

首先会进入到代理中:

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

再进到getDelegate()下的setContentView,会发现是一个抽象方法,这么快就断了线索吗?一回想,代理这个词,那就去找具体实现这个方法的对象吧。

其实在抽象方法的注释上也表明了要去哪里寻找:

/**
 * Should be called instead of {@link Activity#setContentView(int)}}
 */
public abstract void setContentView(@LayoutRes int resId);

由于新建的Activity最终都是继承于Acivity类的,所以我们在Activty类中找到 setContentView 方法实现:

// Activity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    // 获取Window,设置布局 --> 分析2.从哪里获取的Window,怎么样设置的布局
    getWindow().setContentView(layoutResID);
    // 初始化ActionBar
    initWindowDecorActionBar();
}

// 在Activity中有三个重载函数用于在不同的情况下创建布局:
// setContentView(@LayoutRes int layoutResID)
// setContentView(View view)
// setContentView(View view, ViewGroup.LayoutParams params)

可以看到在Activity的setContentView方法中有两步操作,从字面上的意思来看:

  • 获取Window,设置布局
  • 初始化ActionBar

这里就有两个疑问了:

  • 获取了什么Window?
  • 初始化的ActionBar是哪里的,Decor是啥?

下面我们来一一解决我们的疑问:

分析2.从哪里获取的Window,怎么样设置的布局

通过代码追踪我们发现getWindow方法返回了一个mWindow对象:

public Window getWindow() {
    return mWindow;
}

那么这个mWindow实例是从哪创建的呢?通过搜索我们发现这是一个PhoneWindow

// mWindow是一个PhoneWindow的实例对象
mWindow = new PhoneWindow(this, window, activityConfigCallback);

这样我们就知道是通过PhoneWindow来进行布局的设置的,那么我们进到PhoneWindow当中,搜索一下setContentView方法:

// PhoneWindow中的setContentView方法,省略无关的代码
@Override
public void setContentView(int layoutResID) {
    // 判断内容父布局是否为空,如果为空则创建DecorView --> 分析3.创建DecorView的过程
    if (mContentParent == null) {
        installDecor();
    } 
    // 将布局添加到内容父布局当中去 --> 展示过程
    mLayoutInflater.inflate(layoutResID, mContentParent);
    // 此外通过其它重载方法进行布局的设置会调用
    // mContentParent.addView(view, params);方法
    // 原理是一样的,只不过所获的参数不同需要通过不同的方式进行展示
    // 因为最终都会通过addView(view, params)方法进行添加,展示过程将以这个方法作为入口
    
    ···
}

ps:PhoneWindow类在源码中是被隐藏的不能直接通过代码追踪的方式直接进入,可以通过sdk中的源码进行阅读。

分析3.创建DecorView的过程

进入到installDecor方法中,来看一下DecorView这个东西具体是怎么被创建出来的。

// 省略无关代码,只关注DecorView的创建过程
private void installDecor() {
    // 判断DecorView是否为空,为空则创建DecorView
    if (mDecor == null) {
        // 创建DecorView
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } 
    // 不为空,则将DecorView依附到PhoneWindow上
    else {
        mDecor.setWindow(this);
    }
    
    // 如果内容父布局为空,则根据DecorView生成布局
    if (mContentParent == null) {
        // 获取内容布局
        mContentParent = generateLayout(mDecor);

        ···
    }
}

这里有两个方法generateDecorgenerateLayout分别用来创建和生成DecorView,在这其中具体做了什么呢?

generateDecor方法,主要获取了DecorView所需要的相关参数之后,进行DecorView的创建,并返回DecorView实例


protected DecorView generateDecor(int featureId) {
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, getContext());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

generateLayout方法,主要实现了DecorView中布局的创建

protected ViewGroup generateLayout(DecorView decor) {
    ···
    // 布局资源
    int layoutResource;
    // 一般情况下会获得screen_simple布局
    layoutResource = R.layout.screen_simple;
    // 并加载到DecorView中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    // 获取内容布局并返回
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    ···
    return contentParent;
}

至此,我们就获得了DecorView,那么疑问一个接着一个:DecorView中的布局是怎么样的呢?

我们找到R.layout.screen_simple所对应的文件来看看其中的布局到底是怎么样的?

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看到DecorView中包含一个LinearLayout布局,其中包括了两部分actionBar和FrameLayout(会被定义好的布局文件所替换)。

我们可以来画这么一个图:

结构图

整个嵌套关系就很清晰地展示出来了:
Activity-->PhoneWindow-->DecorView-->LinearLayout-->包含ActioneBar和定义的布局视图

创建完了视图,自然就是要展示出来了,下面来聊一聊展示的过程。

展示过程

回到PhoneWindow的setContent方法中,之前我们分析了DecorView的创建过程,接下来我们来看看DecorView创建完之后,是如何进行展示的。

// PhoneWindow中的setContentView方法,省略无关的代码
@Override
public void setContentView(int layoutResID) {
    // 判断内容父布局是否为空,如果为空则创建DecorView --> 之前的分析.创建DecorView的过程
    if (mContentParent == null) {
        installDecor();
    } 
    // 将布局添加到内容父布局当中去 --> 展示过程
    mLayoutInflater.inflate(layoutResID, mContentParent);
    // 此外通过其它重载方法进行布局的设置会调用
    // mContentParent.addView(view, params);方法
    // 原理是一样的,只不过所获的参数不同需要通过不同的方式进行展示
    // 因为最终都会通过addView(view, params)方法进行添加,展示过程将以这个方法作为入口
    
    ···
}

展示过程最终都会通过ViewGroupaddView方法进行绘制展示,在这部分内容中我们会进入到View的绘制流程当中去。

public void addView(View child, int index, LayoutParams params) {
    
    ···
    
    // 请求布局 --> 分析4.请求布局的过程
    requestLayout();
    // 无效化??? invalidate这个单词是无效化的意思 --> 分析5.真的是无效化吗?
    invalidate(true);
    addViewInner(child, index, params, false);
}
分析4.请求布局的过程

首先对requestLayout方法进行分析,进入到方法中,核心方法是mParent.requestLayout();的调用:

// View的requestLayout方法
public void requestLayout() {
    
    if (mParent != null && !mParent.isLayoutRequested()) {
        // 调用ViewRootImpl的requestLayout方法
        mParent.requestLayout();
    }
    
    ···
}

这里出现了一个新的变量mParent,那么它是什么呢?通过代码追踪,我们发现这是一个接口,紧接着找到了接口的实现类ViewRootImpl,那我们进入到ViewRootImplrequestLayout中来看看做了些什么?

// ViewRootImpl的requestLayout方法
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        // 检查线程,是否在UI线程
        checkThread();
        mLayoutRequested = true;
        // 遍历表 --> 分析6.最终的流程
        scheduleTraversals();
    }
}

关于scheduleTraversals先放一放,还记得之前和requestLayout并列的invalidate方法吗?我们先来分析它,避免把它忘记掉了。

分析5.真的是无效化吗?

经过requestLayout();的过程后,代码会执行到invalidate(true);,一开始看见这个方法名的时候很懵逼,什么叫无效化?既然不明白就只能带这个疑问去看源码了。

// 看到这个invalidateCache参数的名字有点明白这是什么意思了
public void invalidate(boolean invalidateCache) {
    // 具体的操作,这里将一些尺寸相关的参数传了进去
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

看到那个参数名字,猜想会不会是这是一个使缓存的绘制数据失效,并重新绘制的过程呢?

我们继续往下看,进到invalidateInternal方法中去,核心代码就一行。

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {

    ···

    // 无效化子控件,老版本的源码是这一行代码,标记为废弃了
    p.invalidateChild(this, damage);
    
    // 新的版本使用了其它方式,如下
    receiver.damageInParent();
}

那么就进到damageInParent方法中:

// View的方法
protected void damageInParent() {
    if (mParent != null && mAttachInfo != null) {
        mParent.onDescendantInvalidated(this, this);
    }
}

这里需要注意一下直接进到mParent.onDescendantInvalidated(this, this);里的话就碰壁了,还记得mParent的实现类ViewRootImpl,我们就进到这个方法里去看它具体的实现是什么:

@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
    // TODO: Re-enable after camera is fixed or consider targetSdk checking this
    // checkThread();
    if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
        mIsAnimating = true;
    }
    invalidate();
}

哈哈,看到invalidate()这个方法终于是放心了,看来Android新的源码只是换了一种实现的方式(小声逼逼叨),那么再进到这个方法中去:

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        // 遍历表 --> 分析6.最终的流程
        scheduleTraversals();
    }
}

emmm,这绕了一大圈又到了scheduleTraversals方法了,那就进去看看这最终的方法里都干了什么吧。

分析6.最终的流程
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 注意这个mTraversalRunnable变量,是将某中操作进行了传递
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

注意其中mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);mTraversalRunnable变量,是将某中操作进行了传递那么就去看看是做了什么操作吧。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

代码追踪到了对应的实现,进到方法中:

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        // 真正进行遍历的地方,也就是之后的View绘制流程的入口
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

代码追踪到这里,setContentView之后所做的事情我们大致就清楚了,之后就是具体的测量绘制流程了,关于View的绘制流程会在之后的文章中去讲解。

绘制流程

虽然关于View的绘制流程,本文暂不做展开,但是关于View的绘制过程还是要来讲一下的。View的绘制可以分为三个部分:

  • 测量
  • 布局
  • 绘制

三个过程是View展示在了屏幕之上,这也很好理解,每一个步骤都对应了一般设计的步骤。

关于View绘制的各个步骤和过程的源码分析将在之后的文章中进行讲解。

功能

要实现一个View,需要进行以下但不止以下事件的处理,在后续的文章中会对他们进行讲解。

  • 创建View
  • 测量
  • 布局
  • 绘制
  • 事件处理
  • 焦点处理
  • 显示处理
  • 动画处理

总结

本文主要介绍了View的体系结构,以及在View进行绘制前所做的那些操作,关于源码分析的部分,并没有很详细地进行展开,只是讲解了一下其中最主要的一部分内容,其实还有很多其它的内容(跳转动画,线程判断)没有展开,由于文章重点不同,感兴趣的同学可以自己查阅相关资料先看看,未来可能也会讲到。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容