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)化。

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
如:

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

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

相關說明:
- Path:網(wǎng)絡請求的URL
- Method:Get或者POST請求
- Status:請求狀態(tài)碼,200正常
- Size:請求的數(shù)據(jù)大小
- Time:請求的耗時,上一個時間是總耗時,下一個請求的等待時間,兩者只差是數(shù)據(jù)接收和處理時間
- Timeline:請求的的時間線
點擊其中一個請求,可以看到請求的詳情,header、request、response、cookie等,如下圖:


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

- 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)
}
}
}
調用鏈路流程圖:

加解密攔截器示例代碼:
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,如圖:

解決方法:
需要翻墻,DevTools彈窗需要連接國外網(wǎng)絡
6、Chrome(谷歌瀏覽器)打開DevTools,界面顯示混亂,各個子頁面顯示不出來,如圖:

解決方法:
最新版Chrome(谷歌瀏覽器)有BUG,打開DevTools有問題,
1、換用Chromium瀏覽器(推薦)
Chromium的下載地址:
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,感興趣的可以去看看