Andriod 網(wǎng)絡框架 OkHttp 源碼解析

1巍佑、OkHttp 的基本使用

OkHttp 是 Square 的一款應用于 Android 和 Java 的 Http 和 Http/2 客戶端寇窑。使用的時候只需要在 Gradle 里面加入下面一行依賴即可引入:

implementation 'com.squareup.okhttp3:okhttp:3.11.0'

我們知道碱呼,Http 請求有多種類型占遥,常用的分為 Get 和 Post阶女,而 POST 又分為 Form 和 Multiple 等喊儡。下面我們以 Form 類型的請求為例來看下 OkHttp 的 API 設計邏輯:

OkHttpClient internalHttpClient = new OkHttpClient();
FormBody.Builder formBodyBuilder = new FormBody.Builder();
RequestBody body = formBodyBuilder.build();
Request.Builder builder = new Request.Builder().url("host:port/url").post(body);
Request request = builder.build();
Response response = internalHttpClient.newCall(request).execute();
String retJson = response.body().string();

這里我們先用了 FormBody 的構(gòu)建者模式創(chuàng)建 Form 類型請求的請求體拨与,然后使用 Request 的構(gòu)建者創(chuàng)建完整的 Form 請求。之后艾猜,我們用創(chuàng)建好的 OkHttp 客戶端 internalHttpClient 來獲取一個請求买喧,并從請求的請求體中獲取 Json 數(shù)據(jù)。

根據(jù) OkHttp 的 API匆赃,如果我們希望發(fā)送一個 Multipart 類型的請求的時候就需要使用 MultipartBody 的構(gòu)建者創(chuàng)建 Multipart 請求的請求體淤毛。然后同樣使用 Request 的構(gòu)建者創(chuàng)建完整的 Multipart 請求,剩下的邏輯相同算柳。

除了使用上面的直接實例化一個 OkHttp 客戶端的方式低淡,我們也可以使用 OkHttpClient 的構(gòu)建者 OkHttpClient.Builder 來創(chuàng)建 OkHttp 客戶端。

所以埠居,我們可以總結(jié):

  1. OkHttp 為不同的請求類型都提供了一個構(gòu)建者方法用來創(chuàng)建請求體 RequestBody查牌;
  2. 因為請求體只是整個請求的一部分,所以滥壕,又要用 Request.Builder 構(gòu)建一個請求對象 Request纸颜;
  3. 這樣我們得到了一個完整的 Http 請求,然后使用 OkHttpClient 對象進行網(wǎng)絡訪問得到響應對象 Response绎橘。

OkHttp 本身的設計比較友好胁孙,思路非常清晰,按照上面的思路搞懂了人家的 API 設計邏輯称鳞,自己再基于 OkHttp 封裝一個庫自然問題不大涮较。

2、OkHttp 源碼分析

上面我們提到的一些是基礎(chǔ)的 API 類冈止,是提供給用戶使用的狂票。這些類的設計只是基于構(gòu)建者模式,非常容易理解熙暴。這里我們關(guān)注點也不在這些 API 類上面闺属,而是 OkHttp 內(nèi)部的請求執(zhí)行相關(guān)的類慌盯。下面我們就開始對 OkHttp 的請求過程進行源碼分析(源碼版本:3.10.0)。

2.1 一個請求的大致流程

參考之前的示例程序掂器,拋棄構(gòu)建請求的過程不講亚皂,單從請求的發(fā)送過程來看,我們的線索應該從 OkHttpClient.newCall(Request) 開始国瓮。下面是這個方法的定義灭必,它會創(chuàng)建一個 RealCall 對象,并把 OkHttpClient 對象和 Request 對象作為參數(shù)傳入進去:

@Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
}

然后乃摹,RealCall 調(diào)用內(nèi)部的靜態(tài)方法 newRealCall 在其中創(chuàng)建一個 RealCall 實例并將其返回:

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
}

然后禁漓,當返回了 RealCall 之后,我們又會調(diào)用它的 execute() 方法來獲取響應結(jié)果峡懈,下面是這個方法的定義:

    @Override public Response execute() throws IOException {
        synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        captureCallStackTrace();
        eventListener.callStart(this);
        try {
            // 加入到一個雙端隊列中
            client.dispatcher().executed(this);
            // 從這里拿的響應Response
            Response result = getResponseWithInterceptorChain();
            if (result == null) throw new IOException("Canceled");
            return result;
        } catch (IOException e) {
            eventListener.callFailed(this, e);
            throw e;
        } finally {
            client.dispatcher().finished(this);
        }
    }

這里我們會用 client 對象(實際也就是上面創(chuàng)建 RealCall 的時候傳入的 OkHttpClient)的 dispatcher() 方法來獲取一個 Dispatcher 對象璃饱,并調(diào)用它的 executed() 方法來將當前的 RealCall 加入到一個雙端隊列中与斤,下面是 executed(RealCall) 方法的定義肪康,這里的 runningSyncCalls 的類型是 Deque<RealCall>

    synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
    }

讓我們回到上面的 execute() 方法,在把 RealCall 加入到雙端隊列之后撩穿,我們又調(diào)用了 getResponseWithInterceptorChain() 方法磷支,下面就是該方法的定義。

    Response getResponseWithInterceptorChain() throws IOException {
        // 添加一系列攔截器食寡,注意添加的順序
        List<Interceptor> interceptors = new ArrayList<>();
        interceptors.addAll(client.interceptors());
        interceptors.add(retryAndFollowUpInterceptor);
        // 橋攔截器
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        // 緩存攔截器:從緩存中拿數(shù)據(jù)
        interceptors.add(new CacheInterceptor(client.internalCache()));
        // 網(wǎng)絡連接攔截器:建立網(wǎng)絡連接
        interceptors.add(new ConnectInterceptor(client));
        if (!forWebSocket) {
            interceptors.addAll(client.networkInterceptors());
        }
        // 服務器請求攔截器:向服務器發(fā)起請求獲取數(shù)據(jù)
        interceptors.add(new CallServerInterceptor(forWebSocket));
        // 構(gòu)建一條責任鏈
        Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
            originalRequest, this, eventListener, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());
        // 處理責任鏈
        return chain.proceed(originalRequest);
    }

這里雾狈,我們創(chuàng)建了一個列表對象之后把 client 中的攔截器、重連攔截器抵皱、橋攔截器善榛、緩存攔截器、網(wǎng)絡連接攔截器和服務器請求攔截器等依次加入到列表中呻畸。然后移盆,我們用這個列表創(chuàng)建了一個攔截器鏈。這里使用了責任鏈設計模式伤为,每當一個攔截器執(zhí)行完畢之后會調(diào)用下一個攔截器或者不調(diào)用并返回結(jié)果咒循。顯然,我們最終拿到的響應就是這個鏈條執(zhí)行之后返回的結(jié)果绞愚。當我們自定義一個攔截器的時候叙甸,也會被加入到這個攔截器鏈條里。

這里我們遇到了很多的新類位衩,比如 RealCall裆蒸、Dispatcher 以及責任鏈等。下文中糖驴,我們會對這些類之間的關(guān)系以及責任鏈中的環(huán)節(jié)做一個分析僚祷,而這里我們先對整個請求的流程做一個大致的梳理哪痰。下面是這個過程大致的時序圖:

OkHttp請求時序圖

2.2 分發(fā)器 Dispatcher

上面我們提到了 Dispatcher 這個類,它的作用是對請求進行分發(fā)久妆。以最開始的示例代碼為例晌杰,在使用 OkHttp 的時候,我們會創(chuàng)建一個 RealCall 并將其加入到雙端隊列中筷弦。但是請注意這里的雙端隊列的名稱是 runningSyncCalls肋演,也就是說這種請求是同步請求,會在當前的線程中立即被執(zhí)行烂琴。所以爹殊,下面的 getResponseWithInterceptorChain() 就是這個同步的執(zhí)行過程。而當我們執(zhí)行完畢的時候奸绷,又會調(diào)用 Dispatcherfinished(RealCall) 方法把該請求從隊列中移除梗夸。所以,這種同步的請求無法體現(xiàn)分發(fā)器的“分發(fā)”功能号醉。

除了同步的請求反症,還有異步類型的請求:當我們拿到了 RealCall 的時候,調(diào)用它的 enqueue(Callback responseCallback) 方法并設置一個回調(diào)即可畔派。該方法會執(zhí)行下面這行代碼:

client.dispatcher().enqueue(new AsyncCall(responseCallback));

即使用上面的回調(diào)創(chuàng)建一個 AsyncCall 并調(diào)用 enqueue(AsyncCall)铅碍。這里的 AsyncCall 間接繼承自 Runnable,是一個可執(zhí)行的對象线椰,并且會在 Runnablerun() 方法里面調(diào)用 AsyncCallexecute() 方法胞谈。AsyncCallexecute() 方法與 RealCallexecute() 方法類似,都使用責任鏈來完成一個網(wǎng)絡請求憨愉。只是后者可以放在一個異步的線程中進行執(zhí)行烦绳。

當我們調(diào)用了 Dispatcherenqueue(AsyncCall) 方法的時候也會將 AsyncCall 加入到一個隊列中,并會在請求執(zhí)行完畢的時候從該隊列中移除配紫,只是這里的隊列是 runningAsyncCalls 或者 readyAsyncCalls径密。它們都是一個雙端隊列,并用來存儲異步類型的請求笨蚁。它們的區(qū)別是睹晒,runningAsyncCalls 是正在執(zhí)行的隊列,當正在執(zhí)行的隊列達到了限制的時候括细,就會將其放置到就緒隊列 readyAsyncCalls 中:

    synchronized void enqueue(AsyncCall call) {
        if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
            runningAsyncCalls.add(call);
            executorService().execute(call);
        } else {
            readyAsyncCalls.add(call);
        }
    }

當把該請求加入到了正在執(zhí)行的隊列之后伪很,我們會立即使用一個線程池來執(zhí)行該 AsyncCall。這樣這個請求的責任鏈就會在一個線程池當中被異步地執(zhí)行了奋单。這里的線程池由 executorService() 方法返回:

    public synchronized ExecutorService executorService() {
        if (executorService == null) {
            executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
        }
        return executorService;
    }

顯然锉试,當線程池不存在的時候會去創(chuàng)建一個線程池。除了上面的這種方式览濒,我們還可以在構(gòu)建 OkHttpClient 的時候呆盖,自定義一個 Dispacher拖云,并在其構(gòu)造方法中為其指定一個線程池。下面我們類比 OkHttp 的同步請求繪制了一個異步請求的時序圖应又。你可以通過將兩個圖對比來了解兩種實現(xiàn)方式的不同:

OkHttp異步請求

以上就是分發(fā)器 Dispacher 的邏輯宙项,看上去并沒有那么復雜。并且從上面的分析中株扛,我們可以看出實際請求的執(zhí)行過程并不是在這里完成的尤筐,這里只能決定在哪個線程當中執(zhí)行請求并把請求用雙端隊列緩存下來,而實際的請求執(zhí)行過程是在責任鏈中完成的洞就。下面我們就來分析一下 OkHttp 里的責任鏈的執(zhí)行過程盆繁。

2.3 責任鏈的執(zhí)行過程

在典型的責任鏈設計模式里,很多對象由每一個對象對其下級的引用而連接起來形成一條鏈旬蟋。請求在這個鏈上傳遞油昂,直到鏈上的某一個對象決定處理此請求。發(fā)出這個請求的客戶端并不知道鏈上的哪一個對象最終處理這個請求倾贰,這使得系統(tǒng)可以在不影響客戶端的情況下動態(tài)地重新組織和分配責任冕碟。責任鏈在現(xiàn)實生活中的一種場景就是面試,當某輪面試官覺得你沒有資格進入下一輪的時候可以否定你躁染,不然會讓下一輪的面試官繼續(xù)面試鸣哀。

在 OkHttp 里面架忌,責任鏈的執(zhí)行模式與之稍有不同吞彤。這里我們主要來分析一下在 OkHttp 里面味赃,責任鏈是如何執(zhí)行的隔嫡,至于每個鏈條里面的具體邏輯,我們會在隨后一一說明墓陈。

回到 2.1 的代碼井仰,有兩個地方需要我們注意:

  1. 是當創(chuàng)建一個責任鏈 RealInterceptorChain 的時候埋嵌,我們傳入的第 5 個參數(shù)是 0。該參數(shù)名為 index俱恶,會被賦值給 RealInterceptorChain 實例內(nèi)部的同名全局變量雹嗦。
  2. 當啟用責任鏈的時候,會調(diào)用它的 proceed(Request) 方法合是。

下面是 proceed(Request) 方法的定義:

    @Override public Response proceed(Request request) throws IOException {
        return proceed(request, streamAllocation, httpCodec, connection);
    }

這里又調(diào)用了內(nèi)部的重載的 proceed() 方法了罪。下面我們對該方法進行了簡化:

    public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
        RealConnection connection) throws IOException {
        if (index >= interceptors.size()) throw new AssertionError();
        // ...
        // 調(diào)用責任鏈的下一個攔截器
        RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
            connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
            writeTimeout);
        Interceptor interceptor = interceptors.get(index);
        Response response = interceptor.intercept(next);
        // ...
        return response;
    }

注意到這里使用責任鏈進行處理的時候,會新建下一個責任鏈并把 index+1 作為下一個責任鏈的 index聪全。然后泊藕,我們使用 index 從攔截器列表中取出一個攔截器,調(diào)用它的 intercept() 方法难礼,并把下一個執(zhí)行鏈作為參數(shù)傳遞進去娃圆。

這樣玫锋,當下一個攔截器希望自己的下一級繼續(xù)處理這個請求的時候,可以調(diào)用傳入的責任鏈的 proceed() 方法讼呢;如果自己處理完畢之后撩鹿,下一級不需要繼續(xù)處理,那么就直接返回一個 Response 實例即可悦屏。因為三痰,每次都是在當前的 index 基礎(chǔ)上面加 1,所以能在調(diào)用 proceed() 的時候準確地從攔截器列表中取出下一個攔截器進行處理窜管。

我們還要注意的地方是之前提到過重試攔截器散劫,這種攔截器會在內(nèi)部啟動一個 while 循環(huán),并在循環(huán)體中調(diào)用執(zhí)行鏈的 proceed() 方法來實現(xiàn)請求的不斷重試幕帆。這是因為在它那里的攔截器鏈的 index 是固定的获搏,所以能夠每次調(diào)用 proceed() 的時候,都能夠從自己的下一級執(zhí)行一遍鏈條失乾。下面就是這個責任鏈的執(zhí)行過程:

責任鏈執(zhí)行過程

清楚了 OkHttp 的攔截器鏈的執(zhí)行過程之后常熙,我們來看一下各個攔截器做了什么邏輯。

2.3 重試和重定向:RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 主要用來當請求失敗的時候進行重試碱茁,以及在需要的情況下進行重定向裸卫。我們上面說,責任鏈會在進行處理的時候調(diào)用第一個攔截器的 intercept() 方法纽竣。如果我們在創(chuàng)建 OkHttp 客戶端的時候沒有加入自定義攔截器墓贿,那么
RetryAndFollowUpInterceptor 就是我們的責任鏈中最先被調(diào)用的攔截器。

    @Override public Response intercept(Chain chain) throws IOException {
        // ...
        // 注意這里我們初始化了一個 StreamAllocation 并賦值給全局變量蜓氨,它的作用我們后面會提到
        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
                createAddress(request.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
        // 用來記錄重定向的次數(shù)
        int followUpCount = 0;
        Response priorResponse = null;
        while (true) {
            if (canceled) {
                streamAllocation.release();
                throw new IOException("Canceled");
            }

            Response response;
            boolean releaseConnection = true;
            try {
                // 這里從當前的責任鏈開始執(zhí)行一遍責任鏈聋袋,是一種重試的邏輯
                response = realChain.proceed(request, streamAllocation, null, null);
                releaseConnection = false;
            } catch (RouteException e) {
                // 調(diào)用 recover 方法從失敗中進行恢復,如果可以恢復就返回true穴吹,否則返回false
                if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                    throw e.getLastConnectException();
                }
                releaseConnection = false;
                continue;
            } catch (IOException e) {
                // 重試與服務器進行連接
                boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
                if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
                releaseConnection = false;
                continue;
            } finally {
                // 如果 releaseConnection 為 true 則表明中間出現(xiàn)了異常幽勒,需要釋放資源
                if (releaseConnection) {
                    streamAllocation.streamFailed(null);
                    streamAllocation.release();
                }
            }

            // 使用之前的響應 priorResponse 構(gòu)建一個響應,這種響應的響應體 body 為空
            if (priorResponse != null) {
                response = response.newBuilder()
                        .priorResponse(priorResponse.newBuilder().body(null).build())
                        .build();
            }

            // 根據(jù)得到的響應進行處理港令,可能會增加一些認證信息啥容、重定向或者處理超時請求
            // 如果該請求無法繼續(xù)被處理或者出現(xiàn)的錯誤不需要繼續(xù)處理,將會返回 null
            Request followUp = followUpRequest(response, streamAllocation.route());

            // 無法重定向顷霹,直接返回之前的響應
            if (followUp == null) {
                if (!forWebSocket) {
                    streamAllocation.release();
                }
                return response;
            }

            // 關(guān)閉資源
            closeQuietly(response.body());

            // 達到了重定向的最大次數(shù)咪惠,就拋出一個異常
            if (++followUpCount > MAX_FOLLOW_UPS) {
                streamAllocation.release();
                throw new ProtocolException("Too many follow-up requests: " + followUpCount);
            }

            if (followUp.body() instanceof UnrepeatableRequestBody) {
                streamAllocation.release();
                throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
            }

            // 這里判斷新的請求是否能夠復用之前的連接,如果無法復用泼返,則創(chuàng)建一個新的連接
            if (!sameConnection(response, followUp.url())) {
                streamAllocation.release();
                streamAllocation = new StreamAllocation(client.connectionPool(),
                        createAddress(followUp.url()), call, eventListener, callStackTrace);
                this.streamAllocation = streamAllocation;
            } else if (streamAllocation.codec() != null) {
                throw new IllegalStateException("Closing the body of " + response
                        + " didn't close its backing stream. Bad interceptor?");
            }

            request = followUp;
            priorResponse = response;
        }
    }

以上的代碼主要用來根據(jù)錯誤的信息做一些處理硝逢,會根據(jù)服務器返回的信息判斷這個請求是否可以重定向,或者是否有必要進行重試。如果值得去重試就會新建或者復用之前的連接在下一次循環(huán)中進行請求重試渠鸽,否則就將得到的請求包裝之后返回給用戶叫乌。這里,我們提到了 StreamAllocation 對象徽缚,它相當于一個管理類憨奸,維護了服務器連接、并發(fā)流和請求之間的關(guān)系凿试,該類還會初始化一個 Socket 連接對象排宰,獲取輸入/輸出流對象。同時那婉,還要注意這里我們通過 client.connectionPool() 傳入了一個連接池對象 ConnectionPool板甘。這里我們只是初始化了這些類,但實際在當前的方法中并沒有真正用到這些類详炬,而是把它們傳遞到下面的攔截器里來從服務器中獲取請求的響應盐类。稍后,我們會說明這些類的用途呛谜,以及之間的關(guān)系在跳。

2.4 BridgeInterceptor

橋攔截器 BridgeInterceptor 用于從用戶的請求中構(gòu)建網(wǎng)絡請求,然后使用該請求訪問網(wǎng)絡隐岛,最后從網(wǎng)絡響應當中構(gòu)建用戶響應猫妙。相對來說這個攔截器的邏輯比較簡單,只是用來對請求進行包裝聚凹,并將服務器響應轉(zhuǎn)換成用戶友好的響應:

    public final class BridgeInterceptor implements Interceptor {
        @Override public Response intercept(Chain chain) throws IOException {
            Request userRequest = chain.request();
            // 從用戶請求中獲取網(wǎng)絡請求構(gòu)建者
            Request.Builder requestBuilder = userRequest.newBuilder();
            // ...
            // 執(zhí)行網(wǎng)絡請求
            Response networkResponse = chain.proceed(requestBuilder.build());
            // ...
            // 從網(wǎng)絡響應中獲取用戶響應構(gòu)建者
            Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);
            // ...
            // 返回用戶響應
            return responseBuilder.build();
        }
    }

2.5 使用緩存:CacheInterceptor

緩存攔截器會根據(jù)請求的信息和緩存的響應的信息來判斷是否存在緩存可用割坠,如果有可以使用的緩存,那么就返回該緩存該用戶元践,否則就繼續(xù)責任鏈來從服務器中獲取響應韭脊。當獲取到響應的時候,又會把響應緩存到磁盤上面单旁。以下是這部分的邏輯:

    public final class CacheInterceptor implements Interceptor {
        @Override public Response intercept(Chain chain) throws IOException {
            Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;
            long now = System.currentTimeMillis();
            // 根據(jù)請求和緩存的響應中的信息來判斷是否存在緩存可用
            CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
            Request networkRequest = strategy.networkRequest; // 如果該請求沒有使用網(wǎng)絡就為空
            Response cacheResponse = strategy.cacheResponse; // 如果該請求沒有使用緩存就為空
            if (cache != null) {
                cache.trackResponse(strategy);
            }
            if (cacheCandidate != null && cacheResponse == null) {
                closeQuietly(cacheCandidate.body());
            }
            // 請求不使用網(wǎng)絡并且不使用緩存,相當于在這里就攔截了饥伊,沒必要交給下一級(網(wǎng)絡請求攔截器)來執(zhí)行
            if (networkRequest == null && cacheResponse == null) {
                return new Response.Builder()
                        .request(chain.request())
                        .protocol(Protocol.HTTP_1_1)
                        .code(504)
                        .message("Unsatisfiable Request (only-if-cached)")
                        .body(Util.EMPTY_RESPONSE)
                        .sentRequestAtMillis(-1L)
                        .receivedResponseAtMillis(System.currentTimeMillis())
                        .build();
            }
            // 該請求使用緩存象浑,但是不使用網(wǎng)絡:從緩存中拿結(jié)果,沒必要交給下一級(網(wǎng)絡請求攔截器)執(zhí)行
            if (networkRequest == null) {
                return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
            }
            Response networkResponse = null;
            try {
                // 這里調(diào)用了執(zhí)行鏈的處理方法琅豆,實際就是交給自己的下一級來執(zhí)行了
                networkResponse = chain.proceed(networkRequest);
            } finally {
                if (networkResponse == null && cacheCandidate != null) {
                    closeQuietly(cacheCandidate.body());
                }
            }
            // 這里當拿到了網(wǎng)絡請求之后調(diào)用愉豺,下一級執(zhí)行完畢會交給它繼續(xù)執(zhí)行,如果使用了緩存就把請求結(jié)果更新到緩存里
            if (cacheResponse != null) {
                // 服務器返回的結(jié)果是304茫因,返回緩存中的結(jié)果
                if (networkResponse.code() == HTTP_NOT_MODIFIED) {
                    Response response = cacheResponse.newBuilder()
                            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                            .cacheResponse(stripBody(cacheResponse))
                            .networkResponse(stripBody(networkResponse))
                            .build();
                    networkResponse.body().close();
                    cache.trackConditionalCacheHit();
                    // 更新緩存
                    cache.update(cacheResponse, response);
                    return response;
                } else {
                    closeQuietly(cacheResponse.body());
                }
            }
            Response response = networkResponse.newBuilder()
                    .cacheResponse(stripBody(cacheResponse))
                    .networkResponse(stripBody(networkResponse))
                    .build();
            // 把請求的結(jié)果放進緩存里
            if (cache != null) {
                if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
                    CacheRequest cacheRequest = cache.put(response);
                    return cacheWritingResponse(cacheRequest, response);
                }
                if (HttpMethod.invalidatesCache(networkRequest.method())) {
                    try {
                        cache.remove(networkRequest);
                    } catch (IOException ignored) {
                        // The cache cannot be written.
                    }
                }
            }
            return response;
        }
    }

對緩存蚪拦,這里我們使用的是全局變量 cache,它是 InternalCache 類型的變量。InternalCache 是一個接口驰贷,在 OkHttp 中只有一個實現(xiàn)類 Cache盛嘿。在 Cache 內(nèi)部,使用了 DiskLruCache 來將緩存的數(shù)據(jù)存到磁盤上括袒。DiskLruCache 以及 LruCache 是 Android 上常用的兩種緩存策略次兆。前者是基于磁盤來進行緩存的,后者是基于內(nèi)存來進行緩存的锹锰,它們的核心思想都是 Least Recently Used芥炭,即最近最少使用算法。我們會在以后的文章中詳細介紹這兩種緩存框架恃慧,也請繼續(xù)關(guān)注我們的文章园蝠。

另外,上面我們根據(jù)請求和緩存的響應中的信息來判斷是否存在緩存可用的時候用到了 CacheStrategy 的兩個字段痢士,得到這兩個字段的時候使用了非常多的判斷砰琢,其中涉及 Http 緩存相關(guān)的知識,感興趣的話可以自己參考源代碼良瞧。

2.6 連接復用:ConnectInterceptor

連接攔截器 ConnectInterceptor 用來打開到指定服務器的網(wǎng)絡連接陪汽,并交給下一個攔截器處理。這里我們只打開了一個網(wǎng)絡連接褥蚯,但是并沒有發(fā)送請求到服務器挚冤。從服務器獲取數(shù)據(jù)的邏輯交給下一級的攔截器來執(zhí)行。雖然赞庶,這里并沒有真正地從網(wǎng)絡中獲取數(shù)據(jù)训挡,而僅僅是打開一個連接,但這里有不少的內(nèi)容值得我們?nèi)リP(guān)注歧强。因為在獲取連接對象的時候澜薄,使用了連接池 ConnectionPool 來復用連接。

    public final class ConnectInterceptor implements Interceptor {

        @Override public Response intercept(Chain chain) throws IOException {
            RealInterceptorChain realChain = (RealInterceptorChain) chain;
            Request request = realChain.request();
            StreamAllocation streamAllocation = realChain.streamAllocation();

            boolean doExtensiveHealthChecks = !request.method().equals("GET");
            HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
            RealConnection connection = streamAllocation.connection();

            return realChain.proceed(request, streamAllocation, httpCodec, connection);
        }
    }

這里的 HttpCodec 用來編碼請求并解碼響應摊册,RealConnection 用來向服務器發(fā)起連接肤京。它們會在下一個攔截器中被用來從服務器中獲取響應信息。下一個攔截器的邏輯并不復雜茅特,這里萬事具備之后忘分,只要它來從服務器中讀取數(shù)據(jù)即可“仔蓿可以說妒峦,OkHttp 中的核心部分大概就在這里,所以兵睛,我們就先好好分析一下肯骇,這里在創(chuàng)建連接的時候如何借助連接池來實現(xiàn)連接復用的窥浪。

根據(jù)上面的代碼,當我們調(diào)用 streamAllocationnewStream() 方法的時候笛丙,最終會經(jīng)過一系列的判斷到達 StreamAllocation 中的 findConnection() 方法漾脂。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        // ...
        synchronized (connectionPool) {
            // ...
            // 嘗試使用已分配的連接,已經(jīng)分配的連接可能已經(jīng)被限制創(chuàng)建新的流
            releasedConnection = this.connection;
            // 釋放當前連接的資源若债,如果該連接已經(jīng)被限制創(chuàng)建新的流符相,就返回一個Socket以關(guān)閉連接
            toClose = releaseIfNoNewStreams();
            if (this.connection != null) {
                // 已分配連接,并且該連接可用
                result = this.connection;
                releasedConnection = null;
            }
            if (!reportedAcquired) {
                // 如果該連接從未被標記為獲得蠢琳,不要標記為發(fā)布狀態(tài)啊终,reportedAcquired 通過 acquire() 方法修改
                releasedConnection = null;
            }

            if (result == null) {
                // 嘗試供連接池中獲取一個連接
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
        }
        // 關(guān)閉連接
        closeQuietly(toClose);

        if (releasedConnection != null) {
            eventListener.connectionReleased(call, releasedConnection);
        }
        if (foundPooledConnection) {
            eventListener.connectionAcquired(call, result);
        }
        if (result != null) {
            // 如果已經(jīng)從連接池中獲取到了一個連接,就將其返回
            return result;
        }

        boolean newRouteSelection = false;
        if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
            newRouteSelection = true;
            routeSelection = routeSelector.next();
        }

        synchronized (connectionPool) {
            if (canceled) throw new IOException("Canceled");

            if (newRouteSelection) {
                // 根據(jù)一系列的 IP 地址從連接池中獲取一個鏈接
                List<Route> routes = routeSelection.getAll();
                for (int i = 0, size = routes.size(); i < size; i++) {
                    Route route = routes.get(i);
                    // 從連接池中獲取一個連接
                    Internal.instance.get(connectionPool, address, this, route);
                    if (connection != null) {
                        foundPooledConnection = true;
                        result = connection;
                        this.route = route;
                        break;
                    }
                }
            }

            if (!foundPooledConnection) {
                if (selectedRoute == null) {
                    selectedRoute = routeSelection.next();
                }

                // 創(chuàng)建一個新的連接傲须,并將其分配蓝牲,這樣我們就可以在握手之前進行終端
                route = selectedRoute;
                refusedStreamCount = 0;
                result = new RealConnection(connectionPool, selectedRoute);
                acquire(result, false);
            }
        }

        // 如果我們在第二次的時候發(fā)現(xiàn)了一個池連接,那么我們就將其返回
        if (foundPooledConnection) {
            eventListener.connectionAcquired(call, result);
            return result;
        }

        // 進行 TCP 和 TLS 握手
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        routeDatabase().connected(result.route());

        Socket socket = null;
        synchronized (connectionPool) {
            reportedAcquired = true;

            // 將該連接放進連接池中
            Internal.instance.put(connectionPool, result);

            // 如果同時創(chuàng)建了另一個到同一地址的多路復用連接泰讽,釋放這個連接并獲取那個連接
            if (result.isMultiplexed()) {
                socket = Internal.instance.deduplicate(connectionPool, address, this);
                result = connection;
            }
        }
        closeQuietly(socket);

        eventListener.connectionAcquired(call, result);
        return result;
    }

該方法會被放置在一個循環(huán)當中被不停地調(diào)用以得到一個可用的連接例衍。它優(yōu)先使用當前已經(jīng)存在的連接,不然就使用連接池中存在的連接已卸,再不行的話佛玄,就創(chuàng)建一個新的連接。所以累澡,上面的代碼大致分成三個部分:

  1. 判斷當前的連接是否可以使用:流是否已經(jīng)被關(guān)閉梦抢,并且已經(jīng)被限制創(chuàng)建新的流;
  2. 如果當前的連接無法使用愧哟,就從連接池中獲取一個連接奥吩;
  3. 連接池中也沒有發(fā)現(xiàn)可用的連接,創(chuàng)建一個新的連接蕊梧,并進行握手霞赫,然后將其放到連接池中。

在從連接池中獲取一個連接的時候肥矢,使用了 Internalget() 方法端衰。Internal 有一個靜態(tài)的實例,會在 OkHttpClient 的靜態(tài)代碼快中被初始化橄抹。我們會在 Internalget() 中調(diào)用連接池的 get() 方法來得到一個連接靴迫。

從上面的代碼中我們也可以看出,實際上楼誓,我們使用連接復用的一個好處就是省去了進行 TCP 和 TLS 握手的一個過程。因為建立連接本身也是需要消耗一些時間的名挥,連接被復用之后可以提升我們網(wǎng)絡訪問的效率疟羹。那么這些連接被放置在連接池之后是如何進行管理的呢?我們會在下文中分析 OkHttp 的 ConnectionPool 中是如何管理這些連接的。

2.7 CallServerInterceptor

服務器請求攔截器 CallServerInterceptor 用來向服務器發(fā)起請求并獲取數(shù)據(jù)榄融。這是整個責任鏈的最后一個攔截器参淫,這里沒有再繼續(xù)調(diào)用執(zhí)行鏈的處理方法,而是把拿到的響應處理之后直接返回給了上一級的攔截器:

    public final class CallServerInterceptor implements Interceptor {

        @Override public Response intercept(Chain chain) throws IOException {
            RealInterceptorChain realChain = (RealInterceptorChain) chain;
            // 獲取 ConnectInterceptor 中初始化的 HttpCodec
            HttpCodec httpCodec = realChain.httpStream();
            // 獲取 RetryAndFollowUpInterceptor 中初始化的 StreamAllocation
            StreamAllocation streamAllocation = realChain.streamAllocation();
            // 獲取 ConnectInterceptor 中初始化的 RealConnection
            RealConnection connection = (RealConnection) realChain.connection();
            Request request = realChain.request();

            long sentRequestMillis = System.currentTimeMillis();

            realChain.eventListener().requestHeadersStart(realChain.call());
            // 在這里寫入請求頭 
            httpCodec.writeRequestHeaders(request);
            realChain.eventListener().requestHeadersEnd(realChain.call(), request);

            Response.Builder responseBuilder = null;
            if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
                if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
                    httpCodec.flushRequest();
                    realChain.eventListener().responseHeadersStart(realChain.call());
                    responseBuilder = httpCodec.readResponseHeaders(true);
                }
                 // 在這里寫入請求體
                if (responseBuilder == null) {
                    realChain.eventListener().requestBodyStart(realChain.call());
                    long contentLength = request.body().contentLength();
                    CountingSink requestBodyOut =
                            new CountingSink(httpCodec.createRequestBody(request, contentLength));
                    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
                    // 寫入請求體
                    request.body().writeTo(bufferedRequestBody);
                    bufferedRequestBody.close();
                    realChain.eventListener()
                            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
                } else if (!connection.isMultiplexed()) {
                    streamAllocation.noNewStreams();
                }
            }
            httpCodec.finishRequest();
            if (responseBuilder == null) {
                realChain.eventListener().responseHeadersStart(realChain.call());
                // 讀取響應頭
                responseBuilder = httpCodec.readResponseHeaders(false);
            }
            Response response = responseBuilder
                    .request(request)
                    .handshake(streamAllocation.connection().handshake())
                    .sentRequestAtMillis(sentRequestMillis)
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();
            // 讀取響應體
            int code = response.code();
            if (code == 100) {
                responseBuilder = httpCodec.readResponseHeaders(false);
                response = responseBuilder
                        .request(request)
                        .handshake(streamAllocation.connection().handshake())
                        .sentRequestAtMillis(sentRequestMillis)
                        .receivedResponseAtMillis(System.currentTimeMillis())
                        .build();
                code = response.code();
            }
            realChain.eventListener().responseHeadersEnd(realChain.call(), response);
            if (forWebSocket && code == 101) {
                response = response.newBuilder()
                        .body(Util.EMPTY_RESPONSE)
                        .build();
            } else {
                response = response.newBuilder()
                        .body(httpCodec.openResponseBody(response))
                        .build();
            }
            // ...
            return response;
        }
    }

2.8 連接管理:ConnectionPool

與請求的緩存類似愧杯,OkHttp 的連接池也使用一個雙端隊列來緩存已經(jīng)創(chuàng)建的連接:

private final Deque<RealConnection> connections = new ArrayDeque<>();

OkHttp 的緩存管理分成兩個步驟涎才,一邊當我們創(chuàng)建了一個新的連接的時候,我們要把它放進緩存里面力九;另一邊耍铜,我們還要來對緩存進行清理。在 ConnectionPool 中跌前,當我們向連接池中緩存一個連接的時候棕兼,只要調(diào)用雙端隊列的 add() 方法,將其加入到雙端隊列即可抵乓,而清理連接緩存的操作則交給線程池來定時執(zhí)行伴挚。

ConnectionPool 中存在一個靜態(tài)的線程池:

    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
        Integer.MAX_VALUE /* maximumPoolSize */, 
        60L /* keepAliveTime */,
        TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(), 
        Util.threadFactory("OkHttp ConnectionPool", true));

每當我們向連接池中插入一個連接的時候就會調(diào)用下面的方法,將連接插入到雙端隊列的同時灾炭,會調(diào)用上面的線程池來執(zhí)行清理緩存的任務:

    void put(RealConnection connection) {
        assert (Thread.holdsLock(this));
        if (!cleanupRunning) {
            cleanupRunning = true;
            // 使用線程池執(zhí)行清理任務
            executor.execute(cleanupRunnable);
        }
        // 將新建的連接插入到雙端隊列中
        connections.add(connection);
    }

這里的清理任務是 cleanupRunnable茎芋,是一個 Runnable 類型的實例。它會在方法內(nèi)部調(diào)用 cleanup() 方法來清理無效的連接:

    private final Runnable cleanupRunnable = new Runnable() {
        @Override public void run() {
            while (true) {
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

下面是 cleanup() 方法:

    long cleanup(long now) {
        int inUseConnectionCount = 0;
        int idleConnectionCount = 0;
        RealConnection longestIdleConnection = null;
        long longestIdleDurationNs = Long.MIN_VALUE;

        synchronized (this) {
            // 遍歷所有的連接
            for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
                RealConnection connection = i.next();
                // 當前的連接正在使用中
                if (pruneAndGetAllocationCount(connection, now) > 0) {
                    inUseConnectionCount++;
                    continue;
                }
                idleConnectionCount++;
                // 如果找到了一個可以被清理的連接蜈出,會嘗試去尋找閑置時間最久的連接來釋放
                long idleDurationNs = now - connection.idleAtNanos;
                if (idleDurationNs > longestIdleDurationNs) {
                    longestIdleDurationNs = idleDurationNs;
                    longestIdleConnection = connection;
                }
            }

            if (longestIdleDurationNs >= this.keepAliveDurationNs 
                    || idleConnectionCount > this.maxIdleConnections) {
                // 該連接的時長超出了最大的活躍時長或者閑置的連接數(shù)量超出了最大允許的范圍田弥,直接移除
                connections.remove(longestIdleConnection);
            } else if (idleConnectionCount > 0) {
                // 閑置的連接的數(shù)量大于0,停頓指定的時間(等會兒會將其清理掉掏缎,現(xiàn)在還不是時候)
                return keepAliveDurationNs - longestIdleDurationNs;
            } else if (inUseConnectionCount > 0) {
                // 所有的連接都在使用中皱蹦,5分鐘后再清理
                return keepAliveDurationNs;
            } else {
                // 沒有連接
                cleanupRunning = false;
                return -1;
            }
        }

        closeQuietly(longestIdleConnection.socket());
        return 0;
    }

在從緩存的連接中取出連接來判斷是否應該將其釋放的時候使用到了兩個變量 maxIdleConnectionskeepAliveDurationNs,分別表示最大允許的閑置的連接的數(shù)量和連接允許存活的最長的時間眷蜈。默認空閑連接最大數(shù)目為5個沪哺,keepalive 時間最長為5分鐘。

上面的方法會對緩存中的連接進行遍歷酌儒,以尋找一個閑置時間最長的連接辜妓,然后根據(jù)該連接的閑置時長和最大允許的連接數(shù)量等參數(shù)來決定是否應該清理該連接。同時注意上面的方法的返回值是一個時間忌怎,如果閑置時間最長的連接仍然需要一段時間才能被清理的時候籍滴,會返回這段時間的時間差,然后會在這段時間之后再次對連接池進行清理榴啸。

總結(jié):

以上就是我們對 OkHttp 內(nèi)部網(wǎng)絡訪問的源碼的分析孽惰。當我們發(fā)起一個請求的時候會初始化一個 Call 的實例,然后根據(jù)同步和異步的不同鸥印,分別調(diào)用它的 execute()enqueue() 方法勋功。雖然坦报,兩個方法一個會在當前的線程中被立即執(zhí)行,一個會在線程池當中執(zhí)行狂鞋,但是它們進行網(wǎng)絡訪問的邏輯都是一樣的:通過攔截器組成的責任鏈片择,依次經(jīng)過重試、橋接骚揍、緩存字管、連接和訪問服務器等過程,來獲取到一個響應并交給用戶信不。其中嘲叔,緩存和連接兩部分內(nèi)容是重點,因為前者涉及到了一些計算機網(wǎng)絡方面的知識浑塞,后者則是 OkHttp 效率和框架的核心借跪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市酌壕,隨后出現(xiàn)的幾起案子掏愁,更是在濱河造成了極大的恐慌,老刑警劉巖卵牍,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件果港,死亡現(xiàn)場離奇詭異,居然都是意外死亡糊昙,警方通過查閱死者的電腦和手機辛掠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來释牺,“玉大人萝衩,你說我怎么就攤上這事∶涣” “怎么了猩谊?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長祭刚。 經(jīng)常有香客問我牌捷,道長,這世上最難降的妖魔是什么涡驮? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任暗甥,我火速辦了婚禮,結(jié)果婚禮上捉捅,老公的妹妹穿的比我還像新娘撤防。我一直安慰自己,他們只是感情好棒口,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布即碗。 她就那樣靜靜地躺著焰情,像睡著了一般陌凳。 火紅的嫁衣襯著肌膚如雪剥懒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天合敦,我揣著相機與錄音初橘,去河邊找鬼。 笑死充岛,一個胖子當著我的面吹牛保檐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播崔梗,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼夜只,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蒜魄?” 一聲冷哼從身側(cè)響起扔亥,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谈为,沒想到半個月后旅挤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡伞鲫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年粘茄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秕脓。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡柒瓣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吠架,到底是詐尸還是另有隱情芙贫,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布诵肛,位于F島的核電站屹培,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏怔檩。R本人自食惡果不足惜褪秀,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望薛训。 院中可真熱鬧媒吗,春花似錦、人聲如沸乙埃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至甫何,卻和暖如春出吹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辙喂。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工捶牢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巍耗。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓秋麸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親炬太。 傳聞我的和親對象是個殘疾皇子灸蟆,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 用OkHttp很久了,也看了很多人寫的源碼分析亲族,在這里結(jié)合自己的感悟炒考,記錄一下對OkHttp源碼理解的幾點心得。 ...
    藍灰_q閱讀 4,281評論 4 34
  • 前言 用OkHttp很久了孽水,也看了很多人寫的源碼分析票腰,在這里結(jié)合自己的感悟,記錄一下對OkHttp源碼理解的幾點心...
    Java小鋪閱讀 1,521評論 0 13
  • 關(guān)于okhttp是一款優(yōu)秀的網(wǎng)絡請求框架女气,關(guān)于它的源碼分析文章有很多杏慰,這里分享我在學習過程中讀到的感覺比較好的文章...
    蕉下孤客閱讀 3,602評論 2 38
  • 不知為何,突然不想抬步 或許是繁忙的軀體讓我不得不歇息幾分 或許是我變得格外懶惰 或許是我根本找不到抬步去往的方向...
    劉留余閱讀 291評論 0 4
  • 【能量性格相處之道】第二階段突破舒適圈訓練 突破舒適圈訓練的四個步驟 第一 炼鞠、今天發(fā)生了什么事情缘滥,讓我感受到觸碰了...
    胡越作業(yè)閱讀 160評論 0 0