Android程序员面试会遇到的算法系列:java
很久没有更新了,前段时间由于签证的问题一直很闹心因此没有写东西。数据结构
今天虽然依然没有好消息,并且按照往年的数据,如今还抽不中H1b的估计都没戏了,也可能个人硅谷梦就会就此破灭。。。并发
可是想了想,生活还得继续,学习不能停下。我仍是要按照正常的节奏来。
这一期就主要给你们介绍在安卓应用或者轮子中最多见的一个设计,就是消息队列。
我此次会以一个简单的例子来一步步的展现消息队列这种设计的应用,最后会借鉴Java和安卓源码中对消息队列实现的实例来作一个简化版的代码,但愿你们在看完这篇文章以后在本身从此的app开发,或者轮子开发中能利用消息队列设计来优化代码结构,让代码更加可读。
相信大部分安卓开发者都有用过这个叫Volley的网络请求库,底层的网络请求其实是用HttpUrlConnection类或者HttpClient这个库作的。Volley在这些基础库上作了封装,例如线程的控制,缓存和回调。这里咱们详细说说大部分网络请求队列的处理。
一个最基本最简单的设计是,使用一个线程(非主线程),不停的从一个队列中获取请求,处理完毕以后从队列抛出而且发射回调,回调确保在主线程运行。
实现起来很是简单,这里借鉴Volley源码的设计,简化一下:
/** 简化版本的请求类,包含请求的Url和一个Runnable 回调 **/
class Request{
public String requestUrl;
public Runnable callback;
public Request(String url, Runnable callback) {
this.requestUrl = url;
this.callback = callback;
}
}
//消息队列
Queue<Request> requestQueue = new LinkedList<Request>();
new Thread( new Runnable(){
public void run(){
//启动一个新的线程,用一个True的while循环不停的从队列里面获取第一个request而且处理
while(true){
if( !requestQueue.isEmpty() ){
Request request = requestQueue.poll();
String response = // 处理request 的 url,这一步将是耗时的操做,省略细节
new Handler( Looper.getMainLooper() ).post( request.callback )
}
}
}
}).start();
复制代码
上面这一系列代码就把咱们的准备工做作好了。那么往这个傻瓜版轮子里面添加一个请求就很是简单了。
requestQueue.add( new Request("http.....", new Runnable( -> //do something )) );
复制代码
就这样,一个简化版的网络请求的轮子就完成了,是否是很简单,虽然咱们没有考虑同步,缓存等问题,但其实看过Volley源码的朋友也应该清楚,Volley的核心就是这样的队列,只不过不是一个队列,而是两种队列(一个队列真正的进行网络请求,一个是尝试从缓存中找对应request的返回内容)
代码的核心也就是用while循环不停的弹出请求,再处理而已。
消息队列的还有一种玩法就是发送延迟消息,好比说我想控制当前发送的消息在三秒以后处理,那这样应该怎么写咱们的代码呢,毕竟在网络请求的例子里面,咱们彻底不在意消息的执行顺序,把请求丢进队列以后就就开始等待回调了。
这个时候咱们能够采用链表这个数据结构来取代队列(固然Java里面链表能够做为队列的实例),按照每一个请求或者消息的执行时间进行排序。
废话很少说,先上简版代码。
//一个消息的类结构,除了runnable,还有一个该Message须要被执行的时间execTime,两个引用,指向该Message在链表中的前任节点和后继节点。
public class Message{
public long execTime = -1;
public Runnable task;
public Message prev;
public Message next;
public Message(Runnable runnable, long milliSec){
this.task = runnable;
this.execTime = milliSec;
}
}
public class MessageQueue{
//维持两个dummy的头和尾做为咱们消息链表的头和尾,这样作的好处是当咱们插入新Message时,不须要考虑头尾为Null的状况,这样代码写起来更加简洁,也是一个小技巧。
//头的执行时间设置为-1,尾是Long的最大值,这样能够保证其余正常的Message确定会落在这两个点之间。
private Message head = new Message(null,-1);
private Message tail = new Message(null,Long.MAX_VALUE);
public void run(){
new Thread( new Runnable(){
public void run(){
//用死循环来不停处理消息
while(true){
//这里是关键,当头不是dummy头,而且当前时间是大于或者等于头节点的执行时间的时候,咱们能够执行头节点的任务task。
if( head.next != tail && System.currentTimeMillis()>= head.next.execTime ){
//执行的过程须要把头结点拿出来而且从链表结构中删除
Message current = head.next;
Message next = current.next;
current.task.run();
current.next = null;
current.prev =null;
head.next = next;
next.prev = head;
}
}
}
}).start();
}
public void post(Runnable task){
//若是是纯post,那么把消息放在最尾部
Message message = new Message( task, System.currentMilliSec() );
Message prev = tail.prev;
prev.next = message;
message.prev = prev;
message.next = tail;
tail.prev = message;
}
public void postDelay(Runnable task, long milliSec){
//若是是延迟消息,生成的Message的执行时间是当前时间+延迟的秒数。
Message message = new Message( task, System.currentMilliSec()+milliSec);
//这里使用一个while循环去找第一个执行时间在新建立的Message以前的Message,新建立的Message就要插在它后面。
Message target = tail;
while(target.execTime>= message.execTime){
target = target.prev;
}
Message next = target.next;
message.prev = target;
target.next = message;
message.next = next;
next.prev = message;
}
}
复制代码
上述代码有几个比较关键的点。
- 消息采用链表的方式存储,为的是方便插入新的消息,每次插入尾部的时间复杂度为O(1),插入中间的复杂度为O(n),你们能够想一想若是换成数组会是什么复杂度。
- 代码中能够用两个Dummy node做为头和尾,这样咱们每次插入新消息的时候不须要检查空指针, 若是头为空,咱们插入Message还须要作
if(head == null){ head = message } else if( tail == null ){head.next = message; tail = message}
这样的检查。 3.每次发送延迟消息的时候,遍历循环找到第一个时间比当前要插入的消息的时间小。如下面这个图为例子。
当前插入Message时间为3的时候,它须要插入在1和5中间,那么1节点就是咱们上面代码循环中的最后的Target了。
这样,咱们就完成了一个延迟消息的轮子了!哈哈,调用代码很是简单。
MessageQueue queue = new MessageQueue();
//开启queue的while循环
queue.run();
queue.post( new Runnable(....) )
//三秒以后执行
queue.postDelay( new Runnable(...) , 3*1000 )
复制代码
你们可能以为post,和postDelay看起来很是眼熟,没错,这个就是安卓里面Handler的经典方法
在安卓系统中的源代码里面,postDelay就是运用上述的原理,只不过安卓系统对回收Message还有额外的处理。可是对于延迟消息的发送,安卓的Handler就是对其对应的Looper里面的消息链表进行处理,比较执行时间从而实现延迟消息发送的。
最后你们再思考一下,像上述代码的例子里面,延迟三秒,是否是精确的作到了在当前时间的三秒后运行
答案固然是NO!
在这个设计下,咱们只能保证:
假如消息A延迟的秒数为X,当前时间为Y,系统能保证A不会在X+Y以前执行。 这样其实很好理解,由于若是使用队列来执行代码的话,你永远不知道你前面那个Message的执行时间是多少,假如前面的Message执行时间异常的长。。。。那么轮到当前Message执行的时候,确定会比它本身的execTime偏后。可是这是可接受的。
若是咱们须要严格让每一个Message按照设计的时间执行,那就须要Alarm,相似闹钟的设计了。你们有兴趣能够想一想看怎么用最基本的数据结构实现。
说到线程池,我一直有不少疑惑,网上不少文章都会以线程池最全解析,或者史上最详细Java线程池原理诸如此类的Title为标题,但却主要以怎么操做Java线程池的API为内容。
在我看来这类文章都是耍流氓,对于一个合格的Java开发来讲,若是连API都不会查,那干脆别干了,还须要你专门写一篇文章来介绍API怎么用嘛。。。。。我也一直在问我本身,为啥你们都对源代码没有兴趣。。。。
这个章节我就会用简单版本的代码把线程池的实现给展现一下。
其实线程池的实现很简单,就是使用一个队列若干Thread就好了。
public class ThreadPool{
//用一个Set或者其余数据结构把建立的线程保存起来,为的是方便之后获取线程的handle,作其余操做。
Set<WorkerThread> set = null;
private Queue<Runnable> queue;
//初始化线程池,建立内部类WorkerThread而且启动它
public ThreadPool(int size){
for( int i = 0 ;i < size ;i++ ){
WorkerThread t = new WorkerThread();
t.start();
set.add( t );
}
queue = new LinkedList<Runnable>();
}
//submit一个runnable进线程池
public void submit(Runnable runnable){
synchronized (queue){
queue.add(runnable);
}
}
//WorkerThread用一个死循环不停的去向Runnable队列拿Runnable执行。
public class WorkerThread extends Thread{
@Override
public void run() {
super.run();
while(true){
synchronized (queue){
Runnable current = queue.poll();
current.run();
}
}
}
}
}
复制代码
这样,一个简单版本的线程池就完成了。。。。使用一组Thread,不停的向Runnable队列去拿Runnable执行就行了。。。看起来彻底没有技术含量。可是这倒是Java的线程池的基本原理。你们抽空能够去看看源码。还有不少细节我都没有写出来,好比说怎么shutdown线程池,或者线程池内部的WorkerThread怎么处理异常。怎么设置最大线程数量等等。
注意点很少,就是要使用synchronized对并发部分的代码作好同步就能够了。
调用代码简单
ThreadPool pool = new ThreadPool(5);
pool.submit(new Runnable(...))
复制代码
华丽丽的分割线
这一期的分享结束啦,其实上面三个例子都是大部分安卓开发者会接触到的,若是稍微有点兴趣和耐心就能够明白其原理,都是用最简单的数据结构加最“幼稚”的设计完成的。
最后我还想说,但愿每一个安卓开发者都能有一颗疑问的心, 好比线程池,基于Java的Thread这个类,怎么去完成一个线程池的实现,若是每次在使用这些API以后都能问问本身,为何,保持一颗愿意提问的心,这些都能学会。愿你们都能有且保持这种热忱。
我也须要时刻提醒本身,不管能不能去硅谷都好,都要一直有这种热情,一刻也不能懈怠。若是个人热情由于不能去硅谷而破灭,那个人坚持也太脆弱了。