xUtils3 源码解析 -- 网络模块

看 xUtils3 的源码时间其实挺长时间了,但是这个过程穿插在项目开发中,所以很多时候都是看明白了或者梳理通了流程后,进入一段时间的项目开发再来看很多东西都模糊了,可能是自己没有看透的原因吧。其实自己想考虑过要不要放弃在看 xUtils3 的源码,毕竟现在比较流行、性能更优的网络框架那么多,自己在平时的项目中也很少用到,但是想到也看了挺长时间,再者很多框架只是设计思想不同,但是在最终的功能实现上代码和思想也如出一辙,我还是觉得会有所收获的。于是就坚定信念搞一波,接着走起~~~

  • 本文主要内容包括:
    1. 以请求 String 数据为栗子,梳理xUtils3 网络请求流程
    2. 梳理网络请求的 数据缓存过程

1. 发起 HTTP 请求

在这里我们为了研究 缓存,回调的接口为 CacheCallback,返回值类型为 String
使用CacheCallback, xUtils将为该请求缓存数据.

a. 建立 HTTP请求

Callback.Cancelable cancelable2
    = x.http().get(params, new Callback.CacheCallback<String>() {

      @Override
      public boolean onCache(String result) {
          // 得到缓存数据, 缓存过期后不会进入这个方法.
          // 如果服务端没有返回过期时间, 参考params.setCacheMaxAge(maxAge)方法.
          //
          // * 客户端会根据服务端返回的 header 中 max-age 或 expires 来确定本地缓存是否给 onCache 方法.
          //   如果服务端没有返回 max-age 或 expires, 那么缓存将一直保存, 除非这里自己定义了返回false的
          //   逻辑, 那么xUtils将请求新数据, 来覆盖它.
          //
          // * 如果信任该缓存返回 true, 将不再请求网络;
          //   返回 false 继续请求网络, 但会在请求头中加上ETag, Last-Modified等信息,
          //   如果服务端返回304, 则表示数据没有更新, 不继续加载数据.
          //
          this.result = result;
          return false; // true: 信任缓存数据, 不在发起网络请求; false不信任缓存数据.
      }

      @Override
      public void onSuccess(String result) {
          // 注意: 如果服务返回304或 onCache 选择了信任缓存, 这里将不会被调用,
          // 但是 onFinished 总会被调用.
          this.result = result;
      }

      @Override
      public void onError(Throwable ex, boolean isOnCallback) {
          ......
      }

      @Override
      public void onCancelled(CancelledException cex) {
      }

      @Override
      public void onFinished() {
          ......
      }
  });

b. HTTP 请求流走向

我们看一下最终执行该代码的类

x#Ext$http
 public static HttpManager http() {
        if (Ext.httpManager == null) {
            HttpManagerImpl.registerInstance();
        }
        return Ext.httpManager;
   }
 x#Ext$setHttpManager
 public static void setHttpManager(HttpManager httpManager) {
         Ext.httpManager = httpManager;
    }

 HttpManagerImpl$registerInstance
 public static void registerInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new HttpManagerImpl();
                }
            }
        }
        x.Ext.setHttpManager(instance);
    }

从上面的代码我们可以看到 x.http() 实际返回的实例为 HttpManagerImpl,我们去看一些 HttpManagerImpl#get() 方法的具体实现。

public <T> Callback.Cancelable get(RequestParams entity, Callback.CommonCallback<T> callback) {
        return request(HttpMethod.GET, entity, callback);
    }

 public <T> Callback.Cancelable request(HttpMethod method, RequestParams entity, Callback.CommonCallback<T> callback) {
        entity.setMethod(method);
        Callback.Cancelable cancelable = null;
        if (callback instanceof Callback.Cancelable) {
            cancelable = (Callback.Cancelable) callback;
        }
        //将 请求方法、参数 、回调 封装成 http网络任务(HttpTask)
        HttpTask<T> task = new HttpTask<T>(entity, cancelable, callback);
        return x.task().start(task);//通过异步任务管理类-TaskController,来进行管理网络请求任务,执行网络请求
    }

很明显的,我们可以看到最终来到request(...)方法,并开启网络请求任务

2. 网络请求任务管理

首先跟进 x.task()代码我们看到的是:

 public static TaskController task() {
        //返回 异步任务的管理类 TaskController 的 实现类
        return Ext.taskController;
    }

 // 在初始化 xutils 时会调用该方法,TaskControllerImpl 类得到了初始化
 public static void init(Application app) {
            TaskControllerImpl.registerInstance();
            if (Ext.app == null) {
                Ext.app = app;
            }
        }

 TaskControllerImpl$registerInstance
 public static void registerInstance() {
        if (instance == null) {
            synchronized (TaskController.class) {
                if (instance == null) {
                    instance = new TaskControllerImpl();
                }
            }
        }
        x.Ext.setTaskController(instance);
    }

  public static void setTaskController(TaskController taskController) {//传入 taskcontroller 的实现类 TaskControllerImpl
             if (Ext.taskController == null) {
                 Ext.taskController = taskController;
             }
         }

从代码中我们可以看到,x.task() 返回的实例对象是 TaskControllerImpl。那么接下来思路就比较清晰了,其实 x.task().start(...)实际执行的代码为TaskControllerImpl.start(...),其代码如下:

@Override
    public <T> AbsTask<T> start(AbsTask<T> task) {//此处的task为 HttpTask
        TaskProxy<T> proxy = null;
        if (task instanceof TaskProxy) {
            proxy = (TaskProxy<T>) task;
        } else {
            proxy = new TaskProxy<T>(task);
        }
        try {
            proxy.doBackground();
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
        return proxy;
    }

3、任务代理

从上面的代码我们可以看到 TaskProxy其实是 HttpTask的静态代理类,至于代理是什么,可以 自己去百度。
下面我们看一下在代理类中我们都做了什么工作。

class TaskProxy<ResultType> extends AbsTask<ResultType> {

    /*package*/ static final InternalHandler sHandler = new InternalHandler();
    /*package*/ static final PriorityExecutor sDefaultExecutor = new PriorityExecutor(true);

    private final AbsTask<ResultType> task;//ci
    private final Executor executor;
    private volatile boolean callOnCanceled = false;
    private volatile boolean callOnFinished = false;

    /*package*/ TaskProxy(AbsTask<ResultType> task) {
        super(task);
        this.task = task;
        this.task.setTaskProxy(this);
        this.setTaskProxy(null);
        /**
         * 获得在 {@link xutils3.http.HttpTask}中实例化线程池
         */
        Executor taskExecutor = task.getExecutor();
        if (taskExecutor == null) {
            taskExecutor = sDefaultExecutor;
        }
        this.executor = taskExecutor;
    }

    /**
     * 根据网络请求的结果进行回调 onSuccess onCancle onError onFinish
     *
     * @return
     * @throws Throwable
     */

    @Override
    protected final ResultType doBackground() throws Throwable {
        this.onWaiting();
        PriorityRunnable runnable = new PriorityRunnable(
                task.getPriority(),
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // 等待过程中取消
                            if (callOnCanceled || TaskProxy.this.isCancelled()) {
                                throw new Callback.CancelledException("");
                            }

                            // start running
                            TaskProxy.this.onStarted();

                            if (TaskProxy.this.isCancelled()) { // 开始时取消
                                throw new Callback.CancelledException("");
                            }

                            // 执行task, 得到结果.
                            task.setResult(task.doBackground());
                            TaskProxy.this.setResult(task.getResult());

                            // 未在doBackground过程中取消成功
                            if (TaskProxy.this.isCancelled()) {
                                throw new Callback.CancelledException("");
                            }

                            /**
                             * 在此处执行的 网络请求时 失败、成功、取消 的接口回调
                             */
                            // 执行成功
                            TaskProxy.this.onSuccess(task.getResult());
                        } catch (Callback.CancelledException cex) {
                            TaskProxy.this.onCancelled(cex);
                        } catch (Throwable ex) {
                            TaskProxy.this.onError(ex, false);
                        } finally {
                            TaskProxy.this.onFinished();
                        }
                    }
                });
        this.executor.execute(runnable);//线程池执行线程操作
        return null;
    }
    ....
}

从以上代码我们可以看到两个比较关键的点:

  1. task.setResult(task.doBackground())
  2. PriorityRunnable runnable = new PriorityRunnable(...) //带有优先级的 Runable
  3. this.executor.execute(runnable);//线程池执行线程操作

不难看出,作为一个代理类,它并没有真正的去进行根本的操作,最终 result 的返回值还是由 HttpTask$doBackground()得到,因为网络操作、数据缓存等操作为耗时操作,所以要在线程内做这些操作。在 xutils 中,通过自定义实现具有优先级的线程池来实现。

4.线程池(FIFO -- first in first out)

获取线程池,因为我们的 Callback 类型为 CacheCallback,所以使用的为 CACHE_EXECUTOR 缓存线程池。

public class HttpTask<ResultType> extends AbsTask<ResultType> implements ProgressHandler {
    ....
    private static final PriorityExecutor HTTP_EXECUTOR = new PriorityExecutor(5, true);
        private static final PriorityExecutor CACHE_EXECUTOR = new PriorityExecutor(5, true);
        ....

 public HttpTask(RequestParams params, Callback.Cancelable cancelHandler,
                    Callback.CommonCallback<ResultType> callback) {
        super(cancelHandler);

        ....
        if (params.getExecutor() != null) {
                    this.executor = params.getExecutor();
                } else {
                    if (cacheCallback != null) {
                        this.executor = CACHE_EXECUTOR;
                    } else {
                        this.executor = HTTP_EXECUTOR;
                    }
                }
}

我们看一下 支持优先级的线程池管理类的实现

public class PriorityExecutor implements Executor {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAXIMUM_POOL_SIZE = 256;
    private static final int KEEP_ALIVE = 1;
    private static final AtomicLong SEQ_SEED = new AtomicLong(0);

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, "xTID#" + mCount.getAndIncrement());
        }
    };

    private static final Comparator<Runnable> FIFO_CMP = new Comparator<Runnable>() {
        @Override
        public int compare(Runnable lhs, Runnable rhs) {
            if (lhs instanceof PriorityRunnable && rhs instanceof PriorityRunnable) {
                PriorityRunnable lpr = ((PriorityRunnable) lhs);
                PriorityRunnable rpr = ((PriorityRunnable) rhs);
                int result = lpr.priority.ordinal() - rpr.priority.ordinal();
                return result == 0 ? (int) (lpr.SEQ - rpr.SEQ) : result;
            } else {
                return 0;
            }
        }
    };

    private static final Comparator<Runnable> FILO_CMP = new Comparator<Runnable>() {
        @Override
        public int compare(Runnable lhs, Runnable rhs) {
            if (lhs instanceof PriorityRunnable && rhs instanceof PriorityRunnable) {
                PriorityRunnable lpr = ((PriorityRunnable) lhs);
                PriorityRunnable rpr = ((PriorityRunnable) rhs);
                int result = lpr.priority.ordinal() - rpr.priority.ordinal();
                return result == 0 ? (int) (rpr.SEQ - lpr.SEQ) : result;
            } else {
                return 0;
            }
        }
    };

    private final ThreadPoolExecutor mThreadPoolExecutor;

    /**
     * 默认工作线程数5
     *
     * @param fifo 优先级相同时, 等待队列的是否优先执行先加入的任务.
     */
    public PriorityExecutor(boolean fifo) {
        this(CORE_POOL_SIZE, fifo);
    }

    /**
     * @param poolSize 工作线程数
     * @param fifo     优先级相同时, 等待队列的是否优先执行先加入的任务.
     */
    public PriorityExecutor(int poolSize, boolean fifo) {
        /**
         * 线程池
         * 阻塞队列 FIFO (first in first out)
         */
        BlockingQueue<Runnable> mPoolWorkQueue =
                new PriorityBlockingQueue<Runnable>(MAXIMUM_POOL_SIZE, fifo ? FIFO_CMP : FILO_CMP);
        mThreadPoolExecutor = new ThreadPoolExecutor(
                poolSize,
                MAXIMUM_POOL_SIZE,
                KEEP_ALIVE,
                TimeUnit.SECONDS,
                mPoolWorkQueue,
                sThreadFactory);
    }

    ....
    ....

    @Override
        public void execute(Runnable runnable) {
            if (runnable instanceof PriorityRunnable) {
                ((PriorityRunnable) runnable).SEQ = SEQ_SEED.getAndIncrement();
            }
            mThreadPoolExecutor.execute(runnable);
        }
}

5、真正的发起网络请求

在代理任务类中的代码 task.setResult(task.doBackground());,下面我们看一下真正执行 task.doBackground()的操作。

public class HttpTask<ResultType> extends AbsTask<ResultType> implements ProgressHandler {
protected ResultType doBackground() throws Throwable {
        // 初始化请求参数
        ResultType result = null;
        resolveLoadType();
        request = createNewRequest();
        checkDownloadTask();//检查现在是否有下载任务
        // retry 初始化
        boolean retry = true;
        int retryCount = 0;
        Throwable exception = null;
        //设置重试次数
        HttpRetryHandler retryHandler = this.params.getHttpRetryHandler();
        if (retryHandler == null) {
            retryHandler = new HttpRetryHandler();
        }
        retryHandler.setMaxRetryCount(this.params.getMaxRetryCount());

        if (this.isCancelled()) {
            throw new Callback.CancelledException("cancelled before request");
        }

        // 检查缓存 允许缓存
        Object cacheResult = null;
        //如果允许缓存,
        if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {
            // 尝试从缓存获取结果, 并为请求头加入缓存控制参数.
            try {
                clearRawResult();
                LogUtil.d("load cache: " + this.request.getRequestUri());
                /**
                 * {@link xutils3.http.request.HttpRequest#loadResultFromCache()}
                 */
                rawResult = this.request.loadResultFromCache();//从缓存中获取数据
            } catch (Throwable ex) {
                LogUtil.w("load disk cache error", ex);
            }

            if (this.isCancelled()) {
                clearRawResult();
                throw new Callback.CancelledException("cancelled before request");
            }

            if (rawResult != null) {
                if (prepareCallback != null) {
                    try {
                        cacheResult = prepareCallback.prepare(rawResult);
                    } catch (Throwable ex) {
                        cacheResult = null;
                        LogUtil.w("prepare disk cache error", ex);
                    } finally {
                        clearRawResult();
                    }
                } else {
                    cacheResult = rawResult;
                }

                if (cacheResult != null) {
                    /**
                     * 同步等待是否信任缓存
                     *最终来到这个位置 {@link HttpTask#onUpdate(int, Object...)}
                     */
                    this.update(FLAG_CACHE, cacheResult);
                    synchronized (cacheLock) {
                        while (trustCache == null) {
                            try {
                                cacheLock.wait();
                            } catch (InterruptedException iex) {
                                throw new Callback.CancelledException("cancelled before request");
                            } catch (Throwable ignored) {
                            }
                        }
                    }

                    // 处理完成,如果信任该缓存,则返回 null,只是不会继续执行下面的代码(网络请求)
                    if (trustCache) {
                        return null;
                    }
                }
            }
        }

        if (trustCache == null) {
            trustCache = false;
        }

        if (cacheResult == null) {
            this.request.clearCacheHeader();
        }

        // 判断请求的缓存策略
        if (callback instanceof Callback.ProxyCacheCallback) {
            if (((Callback.ProxyCacheCallback) callback).onlyCache()) {
                return null;
            }
        }

        // 发起请求
        retry = true;
        while (retry) {
            retry = false;

            try {
                if (this.isCancelled()) {
                    throw new Callback.CancelledException("cancelled before request");
                }

                // 由loader发起请求, 拿到结果.
                this.request.close(); // retry 前关闭上次请求

                try {
                    clearRawResult();
                    // 开始请求工作
                    LogUtil.d("load: " + this.request.getRequestUri());
                    requestWorker = new RequestWorker();
                    requestWorker.request();
                    if (requestWorker.ex != null) {
                        throw requestWorker.ex;
                    }
                    LogUtil.e("rawData111: " + (ResultType) requestWorker.result);
                    rawResult = requestWorker.result;//由 RequestWorker 获得的真正的数据
                } catch (Throwable ex) {
                    clearRawResult();
                    if (this.isCancelled()) {
                        throw new Callback.CancelledException("cancelled during request");
                    } else {
                        throw ex;
                    }
                }

                if (prepareCallback != null) {

                    if (this.isCancelled()) {
                        throw new Callback.CancelledException("cancelled before request");
                    }

                    try {
                        result = (ResultType) prepareCallback.prepare(rawResult);
                    } finally {
                        clearRawResult();
                    }
                } else {
                    result = (ResultType) rawResult;
                }

                // 保存缓存
                if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {
                    this.request.save2Cache();
                }

            } catch (HttpRedirectException redirectEx) {
                retry = true;
                LogUtil.w("Http Redirect:" + params.getUri());
            } catch (Throwable ex) {
               switch (this.request.getResponseCode()) {
                      ....
                      default: {
                          exception = ex;
                          if (this.isCancelled() && !(exception instanceof Callback.CancelledException)) {
                              exception = new Callback.CancelledException("canceled by user");
                          }
                          retry = retryHandler.canRetry(this.request, exception, ++retryCount);
                      }
                  }
            }

        }

         return result;
    }

}

a、初始化请求参数

    private UriRequest createNewRequest() throws Throwable {
        params.init();
        UriRequest result = UriRequestFactory.getUriRequest(params, loadType);
        result.setCallingClassLoader(callback.getClass().getClassLoader());
        result.setProgressHandler(this);
        this.loadingUpdateMaxTimeSpan = params.getLoadingUpdateMaxTimeSpan();
        this.update(FLAG_REQUEST_CREATED, result);
        return result;
    }

b、resolveLoadType()

个人觉得是获取 具体是哪一个 Loader ,StringLoader 、 FileLoader还是其他类型的Loader,但是这个过程的实现,需要进一步的探究。

c、开始请求工作并获取请求数据

requestWorker = new RequestWorker();
requestWorker.request();
if (requestWorker.ex != null) {
    throw requestWorker.ex;
}
LogUtil.e("rawData111: " + (ResultType) requestWorker.result);
rawResult = requestWorker.result;//由 RequestWorker 获得的真正的数据

跟踪 requestWorker.request() 方法,我们来到 HttpTask 的内部类 RequestWorkerrequest() 方法,继续跟进代码我们来到了
UriRequest$loadResult():

public Object loadResult() throws Throwable {
        return this.loader.load(this);
    }

由上面的代码我们可以看到的是最终我们会根据请求的数据类型去不同的 Loader中去执行不同的操作,在这里我们以 String 为例,那么我们的Loader的具体类为 StringLoader,并且在该类中我们做的操作为:

@Override
    public String load(final UriRequest request) throws Throwable {
        /**
         * 执行 {@link HttpRequest#sendRequest() 去发起请求}
         */
        request.sendRequest();
        /**
         * HttpURLConneciton获取响应
         * 调用HttpURLConnection连接对象的getInputStream()函数,
         * 在调用 getInputStream 时候发送 http 请求,同时获取响应
         */
        return this.load(request.getInputStream());
 }

可以看到我们最终发起了网络请求,并且返回了我们需要的数据。现在我们把网络请求的具体步骤一一理顺了,
但是不要忘记,还有一个很重要的点:缓存。下面我们具体看一下缓存的实现。

6、缓存的实现

在发起网络请求时,当你具体的Callback 为CachCallBack 时,那么xutils3 就认为你的这次网络请求是需要进行相关缓存机制的处理的。

a、是否信任缓存

在 Callback.CacheCallback 的具体实现的方法中,我们具体看一下onCache()方法:

@Override
      public boolean onCache(String result) {
          // 得到缓存数据, 缓存过期后不会进入这个方法.
          // 如果服务端没有返回过期时间, 参考params.setCacheMaxAge(maxAge)方法.
          //
          // * 客户端会根据服务端返回的 header 中 max-age 或 expires 来确定本地缓存是否给 onCache 方法.
          //   如果服务端没有返回 max-age 或 expires, 那么缓存将一直保存, 除非这里自己定义了返回false的
          //   逻辑, 那么xUtils将请求新数据, 来覆盖它.
          //
          // * 如果信任该缓存返回 true, 将不再请求网络;
          //   返回 false 继续请求网络, 但会在请求头中加上ETag, Last-Modified等信息,
          //   如果服务端返回304, 则表示数据没有更新, 不继续加载数据.
          //
          this.result = result;
          return false; // true: 信任缓存数据, 不在发起网络请求; false不信任缓存数据.
      }

具体的操作注释已经很清楚了,就不啰嗦了。

b、网络请求过程中的缓存处理

在发起网络请求的过程中 --HttpTask$doBackgroud()中的以下代码:

 if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {
 ....
 rawResult = this.request.loadResultFromCache();//从缓存中获取数据
 ....
 }

即在允许缓存的情况下,获取缓存数据,注意现在只是获取了缓存数据,但是该缓存数据是否有效见 是否信任缓存解释。
但是我们还是来看一下具体的判断逻辑

if (cacheResult != null) {
    //
    /**
     *  1、首先触发update()方法 同步等待是否信任缓存
     *最终来到这个位置 {@link HttpTask#onUpdate(int, Object...)}
     */
    this.update(FLAG_CACHE, cacheResult);
    synchronized (cacheLock) {
        while (trustCache == null) {
            try {
                cacheLock.wait();
            } catch (InterruptedException iex) {
                throw new Callback.CancelledException("cancelled before request");
            } catch (Throwable ignored) {
            }
        }
    }

    // 处理完成,如果信任该缓存,则返回 null,只是不会继续执行下面的代码(网络请求)
    if (trustCache) {
        return null;
    }
}
/**
 *
 *2. 由 1 触发,最后兜兜转转来到了本类的 onUpdate 方法
 */
   protected void onUpdate(int flag, Object... args) {
          switch (flag) {
          ....
              case FLAG_CACHE: {
                  synchronized (cacheLock) {
                      try {
                          ResultType result = (ResultType) args[0];
                          if (tracker != null) {
                              tracker.onCache(request, result);
                          }
                          /**
                           * {@link HttpFragment#onTest1Click(View)#cancelable2#onCache}
                           */
                           //检验是否信任缓存数据,通过回调将数据展示出来
                          trustCache = this.cacheCallback.onCache(result);
                      } catch (Throwable ex) {
                          trustCache = false;
                          callback.onError(ex, true);
                      } finally {
                          cacheLock.notifyAll();
                      }
                  }
                  break;
              }
          ....
          }
      }

因为该过程是发生在线程中的,存在线程安全问题,所以在这里进行了 同步处理(synchronized (cacheLock))、线程等待/唤醒(cacheLock.wait()cacheLock.notifyAll())。通过trustCache = this.cacheCallback.onCache(result);if (trustCache) { return null;}我们可知,当信任缓存,则此次网络请求结束,并将获取的缓存结果通过回调返回调用出。

c、缓存网络请求数据

其实这个具体的实现逻辑就相对简单了,

 if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {
    this.request.save2Cache();
 }

来到 StringLoader 来执行相关操作

@Override
public void save2Cache(UriRequest request) {
    saveStringCache(request, resultStr);
}
//设置 缓存的缓存时间等信息
protected void saveStringCache(UriRequest request, String resultStr) {
    if (!TextUtils.isEmpty(resultStr)) {
        DiskCacheEntity entity = new DiskCacheEntity();
        /**
         * {@link HttpRequest#getCacheKey()}
         */
        entity.setKey(request.getCacheKey());//设置 缓存对象对应的key值
        entity.setLastAccess(System.currentTimeMillis());
        entity.setEtag(request.getETag());
        entity.setExpires(request.getExpiration());//设置了过期时间
        entity.setLastModify(new Date(request.getLastModified()));
        entity.setTextContent(resultStr);
        LruDiskCache.getDiskCache(request.getParams().getCacheDirName()).put(entity);
    }
}

7、总结

至此,在xUtils3的一次支持缓存的网络请求到此执行完毕。再回顾一下本篇所关注的几个方面:

  1. 具体的请求的代码执行步骤
  2. 缓存的具体原理和实现
  3. 优先级线程池的具体设计思想

当然,xUtils毕竟是一个功能强大的第三方框架,其设计思想和功能实现还是还值得我们借鉴的,继续探究,继续学习。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • 三年的光景,还记得我们经历了什么? 2015年,勒布朗荣归故里,他将自己的天赋以及不甘带回克利夫兰这座曾带给他无数...
    篮球行为大赏阅读 209评论 0 4
  • 主题曲很燃,渚薰很帅气,剧情很棒,薰嗣大法好。 没了【。 具体感想有时间再写。
    心怀感恩与荣耀阅读 127评论 0 1
  • 在日常工作中大部分的应用是基于关系型数据库做的构建。虽然近些年nosql发展很迅猛。但是并不能动摇关系型数据库在企...
    一家之主小书童阅读 309评论 2 0
  • 这个冬天,我的母亲病了。她患了膀胱癌。“癌症”在医学技术日益昌明的今日,还是蛮吓人的。 人到中...
    lisa_ly阅读 429评论 1 2