Android Handler异步消息机制的原理分析

前言

 我们知道android从4.0开始,就不允许非UI线程来更新UI了。看一段代码

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

这段代码运行一切正常,是不是有些疑惑?
再看下一段代码

 tv = (TextView) findViewById(R.id.tv);
        new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tv.setVisibility(View.GONE);
            }
        }.start();

现象如下

下面源码分析一下:

在源码中我们看到下面的代码

    private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
        if (text == null) {
            text = "";
        }
 
        mBufferType = type;
        mText = text;
 
        //省略代码
 
 
        if (mLayout != null) {
            checkForRelayout();
        }
 
        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);
 
       //省略代码
        
        if (needEditableForNotification) {
            sendAfterTextChanged((Editable) text);
        }
 
 
    }

当mLayout不为空时 执行checkForRelayout(),mLayout是在onMeasure方法中获取的,所以图一代码没有崩溃的可能原因是TextView还未被加入到TextTree当中。

下面看下checkForRelayout的源码

    /**
     * Check whether entirely new text requires a new view layout
     * or merely a new text layout.
     */
    @UnsupportedAppUsage
    private void checkForRelayout() {
        // If we have a fixed width, we can just swap in a new text layout
        // if the text height stays the same or if the view height is fixed.

        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            // Static width, so try making a new text layout.

            int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            /*
             * No need to bring the text into view, since the size is not
             * changing (unless we do the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

总体意思是说,当TextView不是固定的宽高时,会根据字符大小和数量,TextView会动态计算宽高,否则直接绘制即可。那么图一没有闪退的另一个原因是如果一个TextView 设置的是固定宽高 这个时候在设置Text的时候 就不需要requestLayout,所以就不会触发checkThread()方法。

虽然没有调用requestLayout,但是调用了invalidate()。

from View.class   
 public void invalidate() {
        invalidate(true);
    }
 
    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
 
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
 
            。。。。。。。。。。。。。
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
 
            。。。。。。。。。。。。。
        }
    }

最终走到了ViewParent的invalidateChild 方法中

from ViewGroup.class   
 public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        //开启硬件加速
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            // HW accelerated fast path 
            onDescendantInvalidated(child, child);
            return;
        }
         //不开启硬件加速
        .................................................................
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
                parent = parent.invalidateChildInParent(location, dirty);
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) Math.floor(boundingRect.left),
                                (int) Math.floor(boundingRect.top),
                                (int) Math.ceil(boundingRect.right),
                                (int) Math.ceil(boundingRect.bottom));
                    }
                }
            } while (parent != null);
        }
    }

从这段代码里面我们可以看到

其实在invalidateChild的过程中是分两种情况的,开启硬件加速和不开启硬件加速

先看不开启硬件加速的场景,主要就是走了 parent.invalidateChildInParent ,这是一个do while 循环

    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            .............
            return mParent;
        }
        return null;
    }

invalidateChildInParent 方法会将自己的mParent 返回 并进入下一次循环,这样的最终就会走到ViewRootImpl的invalidateChildInParent ,然后就是checkThread 再然后就是抛出异常

于是我们将demo中的硬件加速关闭,来测试这种场景,果然抛出了异常,证明在非硬件加速情况下这种流程是对的。

总结:

在子线程中给TextView setText 不会抛出异常的两个场景

1:TextView 还没来得及加入到ViewTree中

2:TextView已经被加入了ViewTree,但是被设置了固定宽高,且开启了硬件加速

子线程操作View 确实不一定导致Crash,那是因为刚好满足一定的条件并没有触发checkThread机制,但这并不代表我们在开发过程中可以这么写,谨记。


本文参考了:Android 子线程更新TextView的text 不抛出异常原因 分析总结_冰河514057946的博客-CSDN博客

言归正传,android不建议我们在非UI线程更新UI,也不建议在UI线程做一些耗时的操作。那我们如果有类似的处理完一些逻辑之后要更新UI的需求该怎么办呢?那么就要用到Handle机制了。

1.概念

在了解Handler之前我们先了解下几个相关的类

  1. Message消息,理解为线程间通讯的数据单元。例如后台线程在处理数据完毕后需要更新UI,则可发送一条包含更新信息的Message给UI线程。
  2. MessageQueue 消息队列,用来存放通过Handler发布的消息,按照先进先出执行。
  3. Handler Handler是Message的主要处理者,负责将Message添加到消息队列以及对消息队列中的Message进行处理。
  4. Looper循环器,扮演Message Queue和Handler之间桥梁的角色,循环取出Message Queue里面的Message,并交付给相应的Handler进行处理。
  5. 线程 UI thread 通常就是main thread,而Android启动程序时会替它建立一个Message Queue。每一个线程里可含有一个Looper对象以及一个MessageQueue数据结构。在你的应用程序里,可以定义Handler的子类别来接收Looper所送出的消息。

2.什么是Handler

  1. Handler是一套 Android 消息传递机制,主要用于线程间通信。

3.主线程使用Handler和子线程使用Handler的区别

子线程

new Thread() {
			public void run() {
				Looper.prepare();
				mHandlerTest1=new HandlerTest1(Looper.myLooper());
				Message message = new Message();
				message.obj = "子线程发送的消息";
				mHandlerTest1.sendMessage(message);
				Looper.loop();
			};
		}.start();

上面是一般使用在子线程创建和使用Handler的代码段,我们对比以前在主线程创建和使用Handler的代码,可以发现基本差不多,但是有一点不一样,那就是在子线程中多了下面这两行代码:

Looper.prepare();
Looper.loop();

 看下prepare

就是子线程需要prepare,可以直接理解为组要准备当前线程的looper。就是说子线程没有looper,那我们看看我们怎么准备looper的?

/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
    prepare(true);
}
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
}
    sThreadLocal.set(new Looper(quitAllowed));
}

源码的注释也说得很清楚,就是创建handler需要先有looper。prepare()方法里面是调用了prepare(boolean quitAllowed)方法,后面这个方法比前面就多了个参数,这个参数【quitAllowed】看字面意思可以看出作用就是:允不允许looper退出,从代码中我们可以知道子线程的looper是可以退出的;然后在prepare(boolean quitAllowed)中首先检查时候已经存在looper了,如果已经含有了looper,那就报错,这下我们就知道了Android是怎么保证一个线程只有一个looper了,如果当前线程还不存在looper,那就直接new一个新的looper,然后将创建的looper塞进sThreadLocal中。

loop

那么子线程中loop()方法是什么呢?这个loop()方法就是在当前线程中运行消息队列,并切在结束时需要调用quit()方法.我们接着针对loop()看看源码里面做了什么:

/**
    * Run the message queue in this thread. Be sure to call
    * {@link #quit()} to end the loop.
    */
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
 
    for (;;) {
        Message msg = queue.next(); // might block
        ...
        try {
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } 
        ....
        msg.recycleUnchecked();
    }
}

在loop()方法中主要就是从消息队列中获取消息,然后执行信息分发【这个具体先不深入,我们只要知道loop()的主要工作就行】,对比一下我们在主线程中使用Handler的情况,在子线程中使用Handler很像是条件不好人要自立更生艰苦奋斗,没有条件就自己创造条件。

主线程使用Handler

我们看了子线程中如何使用Handle和相关源码分析,接下来再看看主线程中我们是怎么使用handler的?根据以前的经验,主线程创建使用Handler就是直接创建的,并没有其他的操作:

public Handler mHandler;
mHandler = new Handler() {
    public void handleMessage(Message msg) {
       // process incoming messages here
    }
};

相比子线程,使用时少了prepare()和loop();但是真的是主线程不需要调用这些方法吗?显然不是的,子线程有的主线程也不能少,那接下来我们看看是谁帮了我们的忙?

prepareMainLooper

那我们就看看这个prepareMainLooper()方法是做了什么:

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

这个prepareMainLooper()里面就是调用了prepare()方法,但是传入的参数和在子线程中不同,这里这传入的是false,也就是说主线程的looper是不允许退出的;到这里,我们知道prepareMainLooper将当前线程初始化为循环程序,将其标记为应用程序的主循环程序。应用程序的主循环是由android环境创建的,因此用户不必自己调用此函数
 

 loop

在prepareMainLooper()方法后也调用了loop()方法,看到这里,我们就明白了主线程在使用handler的条件在创建程序之初就已经具备,所以让用户在主线程中使用handler也方便很多。

4.面试常问

4.1  一个线程可以创建多个Handler,但只能创建一个Looper,一个MessageQueue

4.2 Handle中Looper.loop()的死循环为什么在主线程中不会产生卡死现象

 答:Looper.loop()的死循环正是维护了主线程的超长生命周期,loop方法一直循环处理任务,没有任务的时候会休眠,有任务的时候会唤醒然后进行处理,所以也不会占用太多系统资源。

最后本人推荐的一种Handler的写法

  静态内部类+虚引用

 tv.setOnClickListener(view -> {
            mHandler.removeCallbacksAndMessages(null);
            Message msg = Message.obtain(); // 实例化消息对象
            msg.what = 1; // 消息标识
            msg.obj = "获取到消息了"; // 消息内容存放
            mHandler.sendMessage(msg);
        });

    private Handler mHandler = new MyHandler(this);

    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                MainActivity mainActivity = mActivity.get();
                mainActivity.tv.setText(msg.obj.toString());
            }
        }
    }

最后tv显示 "获取到消息了"。

总结
1.也可以在子线程创建handler,需要提前prepare looper和自己调用loop。

2.主线程和子线程的区别:主线程的looper创建完后不可以退出,子线程创建的looper是可以退出的,需要自己去prepare()和loop(),主线程的handler环境在进程启动的时候就创建好了。
参考了:子线程和主线程创建使用Handler不同处的源码分析_小文21的博客-CSDN博客

Android 子线程更新TextView的text 不抛出异常原因 分析总结_冰河514057946的博客-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值