细说Android消息机制

经过这几年的Android开发,慢慢积累了很多相关经验,这里把一些我自认为比较重要的,但网上介绍相对较少或者较浅的知识更加详细的介绍下。大的层面,从这一篇开始会介绍三个方面

  • Android系统的消息机制
  • Android的Activity
  • Android的Fragment

会从更加抽象的层面来介绍。会有一定的阅读门槛,但会较少的分析Android的源码,会重点分析原理,会把大致的脉络介绍清楚。通过这个系列可能会帮你弄明白类似:

  • 为什么我上滑一个列表,列表滚动还没有停止的时候我突然下滑界面能马上开始下滑。
  • Activity都有生命周期回调方法,这些方法是怎么被回调的?入口在哪儿?
  • Fragment究竟是什么?
  • Fragment究竟是怎么被显示出来的?
  • DialogFragment是什么?为什么DialogFragment能正确恢复?
  • 臭名昭著的FragmentStateLoss是怎么来的?

等等类似上面这些问题。会比较深入的从根本上把他们介绍清楚。

从显示HelloWorld说起

介绍消息机制之前,我们先看下面这个简单的代码。

public class Main {

    public static void main(String[] args) {
        System.out.println(">>>onCreate");

        displayHelloWorld();

        System.out.println("<<<onDestroy");
    }

    private static void displayHelloWorld() {
        System.out.println("HelloWorld");
    }
}

我们把上面的代码来对应到Android里。假设在Android里点击一个App后会执行上面main方法。那么就是执行:

System.out.println(">>>onCreate"); // 类似Activity的onCreate

然后执行:

displayHelloWorld(); // 显示Android的TextView,内容:HelloWorld。

最后执行:

System.out.println("<<< onDestroy"); // 类似Activity的onDestroy 执行这一行后界面将不再显示

好了,代码非常简单,思考下这样对应后,我们能看到HelloWorld界面吗?

displayHelloWorld();

被执行完,我们能看到HelloWorld界面,没问题,但关键是紧接着又继续执行了:

System.out.println("<<<onDestory"); // 执行这一行后界面将不再显示

然后程序就退出了。在Android里就相当于HelloWorld界面一闪而过,这肯定不是我们想要的结果。从C入门的朋友还记得写完HelloWorld后,命令提示符界面突然一闪而过吗?你还记得是怎么处理的吗?
怎么办?sleep是否可以?

public class Main {

    public static void main(String[] args) {
        System.out.println(">>>onCreate");

        displayHelloWorld();

        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("<<<onDestroy");
    }

    private static void displayHelloWorld() {
        System.out.println("HelloWorld");
    }
}

改造成上面的代码后,似乎是可以了,最后的onDestroy不会被执行,因为在sleep那里就停下来了。这代码看着虽然很不舒服,但显示HelloWrold似乎是没什么问题了。
但光显示HelloWorld太单调了。假如需求变成了用户从键盘输什么就显示什么怎么办?


import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        System.out.println(">>>onCreate");

        // display("HelloWorld");
        display(readInput());

        Thread.sleep(Integer.MAX_VALUE);
        System.out.println("<<<onDestroy");
    }

    private static void display(String s) {
        System.out.println(s);
    }

    private static String readInput() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s = br.readLine();
        br.close();
        return s;
    }
}

把display方法改造下,把显示内容变成参数。然后增加一个读取输入的方法。试下会发现只能输入一次不能不停的输入。再改造下:

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        System.out.println(">>>onCreate");

        while (true) {
            display(readInput());
        }

        // Thread.sleep(Integer.MAX_VALUE);
        // System.out.println("<<<onDestroy");
    }

    private static void display(String s) {
        System.out.println(s);
    }

    private static String readInput() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s = br.readLine();
        br.close();
        return s;
    }
}

我们用一个死循环(有没有感觉到消息循环要来了?还记得什么是软中断吗?),不停的读取,这样连sleep都不需要了。
到这里我们实现了一个Android界面能实时显示键盘输入的文字。
那假如我们需求又变化了,我们不光要显示问题还做其他操作,比如运算啊,显示图片啊等等。这好办我们把diaplay包装成一个Runnable,你想干嘛干嘛。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

    private static Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            display(readInput());
        }
    };

    public static void main(String[] args) {
        System.out.println(">>>onCreate");

        while (true) {
            sRunnable.run();
        }
    }

    private static void display(String s) {
        System.out.println(s);
    }

    private static String readInput() {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            String s = br.readLine();
            br.close();
            return s;
        } catch (IOException e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }
}

这样修改sRunnable的run方法你想干嘛干嘛。但问题又来了,我们不是只执行一种操作,我们希望能执行操作,我们希望被执行的Runnable是个List,我们能随意向List添加Runnable。继续改造:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class Main {

    private static List<Runnable> sTaskQueue = new ArrayList<>();

    public static void main(String[] args) {
        System.out.println(">>>onCreate");

        sTaskQueue.add(new Runnable() {
            @Override
            public void run() {
                display(readInput());
            }
        });
        sTaskQueue.add(new Runnable() {
            @Override
            public void run() {
                System.out.println("Draw bitmap");
            }
        });
        sTaskQueue.add(new Runnable() {
            @Override
            public void run() {
                System.out.println("Open https://www.google.com");
            }
        });

        while (true) {
            for (Runnable runnable : sTaskQueue) {
                runnable.run();
            }
        }
    }

    private static void display(String s) {
        System.out.println(s);
    }

    private static String readInput() {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            String s = br.readLine();
            br.close();
            return s;
        } catch (IOException e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }
}

这下好了,能执行多种操作了,不光能显示TextView还能绘制bitmap,打开WebView。但运行后发现还有问题,任务一直被重复执行,任务执行后我们应该把任务从TaskQueue从移除。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {

    private static Queue<Runnable> sTaskQueue = new LinkedList<>();

    public static void main(String[] args) {
        System.out.println(">>>onCreate");

        sTaskQueue.add(new Runnable() {
            @Override
            public void run() {
                display(readInput());
            }
        });
        sTaskQueue.add(new Runnable() {
            @Override
            public void run() {
                System.out.println("Draw bitmap");
            }
        });
        sTaskQueue.add(new Runnable() {
            @Override
            public void run() {
                System.out.println("Open https://www.google.com");
            }
        });

        while (!sTaskQueue.isEmpty()) {
            Runnable task = sTaskQueue.poll();
            task.run();
        }

        System.out.println("<<<onDestroy");
    }

    private static void display(String s) {
        System.out.println(s);
    }

    private static String readInput() {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            String s = br.readLine();
            br.close();
            return s;
        } catch (IOException e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }
}

用Queue的数据结构,很方便避免了任务重复执行。这里还有个问题,我们肯定是希望任务能随时被添加,而不应该是像上面一样被固定死了显示字符,绘制bitmap,打开WebView。继续改造:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {

    private static Queue<Runnable> sTaskQueue = new LinkedList<>();

    private static void addTask(Runnable runnable) {
        sTaskQueue.add(runnable);
    }

    public static void main(String[] args) throws Exception {
        System.out.println(">>>onCreate");

        addTask(() -> display(readInput()));
        addTask(() -> System.out.println("Draw bitmap"));
        addTask(() -> System.out.println("Open https://www.google.com"));

        startTaskThread();

        while (true) {
            Thread.sleep(1000);

            if (!sTaskQueue.isEmpty()) {
                Runnable task = sTaskQueue.poll();
                task.run();
            }
        }

        // System.out.println("<<<onDestroy");
    }

    private static void startTaskThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        long millis = (long) (2000 + Math.random() * 3000);
                        Thread.sleep(millis);

                        addTask(new Runnable() {
                            @Override
                            public void run() {
                                System.out.println("Random task: " + millis);
                            }
                        });
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    private static void display(String s) {
        System.out.println(s);
    }

    private static String readInput() {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            String s = br.readLine();
            br.close();
            return s;
        } catch (IOException e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }
}

添加了一个startTaskThread方法,用来运行时创建任务。在Android里startTaskThread有点类似与点击Button的事件。可以是用户滑动了界面还是,或者是任意时刻点击Button等等任意操作。startTaskThread效果就是现在的代码结构能简单的实现界面不退出,然后不停的执行新来的任务。好像基本实现了我们想要的效果。
按照这个模型,我们其实可以构建出任意复杂的App。只要我们把这些复杂的操作都包装成Runnable添加到sTaskQueue即可。

上面的代码其实就是Android的Handler Message MessageQueue Looper的模型。上面代码加以改造,变成Android的Api就是这里的实现https://www.jianshu.com/p/5b8d2f8d4f8d

大家对Handler的理解通常都是说跟主线程通信,在主线程更新UI。事实上跟主线程通信只是很小的一个方面,Handler Message MessageQueue Looper更重要是实现了Android的消息循环。如果你看了上面的Handler的二次实现,而且自己也能独立再写一个,那肯定会对Android的消息机制有个非常深刻的理解。

什么是消息机制

到了这里我们再看什么消息机制(也可以叫消息循环)应该就很容易理解了。消息循环其实就是把一个线程需要执行的任务分割成若干的消息,然后线程一个个把这些任务包装成的消息去循环往复的执行,消息可以在任意时刻被添加。

为什么需要消息机制

如果你的软件不是类似科学计算这样的,不需要随时响应外部事件的软件的划是不需要消息机制的,但如果你的软件是需要随时响应外界操作(比如触摸操作,定时任务),那么你就一定需要用到中断(响应触摸操作就是消息机制实现的软中断,定时任务的定时由时钟硬件中断触发,然后被包装成消息交给消息机制处理),来响应外界操作。这个中断在软件中表现其实就是消息机制。听着似乎很绕。举个例子就非常清楚来。就比如Android中的触摸操作。
当用户手指触摸到一个Button区域的屏幕的时候,触摸屏硬件会通过驱动把事件传递给操作系统,操作系统把事件传递给当前正在运行的App,然后App的消息队列会被插入一个包含触摸信息的消息,然后消息被实际执行,这时候你用来响应触摸事件的代码执行时间应该尽量少,最好小于16ms(1000ms/60hz=16.666ms)。那假如我响应触摸事件的行为是一个View从左到右移动1600个像素,持续1600ms的动画怎么办?1600ms明显是远大于16ms的。这时候动画应该被分割成100个小消息,每个消息把View移动16个像素,这背后会有很多需要处理的细节,不过Android自带的动画框架可以辅助你非常简单的实现这个过程。这样就能实时响应用户的操作了。怎么响应呢?比如现在是触摸后的第160ms,这时候我又点击了一次Button,那这时候我希望的操作是取消动画。实际会怎么被执行呢?
第160ms时候消息循环正在执行之前的移动动画,这时候已经移动了160个像素,这时候触摸屏把触摸事件传递给了操作系统,操作系统把事件分发给App,App的消息队列被插入一个新消息,然后消息被执行,执行的实际操作就是animator.cancel()动画被取消。从用户角度看屏幕实时响应了用户的操作请求。

我们再来思考下假如没有消息机制,这个把View移动1600像素,持续1600ms的动画会有什么问题?如果没有把这个持续1600ms的操作分割成很多小消息,会导致这1600ms内无法响应用户操作。因为进程需要1600ms把整个代码处理完成才能继续处理新代码。

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

推荐阅读更多精彩内容