经过这几年的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把整个代码处理完成才能继续处理新代码。