零基础学Android源码之Handler机制

前言

当我们在非主线程想操作UI时,其中一个方法便是使用Handler。Handler作为Android中的异步消息处理机制,学会使用它是十分重要的,其次也是面试中的常客。对于Android新手来说,在不理解源码的情况下使用起来也是云里雾里,今天就和大家一起探讨一下Handler的实现原理,也让大家有一个更清晰的理解。

代码示例

首先我们看下handler是怎么使用的

private TextView mTextView;

private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        mTextView.setText(msg.obj.toString());
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTextView = (TextView) findViewById(R.id.text);

    new Thread(new Runnable() {
        @Override
        public void run() {
            //耗时操作获取数据
            String data  = "test data";
            Message msg = Message.obtain();
            msg.obj = data;
            mHandler.sendMessage(msg);
        }
    }).start();
}

代码看起来很简单,创建一个handler,之后使用handler发送message,从这两个点我们去分析下代码。

源码分析

看源码之前,我们先对以下几个类有个初步映像,这几个类就是支撑起handler的关键

  • Handler
  • Looper
  • MessageQueue
  • Message

接下来我会把handler机制分为两部分来讲

  • handler的准备
  • handler发送消息

1. handler的准备

有人可能会说,handler的准备不就是new handler的时候吗?这个其实只是准备的其中一个步骤,一切的准备则是要从main函数说起。

我们都知道,java的入口函数是main函数,新手可能会认为Android的入口是主Activity,实际上也是main函数,这个函数的实现是在ThreadActivity这个类中,我们一起看下

public static void main(String[] args) {
   
    ......

    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

找一下我们之前说到的关键字,可以看到有一个Looper,Looper用来干什么的呢?如其字面意思环,有了它线程就可以循环的去读取数据。我们先看下两个关键的方法Looper.prepareMainLooper()Looper.loop()

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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

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));
}

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

Looper.prepareMainLooper()方法中,调用了prepare()方法,这个方法给sThreadLocal赋值,sThreadLocal是一个ThreadLocal<Looper>对象,那么ThreadLocal又是什么呢,我们可以把它理解为线程中的一个容器,这里new 了一个Looper并把它放在当前线程的容器里,代码中做了判断如果不为空就抛出一个异常,说明Looper对象只有一个。此时,当前线程就和Looper有了关联,一旦线程从自己的容器中取出Looper时,就可以使用Looper来循环读取消息。因为是在main函数中调用的,所以是把Looper放在了主线程即UI线程的容器里,这里一定要记住。

再来看看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;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

       ......

        msg.recycleUnchecked();
    }
}

loop()方法中,先从当前线程的容器内把Looper对象取出来,没错,当前线程要开始取数据啦,但是数据从哪里来呢?Looper对象里有一个MessageQueue,这里简单介绍下MessageQueue,MessageQueue是一个单向链表,链表可以简单的理解为一个队列,但是这个队列是单向的,每一个对象有一个变量指向了下一个对象。源码中拿到MessageQueue后,便进入了一个死循环,不断的调用next()方法,取出MessageQueue中的Message。好了关键点来了,拿到的messgae调用了msg.target.dispatchMessage(msg)而这个target在Message的源码中是这样的Handler target;也就是说target就是一个handler,我们再看下dispatchMessage这个方法干了什么。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

首先判断有没有回调,没有则调用handleMessage(msg);是不是很熟悉,这个不就是我们在new handler的时候重写的方法吗?没错,走到这一步的时候handler就会回调我们重写的方法,此时handler就准备完成啦。(这里在简单提一下handleCallback,这个是在handler.post()时回调的,原理和本文讲的类似,就不在累述,有兴趣的童鞋可以自己去看下源码)那又有一个疑问了,源码中最后调用了msg.target.dispatchMessage(msg),但是messgae怎么会有target呢,这个就是我们要分析的第二个过程,handler发送消息。

2. handler发送消息

当我们想使用handler的时候,会调用handler.sendMessage(msg);看下源码

public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

最终代码会走到enqueueMessage方法中,这个方法第一句代码便是msg.target = this; this对象便是我们new出来的handler,此时messgae便拿到了handler,有handler的message便可以顺利调用msg.target.dispatchMessage(msg)至此,handler的整个流程就走完了。

总结

最后再简单理一遍思路

  1. 在调用prepare时,Looper中有一个ThreadLocal对象,这个对象能够把Looper这个对象保存在当前线程中,且只会保存一次。
  2. 在调用loop的时候,Looper会从当前线程中把Looper对象取出来,并从这个对象中取出MessageQueue,接下来会进入一个死循环,不断的从这个MessageQueue中读取Message并调用msg.target.dispatchMessage(msg)该方法最终会调用我们重写的handlerMessage()
  3. 这个msg.target就是一个handler,而这个handler则是在用户发送messgae时传递进来的,即:handler.sendMessage(msg),在send的时候msg.target=this此时便把handler传递进来了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容