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网络任务(HttpTaskHttpTask<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毕竟是一个功能强大的第三方框架,其设计思想和功能实现还是还值得我们借鉴的,继续探究,继续学习。

猜你喜欢

转载自blog.csdn.net/strange_monkey/article/details/80529068