Stetho的介紹和使用

1、簡介

stetho是facebook開發(fā)的一個開源庫,Android應用通過引入stetho,可以在Chrome/Chromium瀏覽器監(jiān)控查看網(wǎng)絡請求、數(shù)據(jù)庫、SharedPreferences、UI布局層級等。

我覺得最好的一點是查看網(wǎng)絡請求,像網(wǎng)頁的開發(fā)者模式一樣,看網(wǎng)絡的執(zhí)行順序,耗費的時間,請求的參數(shù)、返回的數(shù)據(jù)。只需要裝上Chrome/Chromium瀏覽器,就能直觀的查看了,不用再打開Android Studio,調試打印日志,節(jié)省我們的時間,提供我們的工作效率,根據(jù)網(wǎng)絡做特定的優(yōu)化。

stetho_demo_1.gif

PS、我們在開發(fā)測試時,項目代碼引入stetho,便于測試和調試,APP上線版本,這些都要去掉。

本文代碼:https://github.com/jinxiyang/StethoDemo

2、Stetho的引入

Stetho庫的地址:

https://github.com/facebook/stetho

app項目的gradle引入:

//stetho的核心庫
implementation 'com.facebook.stetho:stetho:1.6.0'

//監(jiān)控okhttp的請求
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'

在APP的Application類中,初始化Stetho:

public class MyApplication extends Application {
  public void onCreate() {
    super.onCreate();
    //初始化stetho
    Stetho.initializeWithDefaults(this);
  }
}

在構建OkHttpClient時,添加網(wǎng)絡攔截器:

OkHttpClient.Builder builder = new OkHttpClient.Builder();
//添加StethoInterceptor網(wǎng)絡攔截器
builder.addNetworkInterceptor(new StethoInterceptor());
okHttpClient = builder.build();

3、功能介紹

引入Stetho后,運行APP,打開Chrome/Chromium瀏覽器,地址輸入:

chrome://inspect/#devices

如:

stetho_demo_2.jpg

點擊inspect,彈出DevTools,如下圖:

stetho_demo_3.png

有如若干個tab頁,其中用的最多的是NetworkResources。

Network頁,APP發(fā)送一個或者多次請求,會有相關請求的詳情,我們可以查看網(wǎng)絡的執(zhí)行順序、耗時情況、參數(shù)、返回數(shù)據(jù)、請求的調度順序等,對APP就行優(yōu)化。

stetho_demo_4.png

相關說明:

  • Path:網(wǎng)絡請求的URL
  • Method:Get或者POST請求
  • Status:請求狀態(tài)碼,200正常
  • Size:請求的數(shù)據(jù)大小
  • Time:請求的耗時,上一個時間是總耗時,下一個請求的等待時間,兩者只差是數(shù)據(jù)接收和處理時間
  • Timeline:請求的的時間線

點擊其中一個請求,可以看到請求的詳情,header、request、response、cookie等,如下圖:

stetho_demo_5.png
stetho_demo_6.png

點擊Resources頁,可以看APP的數(shù)據(jù)庫、SharedPreferences

stetho_demo_7.png
  • IndexedDB:數(shù)據(jù)庫,可執(zhí)行SQL語句,查看相應的數(shù)據(jù)
  • Local Storage:SharedPreferences,可直接點擊,修改數(shù)據(jù)

4、DevTools的Network頁如何展示加密的網(wǎng)絡請求?

網(wǎng)絡請求在實體網(wǎng)絡傳輸中,可能會被查看到,然后通過腳本模擬網(wǎng)絡請求,發(fā)起網(wǎng)絡攻擊。

有些APP會加密請求的參數(shù)和加密返回的數(shù)據(jù),以到達規(guī)避接口被查看的風險,增加安全性。

這時DevTools的Network頁,展示的就是被加密的密文,我們就不能直接看到了。有沒有辦法,使Network頁展示的明文(未加密的數(shù)據(jù)),實際發(fā)送時是密文呢?

答案是有的

我們知道,OKHttp使用的責任鏈模式,我們可以添加一個加解密攔截器。在請求發(fā)送時,先明文給到stetho展示,然后通過【加解密攔截器】加密請求數(shù)據(jù),發(fā)送給服務器。在服務器響應后,通過【加解密攔截器】解密返回數(shù)據(jù),給到stetho展示。

分析OKHTTP的RealCall.kt文件(OKHTTP的版本為:4.9.2),攔截器的調用鏈路代碼:

  @Throws(IOException::class)
  internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      //這里會用到我們添加的網(wǎng)絡攔截器:
      //1、StethoInterceptor
      //2、自定義的加解密攔截器
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,
        interceptors = interceptors,
        index = 0,
        exchange = null,
        request = originalRequest,
        connectTimeoutMillis = client.connectTimeoutMillis,
        readTimeoutMillis = client.readTimeoutMillis,
        writeTimeoutMillis = client.writeTimeoutMillis
    )

    var calledNoMoreExchanges = false
    try {
      val response = chain.proceed(originalRequest)
      if (isCanceled()) {
        response.closeQuietly()
        throw IOException("Canceled")
      }
      return response
    } catch (e: IOException) {
      calledNoMoreExchanges = true
      throw noMoreExchanges(e) as Throwable
    } finally {
      if (!calledNoMoreExchanges) {
        noMoreExchanges(null)
      }
    }
  }

調用鏈路流程圖:

stetho_demo_8.png

加解密攔截器示例代碼:

public class EncryptionInterceptor implements Interceptor {

    private static final String HEADER_NAME_CONTENT_LENGTH = "Content-Length";
    private static final String HEADER_NAME_ACCEPT_ENCODING = "Accept-Encoding";

    private static final String ACCEPT_ENCODING_GZIP_ENCODING = "gzip";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request.Builder requestBuilder = null;

        //加解密秘鑰
        //秘鑰不為空,我們要進行加解密
        String encryptKey = getEncryptKey(request);

        if (!TextUtils.isEmpty(encryptKey)){
            Headers headers = request.headers();
            String acceptEncoding = headers.get(HEADER_NAME_ACCEPT_ENCODING);
            if (!TextUtils.isEmpty(acceptEncoding) && !TextUtils.equals(acceptEncoding, ACCEPT_ENCODING_GZIP_ENCODING)){
                //response接收的編碼類型, 支持zip和空
                requestBuilder = request.newBuilder();
                requestBuilder.removeHeader(HEADER_NAME_ACCEPT_ENCODING);
            }
        }

        //秘鑰不為空,且請求body不為空
        if (!TextUtils.isEmpty(encryptKey) && HttpMethod.requiresRequestBody(request.method()) && request.body() != null){
            RequestBody requestBody = request.body();

            //讀取請求體的數(shù)據(jù)
            String body = requestBodyToString(requestBody);

            //加密請求體
            String encryptedBody = encryptBody(encryptKey, body);

            //重新生成請求體
            requestBody = RequestBody.create(requestBody.contentType(), encryptedBody);
            if (requestBuilder == null){
                requestBuilder = request.newBuilder();
            }

            if ("POST".equals(request.method())){
                requestBuilder.post(requestBody);
            } else if ("PUT".equals(request.method())){
                requestBuilder.put(requestBody);
            }

            //復寫Content-Length,數(shù)據(jù)變了,請求的的長度也就變了
            requestBuilder.removeHeader(HEADER_NAME_CONTENT_LENGTH);
            requestBuilder.addHeader(HEADER_NAME_CONTENT_LENGTH, String.valueOf(requestBody.contentLength()));
        }

        if (requestBuilder != null){
            //重新組裝request
            request = requestBuilder.build();
        }

        Response response = chain.proceed(request);

        if (TextUtils.isEmpty(encryptKey)){
            //如果不需加解密,直接返回請求響應
            return response;
        }


        String result;
        ResponseBody responseBody = response.body();

        String contentEncoding = response.header(HEADER_NAME_ACCEPT_ENCODING);
        boolean gzipEncoding = ACCEPT_ENCODING_GZIP_ENCODING.equals(contentEncoding);

        //根據(jù)請求響應的格式,讀取數(shù)據(jù)
        if (gzipEncoding){
            //讀取zip流數(shù)據(jù)
            GzipSource gzipSource = new GzipSource(responseBody.source());
            BufferedSource bufferedSource = Okio.buffer(gzipSource);
            result = bufferedSource.readUtf8();
        } else {
            result = responseBody.string();
        }

        //解密數(shù)據(jù),這里認為是對稱加解密
        result = decryptBody(encryptKey, result);

        //重新組裝response
        String contentType = response.header("Content-Type");
        long contentLength = result.length();//數(shù)據(jù)長度發(fā)生了變化
        ByteArrayInputStream inputStream = new ByteArrayInputStream(result.getBytes());
        Source source = Okio.source(inputStream);
        return response.newBuilder()
                .body(new RealResponseBody(contentType, contentLength, Okio.buffer(source)))
                .build();
    }

    //讀取請求體的數(shù)據(jù)
    private String requestBodyToString(RequestBody requestBody){
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputStream));
        try {
            requestBody.writeTo(bufferedSink);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            try {
                bufferedSink.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        byte[] bytes = outputStream.toByteArray();
        return new String(bytes);
    }


    /**
     * 加解密秘鑰
     */
    private String getEncryptKey(Request request){
        //TODO
        return null;
    }

    /**
     * 加密請求體
     */
    private String encryptBody(String encryptKey, String body){
        //TODO
        return null;
    }

    /**
     * 解密數(shù)據(jù)
     */
    private String decryptBody(String decryptKey, String body){
        //TODO
        return null;
    }
}

加解密攔截器的使用:

        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        //添加StethoInterceptor網(wǎng)絡攔截器
        builder.addNetworkInterceptor(new StethoInterceptor());
        //添加加解密網(wǎng)絡攔截器
        builder.addNetworkInterceptor(new EncryptionInterceptor());
        okHttpClient = builder.build();
  • stetho攔截器在前,加密攔截器在后。chrome看到的明文,實際網(wǎng)絡請求是密文
builder.addNetworkInterceptor(new StethoInterceptor());
builder.addNetworkInterceptor(new EncryptionInterceptor());
  • 加密攔截器在前,stetho攔截器在后。chrome看到的和實際網(wǎng)絡請求是一樣的,都是密文
builder.addNetworkInterceptor(new EncryptionInterceptor());
builder.addNetworkInterceptor(new StethoInterceptor());

5、DevTools彈窗,一直空白,加載不出,或者顯示HTTP/1.1 404 Not Found,如圖:

stetho_problem_1.png

解決方法:

需要翻墻,DevTools彈窗需要連接國外網(wǎng)絡

6、Chrome(谷歌瀏覽器)打開DevTools,界面顯示混亂,各個子頁面顯示不出來,如圖:

stetho_problem_2.gif

解決方法:

最新版Chrome(谷歌瀏覽器)有BUG,打開DevTools有問題,

1、換用Chromium瀏覽器(推薦)

Chromium的下載地址:

Linux x64: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/827102/

Mac OS: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/827102/

Windows: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win/827102/

2、安裝老版的Chrome(谷歌瀏覽器),用19年版本的

PS,Stetho官方也有這個問題記錄,https://github.com/facebook/stetho/issues/696,感興趣的可以去看看

本文代碼:https://github.com/jinxiyang/StethoDemo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容