一、前言


 本篇文章我们来详细分析一下 ViewRootImpl。

 

二、View 通过 ViewRootImpl 来绘制


ViewRootImpl 是一个视图层次结构的顶部,在上一篇文章中我们知道了 ViewRootImpl 实现了 View 与 WindowManager 之间所需要的协议,作为 WindowManagerGlobal 中大部分的内部实现。这个好理解,在 WindowManagerGlobal 中实现方法中,都可以见到 ViewRootImpl,也就说 WindowManagerGlobal 方法最后还是调用到了 ViewRootImpl。addView,removeView,update 的调用顺序:WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl

我们看下前面调用到了 viewRootImpl 的 setView 方法:

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();    ...    try {        ...        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,                getHostVisibility(), mDisplay.getDisplayId(),                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,                mAttachInfo.mOutsets, mInputChannel);    } }

在setView方法中,首先会调用到 requestLayout(),表示添加 Window 之前先完成第一次 layout 布局过程,以确保在收到任何系统事件后面重新布局。ViewRootImpl 调用到 requestLayout() 来完成 View 的绘制操作,代码如下:

@Overridepublic void requestLayout() {    if (!mHandlingLayoutInLayoutRequest) {        checkThread();        mLayoutRequested = true;        scheduleTraversals();    }}

view 的绘制首先会调用 checkThread() 来判断当前线程,代码如下:

void checkThread() {    if (mThread != Thread.currentThread()) {        throw new CalledFromWrongThreadException(            "Only the original thread that created a view hierarchy can touch its views.");    }}

如果不是当前线程则抛出异常,这个异常是不是感觉很熟悉啊,没错,当你在子线程更新 UI 的话就会抛出这个异常:

android.view.ViewRootImpl$CalledFromWrongThreadException:     Only the original thread that created a view hierarchy can touch its views.

抛出地方就是 checkThread() 这里,一般在子线程操作 UI 都会调用到 view.invalidate(),而 View 的重绘会触发ViewRootImpl 的 requestLayout(),就会去判断当前线程。继续看,判断完线程后接着会去调用 scheduleTraversals(),代码如下:

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

scheduleTraversals() 中会通过 handler 去异步调用 mTraversalRunnable 接口:

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

接着看 doTraversal() 的代码:

void doTraversal() {    ...    performTraversals();    ...}

可以看到,最后真正调用绘制的是 performTraversals() 方法,这个方法很长核心便是:

private void performTraversals() {      ...    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    ...    performLayout(lp, desiredWindowWidth, desiredWindowHeight);    ...     performDraw();}

而这个方法各自最终调用到的便是:

...int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  ...mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);  ...mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());  ...mView.draw(canvas);  

会开始触发测量绘制。performTraversals() 方法会经过 measure、layout 和 draw 三个过程才能将一个 View 绘制出来,所以 View 的绘制是 ViewRootImpl 完成的,另外当手动调用 invalidate(),postInvalidate(),requestInvalidate() 也会最终调用performTraversals(),来重新绘制 View。

 

三、ViewRootImpl 是 View 与 WindowManager 联系的桥梁


那么 View 和 WindowManager 之间是怎么通过 ViewRootImpl 联系的呢。从上一篇文章中我们知道,WindowManager 是继承于 ViewManager 接口的,而 ViewManager 提供了 addView(),deleteView(),updateView() 方法。就拿 setContentView() 来说,当 Activity 的 onCreate() 调用到了 setContentView() 后,view 就会被绘制了吗?肯定不是,setContentView() 只是把需要添加的 View 的结构添加保存在 DecorView 中。此时的 DecorView 还并没有被绘制(没有触发 view 的 measure,layout,draw)。

DecorView 真正的绘制显示是在 activity.handleResumeActivity() 方法中 DecorView 被添加到 WindowManager 时候,也就是调用到 windowManager.addView(decorView)。而在 windowManager.addView() 方法中调用到 windowManagerGlobal.addView() 开始创建初始化 ViewRootImpl,再调用到 viewRootImpl.setView(),最后是调用到 viewRootImpl 的 performTraversals() 来进行 view 的绘制(measure,layout,draw),这个时候 View 才真正被绘制出来。这也就是为什么我们在 onCreate() 方法中调用 view.getMeasureHeight() = 0 的原因,我们知道 activity.handleResumeActivity() 最后调用到的是 activity 的 onResume() 方法,但是按上面所说在 onResume 方法中调用就可以得到了吗,答案肯定是否定的,因为 ViewRootImpl 绘制 View 并非是同步的,而是异步(Handler)。

难道就没有得监听了吗?相信大家以前获取使用的大多是如下方式:

view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {    @Override    public void onGlobalLayout() {    // TODO Auto-generated method stub       }});

 没错,的确是这个,为什么呢,因为在 viewRootImpl 的 performTraversals 的绘制最后,调用了

 {        if (triggerGlobalLayoutListener) {            mAttachInfo.mRecomputeGlobalAttributes = false;            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();        }        ...        performDraw();}

dispatchOnGlobalLayout() 会触发 OnGlobalLayoutListener 的 onGlobalLayout() 函数回调,但此时 View 并还没有绘制显示出来,只是先调用了 measure() 和 layout(),但也可以得到它的宽高了。

另外,前面说到,ViewRootImpl 在调用 requestLayout() 准备绘制 View 的时候会先判断线程,这里我们前面分析了,但也只是分析了一点。为什么这么说呢?先看下 Activity 的这段代码:

    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        tv = (TextView) findViewById(R.id.tv);        new Thread(new Runnable() {            @Override            public void run() {                tv.setText("Hohohong Test");            }        }).start();    }

我是在 onCreate() 里面的子线程去更新 UI 的,那么会报错吗?测试后你就会发现不会报错,但是如果你放置个 Button 点击再去更新 UI 的话则会弹出报错。为什么会这样?答案就是跟 ViewRootImpl 的初始化有关,因为在 onCreate() 的时候此时 View 还没被绘制出来,ViewRootImpl 还未创建出来,它的创建是在 activity.handleResumeActivity 的调用到 windowManager.addView(decorView) 时候,如前面说的 ViewRootImpl 才被创建起来.

    public void addView(View view, ViewGroup.LayoutParams params,            Display display, Window parentWindow) {        ...        ViewRootImpl root;        ...                   root = new ViewRootImpl(view.getContext(), display);        view.setLayoutParams(wparams);        mViews.add(view);        //ViewRootImpl保存在一个集合List中        mRoots.add(root);        mParams.add(wparams);        //ViewRootImpl开始绘制view        root.setView(view, wparams, panelParentView);        ...    }

此时创建完才会去判断线程。是不是有种让你豁然开朗的感觉!

 

另外 View 和 ViewRootImpl 是怎么绑定在一起的呢?我看看一下,首先通过 view.getViewRootImpl() 可以获取到 ViewRootImpl,代码如下:

    public ViewRootImpl getViewRootImpl() {        if (mAttachInfo != null) {            return mAttachInfo.mViewRootImpl;        }        return null;    }

而这个 AttachInfo 则是 View 里面一个静态内部类,它的构造方法如下:

AttachInfo(IWindowSession session, IWindow window, Display display,                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {            mSession = session;            mWindow = window;            mWindowToken = window.asBinder();            mDisplay = display;            mViewRootImpl = viewRootImpl;            mHandler = handler;            mRootCallbacks = effectPlayer;}

可以看到 viewRootImpl 在它的构造方法里赋值了,那么这个方法肯定是在 ViewRootImpl 创建后调用的,而 ViewRootImpl 的创建是在调用 WindowManagerGlobal.addView 的时候:

root = new ViewRootImpl(view.getContext(), display);

ViewRootImpl 构造方法代码如下:

 public ViewRootImpl(Context context, Display display) {        mContext = context;        mWindowSession = WindowManagerGlobal.getWindowSession();        ...        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);        ...    }

可以看到 View与 ViewRootImpl 绑定一起了。之后就可以通过 view.getViewRootImpl 获取到,而在 Window 里面也可以获取到 ViewRootImpl,因为 Window 里面有 DecorView(这里说的 Window 都是讲它的实现类PhoneWindo),上一篇已经介绍过了,通过 DecorView 来获取到 ViewRootImpl 的代码如下:

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

另外,一个 View 会对应一个 ViewRootImpl 吗?我们做个测试,在一个布局中打印两个不同控件的 ViewRootImpl 的内存地址,代码如下:

  Log.e(TAG, "getViewRootImpl: textView: " + tv.getViewRootImpl() );  Log.e(TAG, "getViewRootImpl: button: " + btn.getViewRootImpl() );

打印结果:

可以看到,都是同一个对象,共用一个ViewRootImpl。

针对这一小节我们来个小结:

1、之所以说 ViewRoot 是 View 和 WindowManager 的桥梁,是因为真正在操控绘制 View 的是 ViewRootImpl,View 通过 WindowManager 来转接调用 ViewRootImpl。

2、在 ViewRootImpl 未初始化创建的时候是可以进行子线程更新 UI 的,而它创建是在 activity.handleResumeActivity 方法调用,即 DecorView 被添加到 WindowManager 的时候。ViewRootImpl 绘制 View 的时候会先检查当前线程是否是主线程,是才能继续绘制下去。

 

四、Android 中点击事件的来源


在 Andriod 事件分发机制解析 一文中详细介绍了点击事件的传递流程,大致就是点击事件从 Activity 传递给 PhoneWindow,然后 PhoneWindow 再传递给 DecorView,接着 DecorView 就进行后续的遍历式的传递。这都没错,但是点击事件是如何传递给 Activity 的呢?这个大家可能不清楚吧?下面我们分析下这个问题。

首先看 Activity 的实现,Activity 实现了一个特殊的接口:Window.Callback。代码如下:

public class Activity extends ContextThemeWrapper        implements LayoutInflater.Factory2,        Window.Callback, KeyEvent.Callback,        OnCreateContextMenuListener, ComponentCallbacks2,        Window.OnWindowDismissedCallback {    private static final String TAG = "Activity";    private static final boolean DEBUG_LIFECYCLE = false;    ...}

那么 Window.Callback 到底是什么东西呢?我们看一下它的代码:

    public interface Callback {        public boolean dispatchKeyEvent(KeyEvent event);        public boolean dispatchTouchEvent(MotionEvent event);        public boolean dispatchTrackballEvent(MotionEvent event);        ...    }

然后我们似乎看出了一些端倪,难道这个接口和点击事件的传递有关?这里我们可以猜测,如果外界想要传递点击事件给 Activity,那么它就必须持有 Activity 的引用,这没错,在 Activity 的 attach() 方法中,有如下一段代码:

    final void attach(Context context, ActivityThread aThread,            Instrumentation instr, IBinder token, int ident,            Application application, Intent intent, ActivityInfo info,            CharSequence title, Activity parent, String id,            NonConfigurationInstances lastNonConfigurationInstances,            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {        attachBaseContext(context);        mFragments.attachHost(null /*parent*/);        mWindow = new PhoneWindow(this);        mWindow.setCallback(this);        mWindow.setOnWindowDismissedCallback(this);        mWindow.getLayoutInflater().setPrivateFactory(this);        ...    }

显然,mWindow 持有了 Activity 的引用,它通过 setCallback() 方法来持有 Activity。因此,事件是从 Window 传递给了 Activity。也许你会说:“我不信,这理由不充分!”,没关系,我们继续分析。我们知道,Activity 启动以后,在它的 onResume() 以后,DecorView 才开始 attach() 给 WindowManager 从而显示出来。请看 Activity 的 makeVisible 方法,代码如下:

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

接着,系统就会完成添加 Window 的过程,看 WindowManagerGlobal 的 addView() 方法,如下:

    public void addView(View view, ViewGroup.LayoutParams params,            Display display, Window parentWindow) {        ViewRootImpl root;        View panelParentView = null;        ...        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;        }    }

可以看到,ViewRootImpl 创建了,在 ViewRootImpl 的 setView() 方法(此方法运行在UI线程)中,会通过跨进程的方式向 WMS 发起一个调用,从而将 DecorView 最终添加到 Window 上。在这个过程中,ViewRootImpl、DecorView 和 WMS 会彼此相关联,同时会创建 InputChannel、InputQueue 和 WindowInputEventReceiver 来接受点击事件的消息。

好了,言归正传,下面来说,点击事件到底怎么传递给 Activity 的。首先要明白,点击事件是由用户的触摸行为所产生的,因此它必须要通过硬件来捕获,然后点击事件会交给 WMS 来处理。在 ViewRootImpl 中,有一个方法叫做 dispatchInputEvent,代码如下:

    final ViewRootHandler mHandler = new ViewRootHandler();    public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {        SomeArgs args = SomeArgs.obtain();        args.arg1 = event;        args.arg2 = receiver;        Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);        msg.setAsynchronous(true);        mHandler.sendMessage(msg);    }

那么什么是 InputEvent 呢?InputEvent 有 2 个子类:KeyEvent 和 MotionEvent,其中 KeyEvent 表示键盘事件,而 MotionEvent 表示点击事件。在上面的代码中,mHandler 是一个在 UI 线程创建的 Handler,所以它会把执行逻辑切换到 UI 线程中。这个消息的处理如下:

    final class ViewRootHandler extends Handler {        ...        @Override        public void handleMessage(Message msg) {            switch (msg.what) {                 ...                case MSG_DISPATCH_INPUT_EVENT: {                    SomeArgs args = (SomeArgs)msg.obj;                    InputEvent event = (InputEvent)args.arg1;                    InputEventReceiver receiver = (InputEventReceiver)args.arg2;                    enqueueInputEvent(event, receiver, 0, true);                    args.recycle();                } break;                ...            }        }    }

除此之外,WindowInputEventReceiver 也可以来接收点击事件的消息,同样它也有一个 dispatchInputEvent 方法,注意,WindowInputEventReceiver 中的 Looper 为 UI 线程的 Looper。代码如下:

    mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,             Looper.myLooper());    // Called from native code.    @SuppressWarnings("unused")    private void dispatchInputEvent(int seq, InputEvent event) {        mSeqMap.put(event.getSequenceNumber(), seq);        onInputEvent(event);    }    @Override    public void onInputEvent(InputEvent event) {        enqueueInputEvent(event, this, 0, true);    }

可以发现,不管是 ViewRootImpl 的 dispatchInputEvent 方法,还是 WindowInputEventReceiver 的 dispatchInputEvent 方法,它们本质上都是调用 deliverInputEvent 方法来处理点击事件的消息,如下:

    private void deliverInputEvent(QueuedInputEvent q) {        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",                q.mEvent.getSequenceNumber());        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);        }        InputStage stage;        if (q.shouldSendToSynthesizer()) {            stage = mSyntheticInputStage;        } else {            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;        }        if (stage != null) {            stage.deliver(q);        } else {            finishInputEvent(q);        }    }

在 ViewRootImpl 中,有一系列类似于 InputStage(输入事件舞台)的概念,每种 InputStage 可以处理一定的事件类型,比如 AsyncInputStage、ViewPreImeInputStage、ViewPostImeInputStage 等。当一个 InputEvent 到来时,ViewRootImpl 会寻找合适它的 InputStage 来处理。对于点击事件来说,ViewPostImeInputStage 可以处理它,ViewPostImeInputStage 中,有一个 processPointerEvent 方法,如下,它会调用 mView 的 dispatchPointerEvent 方法,注意这里的 mView 其实就是 DecorView。

        private int processPointerEvent(QueuedInputEvent q) {            final MotionEvent event = (MotionEvent)q.mEvent;            mAttachInfo.mUnbufferedDispatchRequested = false;            boolean handled = mView.dispatchPointerEvent(event);            if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {                mUnbufferedInputDispatch = true;                if (mConsumeBatchedInputScheduled) {                    scheduleConsumeBatchedInputImmediately();                }            }            return handled ? FINISH_HANDLED : FORWARD;        }

在 View 的实现中,dispatchPointerEvent 的逻辑如下:

    public final boolean dispatchPointerEvent(MotionEvent event) {        if (event.isTouchEvent()) {            return dispatchTouchEvent(event);        } else {            return dispatchGenericMotionEvent(event);        }    }

这样一来,点击事件就传递给了 DecorView 的 dispatchTouchEvent 方法。DecorView 的 dispatchTouchEvent 的实现如下,需要强调的是,DecorView 是 PhoneWindow 的内部类,还记得前面提到的 Window.Callback 吗?没错,在下面的代码中,这个 cb 对象其实就是 Activity,就这样点击事件就传递给了 Activity 了。

        public boolean dispatchTouchEvent(MotionEvent ev) {            final Callback cb = getCallback();            return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)                    : super.dispatchTouchEvent(ev);        }

 这里放一个流程图便于理解:

写一个简单的例子验证下。选择一个 View,重写其 onTouchEvent() 方法,然后通过 dumpStack() 方法来打印出当前线程的调用栈信息。

    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.d(TAG, "onTouchEvent, ev=" + event.getAction());        Thread.dumpStack();        return true;    }

log如下所示: 

D FrameLayoutEx: onTouchEvent, ev=0W System.err: java.lang.Throwable: stack dumpW System.err:    at java.lang.Thread.dumpStack(Thread.java:490)W System.err:    at com.ryg.reveallayout.ui.FrameLayoutEx.onTouchEvent(FrameLayoutEx.java:27)W System.err:    at android.view.View.dispatchTouchEvent(View.java:9294)W System.err:    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2547)W System.err:    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2240)W System.err:    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)W System.err:    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)W System.err:    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)W System.err:    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)W System.err:    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)W System.err:    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)W System.err:    at com.android.internal.policy.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2403)W System.err:    at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1737)W System.err:    at android.app.Activity.dispatchTouchEvent(Activity.java:2765)W System.err:    at com.android.internal.policy.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2364)W System.err:    at android.view.View.dispatchPointerEvent(View.java:9514)W System.err:    at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4230)W System.err:    at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4096)W System.err:    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642)W System.err:    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3695)W System.err:    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3661)W System.err:    at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3787)W System.err:    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3669)W System.err:    at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3844)W System.err:    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642)W System.err:    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3695)W System.err:    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3661)W System.err:    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3669)W System.err:    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642)W System.err:    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5922)W System.err:    at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5896)W System.err:    at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5857)W System.err:    at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6025)W System.err:    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)W System.err:    at android.os.MessageQueue.nativePollOnce(Native Method)W System.err:    at android.os.MessageQueue.next(MessageQueue.java:323)W System.err:    at android.os.Looper.loop(Looper.java:135)W System.err:    at android.app.ActivityThread.main(ActivityThread.java:5417)W System.err:    at java.lang.reflect.Method.invoke(Native Method)W System.err:    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)W System.err:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

 

更多相关文章

  1. Android(安卓)Studio Mac常用快捷键
  2. 一只不务正业的程序猿玩出个Processing交互库
  3. js判断移动端是否安装某款app的多种方法
  4. android Service相关知识点
  5. Android数据存储之三SQLite嵌入式数据库(1)
  6. 在Android(安卓)studio中调用python代码
  7. Android开发杂项总结
  8. Android中的OnMeasure及OnLayout
  9. Android进程-zygote进程

随机推荐

  1. ionic 打包成Android(安卓)apk
  2. Android(安卓)Connectivity分析(1)- Connec
  3. Lottie调研小结
  4. 【Android】结束活动退出程序的方法
  5. Android(安卓)如何更换屏幕上锁界面背景
  6. make: arm-eabi-gcc: Command not found
  7. Qt Android(安卓)环境搭建
  8. android 4.2.1 下载和编译
  9. android 测试 monkeyrunner
  10. Android的onActivityResult不被调用的解