自定义View(二)UI加载流程

一、分类


在《Android开发艺术探索》中,刚哥将自定义View分为了四种,个人感觉很精确:

  • 继承View,创建新View

  • 继承ViewGroup,创建新Layout

  • 继承现有特定的View进行扩展,如继承Textview自定义字体

  • 继承现有特定的ViewGroup,进行自定义组合控件

二、构造方法


构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。

1、View的构造方法

由View的源码中可以看到,通过代码创建View会调用一个参数构造;通过xml创建View会调用二个参数构造,但最终会View会调用自己的四个参数的构造方法,并且在其中还调用了一个参数构造做一些初始化工作:

/**
 * Simple constructor to use when creating a view from code.
 */
public View(Context context) {
    mContext = context;
    ......
}

/**
 * Constructor that is called when inflating a view from XML
 */
public View(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    this(context);

    final TypedArray a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
    ......
}

2、自定义View构造方法

我们自定义View时,一般会写两个构造(一个参数和两个参数),而在构造方法里最终会调用super();是为了调用View父类构造中的一些代码为我们做一些初始化工作,以TextView为例,TextView用了四个构造:

/**
 * 代码创建该View时会被调用的简单构造函数
 * @param context 运行视图的上下文,通过它可以访问当前主题,资源等
 */
public TextView(Context context) {
   this(context, null);
}

/**
 * 在XML创建该View时会被调用
 * @param context
 * @param attrs 在xml中为该View指定的属性
 */
public TextView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs,com.android.internal.R.attr.textViewStyle); // 可以在这里传入第三个参数指定样式,也可代码创建View时直接调用三参构造传入样式
}

/**
 * 代码手动调用该方法才生效,给View指定样式时使用
 * 如果我们没有对View设置某些属性,就使用这个样式中的属性,在xml中设置style属性不会调用此构造
 * @param context
 * @param attrs
 * @param defStyleAttr 当前主题中的一个属性,它包含对样式资源的引用,该样式资源为视图提供默认值。只有在明确调用该构造的时候才会生效(传入0即没有默认样式)。该style需要在Context使用的theme中被引用才生效
 */
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0); // 可以在这里传入第四个参数指定样式,也可代码创建View时直接调用四参构造传入样式
}

/**
 * API 21时增加,当mindSdkVersion=21时再使用
 * @param context
 * @param attrs
 * @param defStyleAttr
 * @param defStyleRes 指定该View的样式,仅在defStyleAttr为0或在主题中找不到时使用。可以为0则没有默认样式。
 */
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    
    ......
    TypedArray a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
            
    ......
}

可以发现一个重要的方法context.obtainStyledAttributes,用来解析属性:

public final TypedArray obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr,@StyleRes int defStyleRes) {
        
    return getTheme().obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
}

了解一下这几个参数:

  • AttributeSet set

    工具类用于解析属性集,获得属性值集合,就是我们为view设置的属性。而我们可以通过多种方式设置属性值,最终的取值是有优先级顺序的:

相同属性取值优先级:

  1. AttributeSet中的值(xml中指定的属性) >
  2. AttributeSet中的style资源(xml中使用style属性引用的资源)
  3. defStyleAttr指定的默认样式
  4. defStyleResource指定的默认样式资源
  5. context的Theme中的值

不同的属性:
取并集

  • int[] attrs
    attrs.xml中自定义属性在R文件中生成的id数组,通过R.styleable.自定义属性名称引用

  • int defStyleAttr
    attrs.xml中自定义属性使用时引用的style样式,通过R.attr.属性名称引用,可以用来全局定义某类view的样式。上面的注释中也说明了,这个style样式必须在context的theme中引用才生效,以TextView为例:

// attrs.xml定义属性textViewStyle
<resources>
    ……
    <!-- Default TextView style. -->
    <attr name="textViewStyle" format="reference" />
    ……
</resources>
// 指定全局theme
<application
        ……
        android:theme="@style/AppTheme">
        ……
</application>

// theme定义
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

// 往上追溯可找到AppTheme继承于Theme.Light,而其中使用textViewStyle属性指定一个TextView默认的style
<style name="Theme.Holo.Light" parent="Theme.Light">
    ……
    <item name="textViewStyle">@style/Widget.Holo.Light.TextView</item>
    ……
</style>
// textViewStyle
<style name="Widget.Holo.Light.TextView" parent="Widget.TextView" />

<style name="Widget.TextView">
    <item name="textAppearance">?attr/textAppearanceSmall</item>
    <item name="textSelectHandleLeft">?attr/textSelectHandleLeft</item>
    <item name="textSelectHandleRight">?attr/textSelectHandleRight</item>
    ……
</style>
// 设置defStyleAttr为R.attr.textViewStyle
public TextView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs,com.android.internal.R.attr.textViewStyle); 
}

如上设置的defStyleAttr将会生效,在我们设置TextView相关属性时,未设置的属性将会从R.attr.textViewStyle引用的style中去取,继续往下看 ↓↓

// 以TextView的外观为例,TextView默认外观是属性textAppearanceSmall所对应的样式,跟踪
<item name="textAppearance">?attr/textAppearanceSmall</item>

    ↓↓
    
<item name="android:textAppearanceSmall">@style/TextAppearance.AppCompat.Small</item>

    ↓↓
    
<style name="TextAppearance.AppCompat.Small" parent="Base.TextAppearance.AppCompat.Small"/>

    ↓↓
    
// textview默认外观
<style name="Base.TextAppearance.AppCompat.Small">
    <!--14sp-->
    <item name="android:textSize">@dimen/abc_text_size_small_material</item>
    <item name="android:textColor">?android:attr/textColorTertiary</item>
</style>

     ↓↓
     
// textColor
<item name="android:textColorTertiary">@color/abc_secondary_text_material_light</item>

    ↓↓
    
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--#24000000-->
    <item android:state_enabled="false" android:color="@color/secondary_text_disabled_material_light"/>
    <!--#8a000000-->
    <item android:color="@color/secondary_text_default_material_light"/>
</selector>

由上可知,如果我们使用TextView未设置字体大小和颜色,那么默认字体为14sp,enable=true时,字体颜色为#8a000000 ,当然,这些是建立在我们的主题样式为Theme.AppCompat.Light.DarkActionBar,如果设置其他样式,字体颜色会有不同。

  • int defStyleRes
    指定一个默认的样式资源,形式如R.style.样式资源名称,只有defStyleAttr属性为0或指定了但没找到时才使用这个,和context的theme没关系,不需要如defStyleAttr必须在theme中引用。

三、UI加载流程


我们来分析我们为Activity设置布局是如何加载的:

1、由Activity#setContentView()开始,最终调用的是PhoneWindow#setContentView()

// MainActivity.java
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 设置布局资源
        setContentView(R.layout.activity_main);
    }
}

↓↓

// Activity.java
public void setContentView(@LayoutRes int layoutResID) {
    // 调用的是window的setContentView()
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

↓↓

// 获取Window
public Window getWindow() {
    return mWindow;
}

↓↓

// Window是抽象类,所以由PhoneWindow实现
mWindow = new PhoneWindow(this, window, activityConfigCallback);

↓↓

// PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        // 初始化DecorView
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 将我们在Activity中设置的布局资源,加载到mContentParent
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

2、跟踪PhoneWindow#installDecor()

// PhoneWindow.java中
private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // 创建DecorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            // DecorView与PhoneWindow关联
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
           // 获得mContentParent
            mContentParent = generateLayout(mDecor);
        }
        ......
}


3、跟踪PhoneWindow#generateDecor(),查看DecorView的创建

// DecorView生成
protected DecorView generateDecor(int featureId) {
    ......
    // 调用构造方法创建DecorView
    return new DecorView(context, featureId, this, getAttributes());
}

↓↓

// DecorView.java , 可以知道DecorView 是个 FrameLayout
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    ......
    DecorView(Context context, int featureId, PhoneWindow window,
            WindowManager.LayoutParams params) {
        super(context);
        ......
        // 关联PhoneWindow
        setWindow(window);
        ......
    }

}

4、跟踪PhoneWindow#generateLayout(),查看mContentParent是什么


//  PhoneWindow.java中,再看mContentParent生成
protected ViewGroup generateLayout(DecorView decor) {
    TypedArray a = getWindowStyle();
    
    ......
    // 根据Feature判断,获得应该加载的布局资源
    layoutResource = R.layout.screen_simple;
    
    ......
    
    mDecor.startChanging();
    
    // 将系统布局资源加载到DecorView中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    
    // 获得mContentParent,系统布局资源中的其中一个ViewGroup
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }
    
    ......
    
    mDecor.finishChanging();

    return contentParent;
}

↓↓

// 跳到DecorView.java中 ,看看是如何加载系统布局资源到DecorView中
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    ......
    
    mDecorCaptionView = createDecorCaptionView(inflater);
    // 加载布局资源
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        
        // 添加到DecorView
        mDecorCaptionView.addView(root,
                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {

        // 添加到DecorView
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

↓↓

// 回到PhoneWindow.java中,ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 跟进Window.java中
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
    // contentParent在DecorView中获取
    return getDecorView().findViewById(id);
}

↓↓

// PhoneWindow.java中 ,看下传入的id ,最终在DecorView中找到这个id
/**
 * The ID that the main layout in the XML layout file should have.
 */
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

↓↓

// 最后来看一下, 加载到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" />
    <!--我们在Activity中设置的资源最终都放入这个id为content的FrameLayout中-->
    <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>

5、跟踪ActivityThread#handleResumeActivity()

以上主要分析的是创建DecorView和contentView加载到DecorView上的过程,而这些都是在Activity的onCreate()中的setContentView()中进行的,那么什么时候会调用Activity的onCreate()呢?DecorView又什么时候加载到window中的呢?

先说下流程:

Activity启动流程中会在ActivityThread#handleLaunchActivity()中,调用performLaunchActivity()启动Activity,在这里面会调用到Activity#onCreate()方法,从而完成上面所述的DecorView创建动作。

后面会调用ActivityThread#handleResumeActivity()方法*(在android-28的源码之前,这个方法是在ActivityThread#handleLaunchActivity()中调用performLaunchActivity()之后被调用,但在android-28的源码中不是,后面研究Activity启动流程再看)*,在其中将DecorView添加到Window显示

下面我们跟踪ActivityThread#handleResumeActivity()部分源码:

// ActivityThread.java
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    ......
    
    // 关键代码,调用WindowManager的addView()方法,将decorView添加到window;因为WindowManager是抽象类,所以我们跟进到WindowManagerImpl中去看
    wm.addView(decor, l);    
    
    ......    
}

↓↓

// WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    // 关键代码
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

↓↓

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ......
    
    // 创建ViewRootImpl
    root = new ViewRootImpl(view.getContext(), display);
    
    ......
    
    // 关键方法,在其中完成View的绘制显示流程
    root.setView(view, wparams, panelParentView);
            
}

以上就是大致的UI加载流程,有点乱,自己跟踪一遍就清楚了,最终可以清楚的了解到我们的布局资源加载到DecorView、DecorView加载到window中的整个流程,也能轻松的画出以下UI层级图:

四、UI显示流程


先补充个概念:

ViewRoot

ViewRoot对应ViewRootImpl类,它是连接DecorView和windowManager的纽带,View的三大流程都是通过它来完成的。

/**

  • The top of a view hierarchy, implementing the needed protocol between View
  • and the WindowManager. This is for the most part an internal implementation
  • detail of {@link WindowManagerGlobal}.
  • {@hide}
    */
    public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    }

上一节,我们跟踪到了root.setView(view, wparams, panelParentView); 说到这个是整个加载流程中最关键的方法,在这个方法中,将完成整个视图树的绘制显示,下面我们继续跟踪:

1、跟踪ViewRootImpl#setView()方法,最终调用ViewRootImpl#requestLayout()方法


// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    // 关键代码
    // Schedule the first layout -before- adding to the window
    // manager, to make sure we do the relayout before receiving
    // any other events from the system.
    requestLayout();

    // 关键代码,调用的是View中的方法,此处的view是DecorView,this是ViewRootImpl,将ViewRootImpl关联到DecorView的mParent变量,可通过getParent()获取
    view.assignParent(this);
}

↓↓

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        // 重要代码
        scheduleTraversals();
    }
}

2、跟踪ViewRootImpl#requestLayout()方法,最终调用ViewRootImpl#performTraversals()

// ViewRootImpl.java
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        // 重要代码
        scheduleTraversals();
    }
}

↓↓

void scheduleTraversals() {
    mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}

↓↓

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

↓↓

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

↓↓

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

    if (mProfile) {
        Debug.startMethodTracing("ViewAncestor");
    }
    // 关键代码
    performTraversals();

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

↓↓

private void performTraversals() {
    ......
    
    // 测量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    ......
    
    // 布局
    performLayout(lp, mWidth, mHeight);
    
    ......
    
    // 绘制
    performDraw();
}

3、测量,跟踪ViewRootImpl#performMeasure()

// ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    ......
    
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    ......
}

↓↓

// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ......
}

↓↓

// 自定义View时用来重写
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

4、布局,跟踪ViewRootImpl#performLayout()

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
    ......
    final View host = mView;
    ......
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ......        
}  

↓↓

// View.java
public void layout(int l, int t, int r, int b) {
    ......
    onLayout(changed, l, t, r, b);
    ......
}

↓↓

// 自定义View时用来重写
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

5、绘制,跟踪ViewRootImpl#performDraw()

// ViewRootImpl.java
private void performDraw() {
    ......
    boolean canUseAsync = draw(fullRedrawNeeded);
    ......
}

↓↓

private boolean draw(boolean fullRedrawNeeded) {
    ......
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
        return false;
    }
    ......
}

↓↓

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    final Canvas canvas;
    ......
    // 自定义View重写onDraw(canvas)中的canvas就是在这里被创建
    canvas = mSurface.lockCanvas(dirty);
    ......
    mView.draw(canvas);
    ......
}

↓↓

// View.java
public void draw(Canvas canvas) {
    /*
     * 绘制步骤
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background      背景
     *      2. If necessary, save the canvas' layers to prepare for fading 
     *      3. Draw view's content      内容
     *      4. Draw children            子View
     *      5. If necessary, draw the fading edges and restore layers 
     *      6. Draw decorations (scrollbars for instance)     滚动条
     */
    ......
    onDraw(canvas)
    .....
}

↓↓

// 自定义View时用来重写
protected void onDraw(Canvas canvas) {
}


个人总结,水平有限,如果有错误,希望大家能给留言指正!如果对您有所帮助,可以帮忙点个赞!如果转载,希望可以标明文章出处!最后,非常感谢您的阅读!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值