美文网首页
View的面试锦囊

View的面试锦囊

作者: dashingqi | 来源:发表于2020-10-12 17:18 被阅读0次
    Android_Banner.jpg

    View的draw流程

    • 绘制背景
    • 绘制自己
    • 绘制子View
    • 绘制装饰

    View.getLocationInWindow()和View.getLocationOnScreen()区别

    getLocationInWindow

    • 一个控件相对父控件上的坐标位置。

    getLocationOnScreen

    • 一个控件在相对整个屏幕的坐标位置。

    首次View的绘制是发生在什么时候

    是在ActivityThread的handleResumeActivity()方法中

    流程分析
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,                                   String reason) { 
    if (r.window == null && !a.mFinished && willBeVisible) {
                        // 获取到Window对象
                r.window = r.activity.getWindow();
                        //获取到DecorView
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                        //获取到WindowMananger对象
                ViewManager wm = a.getWindowManager();
                 ........ 
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        /**
                         * 重点中的重点
                         * 调用WindowManager的addView(还记得 WindowManager是一个接口类吧,唯一实现子类是WindowManagerImpl)
                         * 其实调用了WindowManger的addView()方法就是将DecoeView添加到了WindowManagerService中了
                         * (PhoneWindow只是处理一些应用窗口通用的逻辑(设置标题栏、导航栏))
                         * 去看下WindowManagerImpl的addView()
                         */
                        wm.addView(decor, l);
                    } else {
                        a.onWindowAttributesChanged(l);
                    }
                }
    }
      
    // WindowManagerImpl # addView()
      
      @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            /**
             * mGlobal ----> WindowManagerGlobal # addView
             */
            mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
        }
      // 在这里委托给WindowManagerGlobal 调用了它的addView()方法 
      
      public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            if (view == null) {
                throw new IllegalArgumentException("view must not be null");
            }
                        ....... 省略代码
    
                //创建了ViewRootImpl
                root = new ViewRootImpl(view.getContext(), display);
    
                // view ---> DecorView 设置布局参数
                view.setLayoutParams(wparams);
    
                //将添加的View保存到View列表中
                mViews.add(view);
                // 将root保存到ViewRootImpl的列表中
                mRoots.add(root);
                // 将wparams参数保存到mParams列表中
                mParams.add(wparams);
    
                // do this last because it fires off messages to start doing things
                try {
                    /**
                    重点
                     * root 是ViewRootImpl的对象
                     * 调用了ViewRootImpl的setView()
                     * 将DecorView添加到WMS
                     * 将窗口和窗口参数设置到ViewRootImpl中
                     */
                    root.setView(view, wparams, panelParentView);
                } catch (RuntimeException e) {
                    // BadTokenException or InvalidDisplayException, clean up.
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
            }
        }
    // 以上主要保存了一些信息。并且将View和布局参数传入到了ViewRootImpl的setView中
    
    //ViewRootImpl # setView()
     public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                if (mView == null) {
    
                    //这里传递进来的view其实是创建好的DecorView
                    mView = view;
                                    .......省略代码
                    /**
                     * 重点呀
                     * 在将View添加到WindowManagerService中之前,确保View进行了一次 measure-->layout->draw
                     * 调用了此方法后,与ViewRootImpl关联的View会执行一次measure-->layout->draw
                     */
                    requestLayout();
                    if ((mWindowAttributes.inputFeatures
                            & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                        mInputChannel = new InputChannel();
                    }
                    mForceDecorViewVisibility = (mWindowAttributes.privateFlags
                            & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
                    try {
                        mOrigWindowType = mWindowAttributes.type;
                        mAttachInfo.mRecomputeGlobalAttributes = true;
                        collectViewAttributes();
                        /**
                         * 重点分析
                         */
                        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                                mTempInsets);
                        setFrame(mTmpFrame);
                    } 
    
                        ......省略代码
                                    // 这里这个iew 是DecorView,把ViewRootImpl作为它的父亲设置给DecorView
                    // 这也是DecorView与ViewRootImpl之间的关系
                    view.assignParent(this);  
            }
        }
    
    • 可以说首次View的绘制 ActivityThread # performResumeActivity()方法是作为入口,真正调用了requestLayout()方法是在ViewRootImpl的setView()方法中。

    ViewRootImpl的创建时机在什么时候

    正如“View首次绘制时机时的分析”,ViewRootImpl的创建时机是在View首次绘制时,是在调用WindowManagerGlobal的addView()的方法中进行创建的。

    ViewRootImpl与DecorView的关系

    ViewRootImpl 是作为DecorView的父亲

    在ViewRootImpl的setView()方法中 -----> view.assignParent(this);

    ViewRootImpl实现了ViewParent的接口

    DecorView的布局是怎么样的

    • 对于布局层级,是这样的,Activity ---> PhoneWindow ---> DecorView

    • 对于DecorView 是包含 titel_bar和content两部分的,DecorView是继承FrameLayout

    • 上述所说的是title_bar 和 content 对应的是 R.layout.screen_simple布局,这也是默认的布局

    • 那么这个布局是在什么设置的呢?

      • 入口是setContentView,真正执行的地方是在PhoneWindow的installDecor()方法中

        // PhoneWindow # installDecor
        private void installDecor() {
                mForceDecorInstall = false;
                if (mDecor == null) {
                    //创建DecorView(其实是一个FrameLayout)
                    mDecor = generateDecor(-1);
                    mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
                    mDecor.setIsRootNamespace(true);
                    if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                        mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
                    }
                } else {
                  // 绑定一个Window
                    mDecor.setWindow(this);
                }
                if (mContentParent == null) {
                    // 将创建的DecorView传递 调用generateLayout方法
                    mContentParent = generateLayout(mDecor);
                 }
               }
        // generaLayout(DedcorView view)
        
        protected ViewGroup generateLayout(DecorView decor) {
               
                ...... 省略代码
                  
                // Inflate the window decor. 
                int layoutResource;
                    // 默认加载的布局是R.layout.screen_simple; 实则是通过获取到的features来加载不同的
                int features = getLocalFeatures();
                if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
                    // 这是一种布局
                    layoutResource = R.layout.screen_swipe_dismiss;
                    setCloseOnSwipeEnabled(true);
                } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
                    if (mIsFloating) {
                        TypedValue res = new TypedValue();
                        getContext().getTheme().resolveAttribute(
                                R.attr.dialogTitleIconsDecorLayout, res, true);
                        layoutResource = res.resourceId;
                    } else {
                        //这是一种
                        layoutResource = R.layout.screen_title_icons;
                    }
                    // XXX Remove this once action bar supports these features.
                    removeFeature(FEATURE_ACTION_BAR);
                    // System.out.println("Title Icons!");
                } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                        && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
                    // Special case for a window with only a progress bar (and title).
                    // XXX Need to have a no-title version of embedded windows.
                    // 这是一种
                    layoutResource = R.layout.screen_progress;
                    // System.out.println("Progress!");
                } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
                    // Special case for a window with a custom title.
                    // If the window is floating, we need a dialog layout
                    if (mIsFloating) {
                        TypedValue res = new TypedValue();
                        getContext().getTheme().resolveAttribute(
                                R.attr.dialogCustomTitleDecorLayout, res, true);
                        layoutResource = res.resourceId;
                    } else {
                        //这是一种
                        layoutResource = R.layout.screen_custom_title;
                    }
                    // XXX Remove this once action bar supports these features.
                    removeFeature(FEATURE_ACTION_BAR);
                } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
                    // If no other features and not embedded, only need a title.
                    // If the window is floating, we need a dialog layout
                    if (mIsFloating) {
                        TypedValue res = new TypedValue();
                        getContext().getTheme().resolveAttribute(
                                R.attr.dialogTitleDecorLayout, res, true);
                        layoutResource = res.resourceId;
                    } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                      
                        layoutResource = a.getResourceId(
                                R.styleable.Window_windowActionBarFullscreenDecorLayout,
                                R.layout.screen_action_bar);
                    } else {
                      //这是一种
                        layoutResource = R.layout.screen_title;
                    }
                    // System.out.println("Title!");
                } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
                    layoutResource = R.layout.screen_simple_overlay_action_mode;
                } else {
                    // Embedded, so no decoration is needed.
                    // 默认的布局
                    layoutResource = R.layout.screen_simple;     
                }
                mDecor.startChanging();
                    //加载布局
                mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
            }
        
        // 加载布局
          void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
                if (mBackdropFrameRenderer != null) {
                    loadBackgroundDrawablesIfNeeded();
                    mBackdropFrameRenderer.onResourcesLoaded(
                            this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                            mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                            getCurrentColor(mNavigationColorViewState));
                }
        
                mDecorCaptionView = createDecorCaptionView(inflater);
                    //解析资源文件,获取到View的对象 也就是布局文件对应的View对象
                final View root = inflater.inflate(layoutResource, null);
                if (mDecorCaptionView != null) {
                    if (mDecorCaptionView.getParent() == null) {
                        addView(mDecorCaptionView,
                                new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
                    }
                    mDecorCaptionView.addView(root,
                            new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
                } else {
        
                    // 将获取到的View添加到DecorView中
                    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
                }
                mContentRoot = (ViewGroup) root;
                initializeElevation();
            }
        

    DecorView的创建时机

    在上述分析过程中,在PhoneWindow的installDecor()方法中有调用到generateLayout(-1)创建了DecorView

    这是DecorView创建的地方。

    installDecor() 调用的时机是从setContentView()作为入口的,执行的流程就是 Activity(setContentView) ---> PhoneWindow(setContentView) ----> installDecor()

    所以DecorView的创建时机是setContentView()方法的执行

    setContentView()干了什么

    • 创建了DecorView
    • 调用了LayoutInflate.inflate()方法,将布局文件解析成View
    • 将View添加到DecorView当中。

    layoutInflate的流程是什么

    https://www.jianshu.com/p/cc5db309cdcf

    PhoneWindow的创建时机

    • 创建的地方是Activity的attach方法中
    • Activity#attach方法的执行入口是在ActivityThread # performLacunchActivity()
    • perfromLaunchActivity()方法的执行设计到Activity的启动流程。

    如何触发View的重新绘制呢?

    • requestLayout()
    • Invalidate()

    requestLayout 和 invalidate的区别

    区别和联系

    • requestLayout:当我们移动一个View位置或者改变View的大小时候调用
      • View调用这个requestLayout方法,会标记当前View以及父容器,同时逐层向上提交,直到ViewRootImpl中,会调用三大工作流程 measure、layout、draw 对每一个由标记的View进行这三步操作。
    • invalidate的方法调用会引起View树的重新绘制,比如说 setVisible
      • 当View调用了invalidate方法后,会为该View添加一个标记位,不断向父容器请求刷新,父容器计算出需要重绘的区域,传递到ViewRootImpl中,触发performTraversals方法,开始对View树重新绘制(绘制需要重新绘的视图)
      • postInvalidate功能如同 invalidate 只不过是在非UI线程中调用。
    • 总的来说就是:requestLayout() 会执行 onMeasure()和onLayout();invalidate()方法会执行onDraw()
      • requestLayout()过程会设置 FORCE_LAYOUT标志位;invalidate()过程会设置dirty标志
    • onLayout:如果该View是ViewGroup对象,需要实现该方法,对每个子View进行布局
    • onDraw:绘制视图本身(ViewGroup不需要实现,View都需要重载这个方法)
    • drawChild:去重新回调每个子View的draw()方法。

    点击事件中开启一个子线程更新UI会怎么样?

     btn_toast.setOnClickListener { listener ->
                //Toast.makeText(this, "展示 吐司", Toast.LENGTH_SHORT).show()
                Thread(Runnable {
                    var name = Thread.currentThread().name
                    Log.d("tag", "$name")
                    btn_toast.text = "子线程"
                }).start()
            }
    

    这个问题分两种情况,控件宽高固定,控件高度固定宽度warp_content,

    情况1
    • 这种情况是会更新成功,并且不会crash
    • 由于宽和高都是固定的,此时操作更新UI并不会走requestLayout,仅仅重新绘制一下内容,所以就不存在线程检查的操作,所以就更新成功了。
    情况2
    • 这种情况会crash
      android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    • 原因就在于,当前的宽度是wrap_content,而当我们去操作更新的UI操作时,此时会调用requestLayout(重新进行测量和布局),在我们的requestlayout中会进行线程的检查,发现没有在主线程就会报错。

    从Activity创建到View的显示这个过程中主要都发生了什么?

    这个问题很广泛,我个人认为可以拆分成以下几点回答

    handleLaunchActivity

    在handleLaunchActivity方法主要调用了performLaunchActivity() 和 handleResumeActivity()

    • 在performLaunchActivity中主要做了如下几个事情
      • 创建了Context
      • 通过反射创建了Activity
      • 通过反射创建了Application
      • 调用了attach
        • 为每个Activity创建一个PhoneWindow
        • 将WindowManger与PhoneWindow进行关联
      • 执行了Activity的onCreate()方法
    setContentView()过程

    调用流程是 Activity(setContentView) ---> PhoneWindow(setContentView) ---> installDecor()

    • 在installDecor中主要做了如下几个事情
      • 创建了DecorView ---> DecorView extends FrameLayout
      • 为DecorView中content加载布局文件
    handleResumeActivity()方法的执行过程
    • 首先调用了performResumeActivity()
      • 执行了Activity的onResume()
      • 获取了Activity专属的Window
      • 获取到了DecorView
    • 接着调用了WindowManagerImpl的addView()
      • 内部调用了WindowManagerGlobal # addView()
    • WindowManagerGlobal # addView
      • 创建了ViewRootImpl
      • 调用了ViewRootImpl # setView()
    • ViewRootImpl # setView
      • requestLayout() ---> 保证在绘制之前 进行了 View的绘制流程
      • 将携带数据的Window发送到系统进程 WMS中进行数据的绘制。

    相关文章

      网友评论

          本文标题:View的面试锦囊

          本文链接:https://www.haomeiwen.com/subject/rakppktx.html