Android源码解析Handler系列第(一)篇 --- Message全局池

转载请注明文章出处LooperJing

1、UI不能在子线程中更新是个伪命题

我们常说UI需要在主线程中进行更新,子线程就不能更新UI吗?不是,我们并不是说不能在子线程中更新UI,而是说UI必须要在它的创建线程中进行更新,比如下面一段代码在子线程更新UI就不会报错。

    new Thread(new Runnable() {
            @Override
            public void run() {
                TextView  textView=new TextView(MainActivity.this);
                textView.setText("Hello");
            }
     }).start();

OK,那我偏偏不在创建线程中进行更新,如下!view在主线程中创建,在子线程中add一个Button。

     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final LinearLayout view = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main, null, false);
        setContentView(view);

        new Thread(new Runnable() {
            @Override
            public void run() {
                Button btn=new Button(MainActivity.this);
                btn.setText("Hello");
                view.addView(btn);
            }
        }).start();
    }

运行后,Button被顺利的加到了根布局中。尼玛,“UI必须要在它的创建线程中进行更新”这种说法也不对啊!!!不是不对,而是欠妥,我修改一下代码!

     new Thread(new Runnable() {
            @Override
            public void run() {
                Button btn=new Button(MainActivity.this);
                btn.setText("Hello");
                SystemClock.sleep(2000);
                view.addView(btn);
            }
        }).start();

就是在addView之前,睡眠了一会,结果还是抛出了 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.这个错误!通常这种错误,就要用Handler向UI线程发送消息来更新!这个后面会对源码进行分析。说了半天,进入正题,我今天要讨论的是Message。要掌握Handler,先对Handler发送的Message有所了解。

2、Message的初步认识

Message就是一个实现了Parcelable的java类,我的第一感觉是它可以可以用在进程间通信(IPC),可以用在网络中传输,看一下它比较重要的几个字段。先来几个熟悉的。

    //当有多个Handler发消息的时候,或者一个Handler发多个消息的时候,可以用what标识一个唯一的消息
   public int what;

    //当传递整形数据的时候,不需要使用setData(Bundle)去设置数据,使用arg1,arg2开销更小。
    public int arg1; 

    public int arg2;

    //Message所携带的数据对象
    public Object obj;

这几个都是很常用的,要深入了解Message,不得不了解下面几个。

    //flags表示这个Message有没有在使用,1表示在池中,等待复用,0表示正在被使用,
     int flags;

    //Message发送之后,何时才能被处理
     long when;
    
   // Message所携带的数据 
     Bundle data;

    //表示这个消息被哪个Handler处理
     Handler target;
    
    //我们用Handler.post一个Runnable,这个Runnable也是被包装成Message对象的
     Runnable callback;
    
    // 作为消息链表所用的一个成员
    Message next;

    //sPoolSync是对象锁,因为Message.obtain方法会在任意线程调用
    private static final Object sPoolSync = new Object();

    //sPool代表接下来要被重用的Message对象
    private static Message sPool;

    //sPoolSize表示有多少个可以被重用的对象
    private static int sPoolSize = 0;

    //MAX_POOL_SIZE是pool的上限,这里hardcode是50
    private static final int MAX_POOL_SIZE = 50;

对于上面列举的,挑几个重点分析一下。

Runable是怎么被封装到Message中的?我们常为会延迟做某个操作写出下面的代码

handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                
            }
        },1000);
跟踪进去发现在postDelayed中调用了getPostMessage
   private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

所以这个run方法只是一个普通的回调而已,千万不要认为他是在另外一个线程中。刚说了Message中的target表示了这个消息要被哪一个Handler所处理,对于Message是怎么被处理的呢?尽管后面要分析Handler源码,这里还是提一下。

Message的处理顺序
Looper.loop()方法中Message被从MessageQueue取出来后会调用msg.target.dispatchMessage(msg)

   /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

在dispatchMessage方法中,检查是否有由Runnable封装的消息,如果有,首先处理;其次处理的是mCallback,mCallback是什么?mCallback是Callback的对象,Callback是Handler中的一个内部接口,这个接口的作用是,当你实例化一个Handler对象的时候,可以避免不去实现Handler的handleMessage方法。

    /**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     *
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    public interface Callback {
        public boolean handleMessage(Message msg);
    }

从消息的分发可以看到,如果返回了true,那么handler中的handleMessge方法是不会被执行的,如果返回false或者mCallback没有被赋值,那么就会回调Handlerd的handleMessge,这就是一个Message的分发流程。了解了Message的分发之后,那么Message是怎么被创建的呢。

3、Message全局池

ANDROID系统中很多操作都是靠发消息完成的,比如按下返回键就是一个消息,这样系统会不会构建大量的Message对象呢?上面看到getPostMessage方法中,以 Message.obtain()的方式获取一个消息,而不是通过它的构造函数,获取消息的代码很简单。

 /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

有一个关键词是sPool,初步感觉是池的意思,别忘记了,在第一节Message的初步认识中,sPool是一个Message对象 ,这里有点蒙逼,一个对象起个名字跟池有什么关系。回顾一下,Message还有一个成员是 next。

// sometimes we store linked lists of these things
   Message next;

这样每一个Message对象都有一个next指针指向下一个可用的Message,这不就是大学数据结构中的链表嘛!所以一个消息池的结构是这样的。

Message链表

既然知道Message有一个消息池(我们通常称这个消息池为全局池)的机制,那么它设计初衷肯定是要做到复用的,那么还有一个问题,Message何时被入池,何时出池?(MessageQueue虽然是存储消息的,但要弄清楚,这里说的消息池跟MessageQueue并没有什么关系。)

消息要入池,也就是这个消息被回收到池中,等待复用,所以我们大胆猜测这个消息肯定不在使用之中,如果这个消息正在使用之中,是肯定不会把它放到全局池里面的,也就是说只有这个消息完成了它的使命,系统才能把它回收到全局池中。通过这样的分析,消息入池不是在它的创建阶段,而是在回收阶段。直接看代码吧。Message中有一个recycle的方法。

 /**
     * Return a Message instance to the global pool.
     * <p>
     * You MUST NOT touch the Message after calling this function because it has
     * effectively been freed.  It is an error to recycle a message that is currently
     * enqueued or that is in the process of being delivered to a Handler.
     * </p>
     */
    public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

首先判断消息是否在使用之中

    boolean isInUse() {
        return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
    }

如果在使用之中,继续判断gCheckRecycle,gCheckRecycle的默认值是true,这个一个与版本相关的常量,在5.0之后的版本这个值是false,不知道作什么使用,有谁知道可以告诉我一下。在此不纠结了。如果不在使用之中,最后会走进recycleUnchecked。

/**
     * Recycles a Message that may be in-use.
     * Used internally by the MessageQueue and Looper when disposing of queued Messages.
     */
    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

这段代码也不是很长,很好理解,一开始,把这个消息所有成员赋值成最初的状态,FLAG_IN_USE的值是1一开始说了Message的flags表示这个Message有没有在使用,1表示在池中,等待复用,0表示正在被使用。重点看同步锁中的代码。
假设全局池没有元素时,我们将第一个消息放到池中,sPool一开始是NULL,next指向了sPool,所以此时的消息的sPool和next都是NULL,然后sPool指向当前的Message对象,最后池的数量加1。大致如下图。


全局池中只有一个消息等待被复用

假设有来个消息m2,在走一遍同步锁中的代码,此时全局池的状态如下图所示。

全局池中两个消息等待被复用

同理,三个消息的时候,是这样

全局池中三个消息等待被复用

看到这,相信你已经知道了消息是怎么加到全局池中的,那么何时出池呢?再看消息的获取代码,尝试找出消息何时出池的。

 /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

obtain经常使用在多线程之中,所以用sPoolSync作为同步锁,第一次sPool为空,会new一个消息返回,这new的消息会在回收的时候,会被加到全局池中。如果sPool不为空,sPool是什么?sPool是指向全局池的头指针,sPool不为空,说明了全局池中有元素。把sPool赋值给一个Message对象m,同时全局池的头指针向后移,指向下一个被复用的消息,然后把m的flags赋值为0,表示这个消息被复用了,池中元素数量减1。经过这个逻辑,上图中的消息m3是不是就被出池了呢?

消息m3离开全局池

OK,本文分析到这里,一个Message大部分内容都被分析了,我们知道了Message内部有一个全局池,保证了开发者可以不构建大量的消息,提高性能。我们也知道了一个消息何时入池,何时出池。OK,Message复用机制到此结束。

Please accept mybest wishes for your happiness andsuccess

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

推荐阅读更多精彩内容