【Android源码解析】从Window层开始,玩转Activity、View!

前言:这是我第一次写源码解析类的文章,阅读源码真的能学到不少,惊讶大牛设计思路的同时,感觉到更多的是自己好多知识都不清楚。由于水平有限,文中难免有些错误,欢迎指正互相学习~

Window概述

Window,正如它的直译,表示一个窗口。以前我们常说,Activity是直接可与用户交互的UI界面,而这些交互界面都要依附在Window窗口下才能工作、显示!从某种程度甚至可以这么说,Android中的视图(Activity,Dialog,PopupWindow......)都是依附于Window来呈现的,所有的Event事件也都是从Window层下发的。关于Window,Activity,View的关系,这里可以先给出一张粗糙的二维关系图:


Window,Activity,View

从面向对象的角度来说,Window是一个抽象的概念,它对应着一个顶级view,还有一个ViewRootImpl,通过这个实现类中的ViewRootImpl,我们可以操作具体的View,并向它们下发事件。这从抽象Window的实现类PhoneWindow中可以找到如下源码:

    @Override
    public void injectInputEvent(InputEvent event) {
        getViewRootImpl().dispatchInputEvent(event);
    }

    private ViewRootImpl getViewRootImpl() {
        if (mDecor != null) {
            ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
            if (viewRootImpl != null) {
                return viewRootImpl;
            }
        }
        throw new IllegalStateException("view not added");
    }

关于view事件的dispatch(分发)、onIntercept(拦截)、onTouch(消耗)就不在这宣兵夺主详细讲了。

除此之外,Window内部还向我们提供了一个方便各种状态下回调的CallBack接口,主要回调方法如下:

    /**
     * API from a Window back to its caller.  This allows the client to
     * intercept key dispatching, panels and menus, etc.
     */
    public interface Callback {

        public boolean dispatchKeyEvent(KeyEvent event);

        public boolean dispatchTouchEvent(MotionEvent event);

        public boolean onMenuItemSelected(int featureId, MenuItem item);

        public void onContentChanged();

        public void onWindowFocusChanged(boolean hasFocus);

        public void onAttachedToWindow();

        public void onDetachedFromWindow();

    }

很熟悉把~原来我们在Activiy中的各种回调方法都是这其中来的,这些方法回调的周期相信大家也都知道把,这里只说一下onContentChanged(),还记得在Activiy的creat方法中,我们必须给界面通过setContenViewt设置布局,而当布局设置完成后,Window便会回调此方法。至于到底什么是ContentView?后面会分析。

而外部访问Window则必须通过WindowManager的实现类WindowMangerImpl,可以发现其中向外部提供了三个增加、更新、删除View的方法:

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }
    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

通过方法名我们可以很清楚地辨别这三个方法的作用,但是mGlobal又是什么呢?其实这只是一个工作业务的桥接类,将addView的工作通过WindowManagerGlobal全部桥接给了ViewRootImpl来实现了。这也就与上文中说的``Window是一个抽象的概念,它对应着一个顶级view,还有一个ViewRootImpl,通过这个实现类中的ViewRootImpl,我们可以操作具体的View,向它们下发事件`对应起来了,我们主要分析addView栗子:

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }
        synchronized (mLock) {
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

这里很清晰地看到new了一个ViewRootImpl实例,随后将Window中的view,root(ViewRootImpl),wparams(布局参数)存入了相应列表中,之后又通过root.setView(view, wparams, panelParentView);将事情全部移交给了ViewRootImpl来做。setView方法比较复杂,大致思路是先通过requestLayout刷新当前布局,随后通过IPC机制,远程调用WindowManagerService完成View的set。给出一小部分源码:

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                mView = view;
                // 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();
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
    }

最后在这里简单提及下DecorView(PhoneWindow中的内部类),具体的会在下文Activity中讲更好理解,这里只是说明下Window中有这么一个顶级view。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可以看出DecorView本质还是一个FrameLayout,这里面就负责我们的各种UI显示,各种事件消耗、分发。通过Activity,我们就可以将自己的UI布局载入Window中的DecorView!

这里总结一下Window中的知识点:

  • 1.一个Window可以抽象理解为一个View和一个ViewRootImpl的组合。
  • 2.Window内部有一个十分丰富的CallBack接口,可以满足我们大部分的回调需求。
  • 3.通过WindowManager的实现类WindowManagerImpl,管理修改我们的Window,本质上是将这些操作桥接给了ViewRootImpl。
  • 4.Window中的UI承载体--DecorView。
Activity概述

关于Activity的启动流程和Thread这里不会讲述(其实是我也弄不清楚,_~),主要讲Activity是如何和Window建立起联系的。
当新建一个Activity时,通常ide工具都会自动帮我setContentView,之前一直以为这是将我们的布局文件通过ID直接设置给Activity,见多了也就以为这是一种定理了。其实看源码,很清楚地可以发现它本质还是获取的Window:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

先不急着看Window中的setContentView方法,我们先看Window是如何获取的。

    public Window getWindow() {
        return mWindow;
    }

对比源码发现,这个mWindow在L、M版本上初始化的方式还不太一样,如图:

L版本----M版本

L版本是通过一个策略类PolicyManager,使用反射机制获取IPolicy来实例化mWindow,而M版本直接在attach方法中,mWindow = new PhoneWindow(this);直接实例化。不知道这其中是不是考虑到了性能的优化。

通过mWindow.setCallback(this);,便在Activity绑定了Window中的回调接口Callback。

接下来我们看PhoneWindow中的setContentView具体逻辑:

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            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 {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

当我们初次设置contentView时,会执行installDecor(),否则只是清除所有子view,接着inflate我们的布局进decorView的内容区域,最后回调onContentChanged(),通知Activity,DecorView装载完毕了!

再追踪installDecor()方法前,我们先了解一下DecorView的具体结构:


DecorView

不难看出,一个DecorView可以非为两部分,第一部分就是上放的titleBar(标题栏),第二部分就是contentParent(内容区),我们的布局便是装载进contentParent区域。

接着我们看installDecor()方法:

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }
    .................
    .................
    .................
    }

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

很好理解,如果我们的DecorView不存在,则为我们生成一个,接着在生成一个内容区域以供装载布局。

我们主要看generateLayout()的源码:

    protected ViewGroup generateLayout(DecorView decor) {

    //省略ViewGroup参数、样式设置部分//

        mDecor.startChanging();

        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if (getContainer() == null) {
            final Drawable background;
            if (mBackgroundResource != 0) {
                background = getContext().getDrawable(mBackgroundResource);
            } else {
                background = mBackgroundDrawable;
            }
            mDecor.setWindowBackground(background);

            final Drawable frame;
            if (mFrameResource != 0) {
                frame = getContext().getDrawable(mFrameResource);
            } else {
                frame = null;
            }
            mDecor.setWindowFrame(frame);

            mDecor.setElevation(mElevation);
            mDecor.setClipToOutline(mClipToOutline);

            if (mTitle != null) {
                setTitle(mTitle);
            }

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }
            setTitleColor(mTitleColor);
        }

        mDecor.finishChanging();

        return contentParent;
    }

发现contentParent内容区也是通过ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);找到我们传入布局文件的ID,为我们生成一个ViewGroup,在对其做一系列UI优化后,就可以正常使用了。

当然,我们并不一定每次都必须通过布局文件的ID来setContentView,完全可以自己动态地设置内容,栗:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        LinearLayout linearLayout = new LinearLayout(this);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        for (int i= 0; i < 5; i++) {
            TextView textView = new TextView(this);
            textView.setTextSize(12);
            textView.setText(i+"");
            linearLayout.addView(textView);
        }
        setContentView(linearLayout);
    }

不过要想让用户正常看到这些布局,还需要等待Activity的onResume生命周期中执行如下方法:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

我想这也是初识Android时,会说必须经过onResume生命周期后,Activity才能"获取焦点"的原因吧!

其实从另一个常用的方法,我们也能清楚地理解到Activity,Window,DecorView的关系。

findViewById :

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

推荐阅读更多精彩内容