如何給安卓 APP 安裝聽診器,檢查數據問題

開發者在開發中想查看安卓APP運行時的網絡訪問和數據存儲狀況,調試太麻煩,日誌也挺煩,有沒有更好的辦法呢?Facebook給廣大開發者傳了福音,帶了福利,放在下午茶的小桌子上,美食乾貨不敢獨吞,因此拿來分享給你們javascript

從事移動端安卓APP的開發,除了代碼邏輯以外就是在和數據打交道。數據的輸入輸出,往返於網絡接口之間,流竄於內存之中存儲以內,不能像編寫的代碼那樣直接在代碼編輯器中看到其具體的內容。因此若是想窺探數據的真僞對錯,目前來講,不外三法。本文開始,告訴你第四條路。html

現狀,以及各自的問題

前面說傳統上有兩條路能夠幫助開發者查看APP運行過程當中處理的數據,這裏簡單描述下處理方式以及每種方式的優缺點。java

  • 斷點調試運行中的APP。你能夠用調試器直接調試一個APP,但若是這個APP過於龐大,初始化加載時間好久,那麼,最好的調試辦法是先將APP在設備(手機或者模擬器)上運行起來,而後用attach to process的方式在被調試的APP進程上加載調試器,這樣會比一上來直接調試APP更快一些。想看數據的話,直接在相應代碼行加上斷點,附着在APP進程上的調試器會自動斷下程序,而後查看當前上下文中各類變量的值以及內存的數據,也能夠修改這些數據。可是,若是你想看某個數據是否是真的寫到存儲的文件裏,估計須要添加額外的讀取代碼來查看,並且,每次給APP掛調試器查看數據,感受仍是有些不方便。若是你想診斷和分析網絡的訪問速度和數據的流量,數據存儲的空間和整體數據量,單靠調試這種手段顯得力不從心了。並且若是斷點的地方在UI的某處代碼,長時間處於斷點狀態查看數據,會致使APP發生ANR的異常。python

  • 加打印日誌。相似產品運營的埋點和服務端訪問/操做日誌,咱們也能夠在客戶端APP相應的位置大書相似到此一遊此地無淫三百靚的句式,讓APP進程經過一個叫控制檯的老東西(console是計算機世界的老司機了,啥大風大浪沒見過的)告訴咱們發生了啥,如何發生的,以及發生的結果如何。斷點很差作到的網絡訪問速度和數據流量等東西也能夠經過日誌叫喚了。這麼看起來,貌似加日誌已是一種很完美的辦法了。可是,你有沒有感受到這樣超級麻煩?首先是你的代碼量忽然變大了,代碼結構變醜了,代碼環境衛生變差了,翠花上的酸菜我不敢吃了。相信我,日誌海(骷髏海的代碼態)必定會讓你疲憊的雙眼猶如暴風驟雨裏的一葉孤舟,說翻就翻,眼都不帶眨一下的。說人話,日誌是一種侵入式的調試手段,啥叫侵入式?就是它必須由您老人家親自動手埋藏在代碼的心房裏,直到天荒地老,APP下架,它也不會化做半點春泥更護花的。而對於調試來講,看日誌的情調less than lower,千篇飛過如同嚼蠟。看過安卓日誌的童鞋都知道,前塵往事並木有渺雲煙,那些個每天在微信羣裏大呼小叫的羣主來看看到底啥叫刷屏。可憐的安卓開發們,每天被日誌刷屏。android

  • 藉助第三方工具。對於網絡來講,基本就是設置代理,最經常使用的不外乎Charles (收費,基於Java開發,跨平臺);Fiddler(免費&收費,基於.Net開發,目前支持經過mono的方式運行在Mac和Linux上);Mitmproxy(免費&開源,基於Python開發,跨平臺);還有比較麻煩的辦法,好比Http/Https代理+Wireshark/tcpdump這種。這些工具只能知足網絡監控,對於非網絡數據就無能爲力了。對於存儲在手機上的數據,能夠經過adb登錄到手機,得到root權限後查看APP內部數據,也能夠採用一些安裝在手機端的帶圖形界面的APP來查看和修改數據,好比SQLEditor之類的,這類APP一樣須要獲取root權限。c++

那麼,後來,Facebook給咱們這些可憐的娃帶來了福音和福利,試試看咯git

聽診器來了

Stetho英譯爲「聽診」,是Facebook研發的安卓APP網絡診斷和數據監控的框架,目前已經開放源代碼,開發者接入Stetho框架提供的SDK到APP中,這樣就能夠經過安裝在開發機(PC/MAC,Windows/OS X/Linux)上安裝的谷歌的Chrome開發者工具(經過Chrome瀏覽器使用)來查看,診斷和分析APP中發生的網絡請求和響應以及數據內容,就像用Chrome調試網站同樣調試APP程序。固然,幾乎任何工具都自帶老司機console,Stetho也不例外,它提供了一個叫作dumpapp的工具,能夠向你傾述更多的APP心裏世界。github

接入其實很簡單

再簡單的接入也總有1234步,這裏簡單叨逼叨逼幾句。chrome

gradle配置

這裏不說mvn和low逼的下載&拷貝庫的方式了(拷貝源代碼的方式集成就更不能忍了),直接上gradle配置數據庫

// Gradle dependency on Stetho 
  dependencies { 
    compile 'com.facebook.stetho:stetho:1.3.1' 
  }

若是你使用了Okhttp 3.x的網絡棧,請集成以下網絡工具庫

dependencies { 
    compile 'com.facebook.stetho:stetho-okhttp3:1.3.1' 
  }

Okhttp 2.2.x+

dependencies { 
    compile 'com.facebook.stetho:stetho-okhttp:1.3.1' 
  }

若是使用的是HttpURLConnection

dependencies { 
    compile 'com.facebook.stetho:stetho-urlconnection:1.3.1' 
  }

小白兔和大灰狼請注意:

  • 若是你使用的是Apache HttpClient,對不起,你out了,請自行升級網絡棧,固然你也能夠在瞭解了Stetho的玩法以後本身寫一套網絡監控來適配Apache HttpClient。

  • 若是你使用的網絡棧不在上面列舉的裏面,或者你用c/c++寫的網絡操做,又或者你採用的協議不是http/https的,那麼,網絡這部分的診斷和監控方法,估計是很難用了。還想用,本身寫咯。

初始化

須要寫的代碼其實不多,並且幾乎全部的應用都是同樣的代碼,首先是在Application類中初始化

public class MyApplication extends Application {
  public void onCreate() {
    super.onCreate();
    Stetho.initializeWithDefaults(this);
  }
}

這個初始化會開啓大部分的聽診模塊,可是網絡監控等一些附加的鉤子模塊除外

網絡診斷

若是你使用的網絡棧是OkHttp,並且版本區間在2.2.x+到3.x,那麼想要打開網絡診斷模塊,只須要在程序合適的位置調用以下代碼便可

OkHttp 2.2.x+

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

OkHttp 3.x

new OkHttpClient.Builder()
    .addNetworkInterceptor(new StethoInterceptor())
    .build();

HttpURLConnection

若是你使用的HttpURLConnection,稍微有些麻煩的說,你可使用Stetho框架SDK提供的類StethoURLConnectionManager來完成客戶端網絡診斷的開啓,可是有一些坑是要注意的。

好比爲了讓Stetho向開發機上的Chrome彙報正確的通過壓縮的有效載荷的大小,你須要親自在http/https的請求頭加上"Accept-Encoding: gzip",而且本身處理壓縮過的響應數據。若是採用Okhttp,這些都沒必要勞煩您老人家操心了,框架默認幫你考慮了。

參考代碼以下:

private final StethoURLConnectionManager stethoManager;

private static final int READ_TIMEOUT_MS = 10000;
private static final int CONNECT_TIMEOUT_MS = 15000;

private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
private static final String GZIP_ENCODING = "gzip";

private String url = "http://www.figotan.org";
stethoManager = new StethoURLConnectionManager(url);

URL url = new URL(url);

// Note that this does not actually create a new connection so it is appropriate to
// defer preConnect until after the HttpURLConnection instance is configured.  Do not
// invoke connect, conn.getInputStream, conn.getOutputStream, etc before calling
// preConnect!
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
try {
    conn.setReadTimeout(READ_TIMEOUT_MS);
    conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
    conn.setRequestMethod(request.method.toString());

    // Adding this disables transparent gzip compression so that we can intercept
    // the raw stream and display the correct response body size.
    conn.setRequestProperty(HEADER_ACCEPT_ENCODING, GZIP_ENCODING);

    SimpleRequestEntity requestEntity = null;
    if (request.body != null) {
        requestEntity = new ByteArrayRequestEntity(request.body);
    }

    stethoManager.preConnect(conn, requestEntity);
    try {
          if (request.method == HttpMethod.POST) {
            if (requestEntity == null) {
              throw new IllegalStateException("POST requires an entity");
            }
            conn.setDoOutput(true);
            requestEntity.writeTo(conn.getOutputStream());
          }

          // Ensure that we are connected after this point.  Note that getOutputStream above will
          // also connect and exchange HTTP messages.
          conn.connect();

          stethoManager.postConnect();

    } catch (IOException inner) {
          // This must only be called after preConnect.  Failures before that cannot be
          // represented since the request has not yet begun according to Stetho.
          stethoManager.httpExchangeFailed(inner);
          throw inner;
    }
} catch (IOException outer) {
        conn.disconnect();
        throw outer;
}
      
try {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    InputStream rawStream = conn.getInputStream();
    try {
        // Let Stetho see the raw, possibly compressed stream.
        rawStream = stethoManager.interpretResponseStream(rawStream);
        if (rawStream != null && GZIP_ENCODING.equals(conn.getContentEncoding())) {
            decompressedStream  = new GZIPInputStream(in);
        } else {
            decompressedStream = rawStream;
        } 
    
        if (decompressedStream != null) {
            int n;
            byte[] buf = new byte[1024];
            while ((n = decompressedStream.read(buf)) != -1) {
                out.write(buf, 0, n);
            }
        }
    } finally {
        if (rawStream != null) {
            rawStream.close();
          }
    }
} finally {
    conn.disconnect();
}

經過如上步驟,已經可讓你的APP支持網絡監控,數據庫監控和SharedPreferences文件內容監控了。若是想玩更高級的,請看後面的自定義dumpapp插件和Rhino,若是想直接玩起來,請繼續往下看。

使用起來也不麻煩

使用步驟以下:

  1. 首先在手機上運行APP

  2. 確保手機USB鏈接開發機,在開發機上打開Chrome瀏覽器

  3. 在Chrome瀏覽器地址欄中輸入chrome://inspect,會看到以下這張圖,若是圖裏面沒有你的APP,請返回到上面檢查代碼接入是否正確 

  4. 點擊APP旁邊的inspect鏈接,這個時候會彈出一個窗口,若是你用過Chrome的開發者工具,是否是會以爲這個界面很熟悉?對了,這個窗口就是Chrome內置的開發者工具,只不過裏面監控的內容從網頁變成了APP 

  5. 首先看功能導航條的第一個tab,叫作"Elements",工做區中的內容是否是很熟悉,Hierarchy Viewer,很像吧,點擊具體的xml節點,能夠看到鏈接的手機上對應的UI控件高亮顯示了,這個能夠像Hierarchy Viewer那樣分析APP頁面的嵌套層級 

  6. 第二個tab叫作"Network",是用來作網絡監控的,基本上覆蓋了Chrome開發者工具中"Network inspection"的全部功能點,包括下載圖片的預覽,JSON數據查看,網絡請求內容和返回內容 

  7. 第三個tab是"Sources",用來查看網頁的詳細內容 

  8. 直接跳過"Timeline", "Profiles"看第六個tab,"Resources",顧名思義,這裏應該就是查看APP內部產生數據的地方啦,目前支持的數據有兩種,一種是數據庫(ContentProvider和Sql的方式)的數據,另外一種是SharedPreferences數據 

  9. "Audits" 跳過,如同"Timeline"和"Profiles",目前沒怎麼支持,有待進一步發掘的功能。

  10. "Console"老司機下面講

有坑嗎?

關於網絡監控的有一些須要注意的字段的含義,詳細的內容能夠去精讀一遍Chrome開發者工具官方文檔

這裏只詳細解釋下上面圖中個字段的含義。

  • Name/Path 網絡資源的名稱和URL路徑,好比http://www.figotan.org/c/v/logo.jpg這個網絡資源,Name是logo.jpg Path是www.figotan.org/c/v/

  • Method HTTP協議規定的請求方法,好比GET POST

  • Status/Text HTTP協議規定的返回碼和這個返回碼對應的含義解釋文字 ,好比200/OK

  • Type 請求資源的MIME類型,好比application/json image/jpeg image/png等等

  • Initiator 發起請求的對象,能夠是Parser/Redirect/Script/Other,詳見上面的官方文檔

  • Size/Content Size表示HTTP響應的頭和數據體的和,由遠程服務端返回;Content是返回的資源解碼後的大小

  • Time/Latentcy Time是總的時間間隔,從發起請求開始到接收到服務端返回的最後一個字節爲止;Latency是指的接收到服務端返回的第一個字節消耗的時間

  • Timeline 顯示了全部網絡請求的瀑布流

還能夠作什麼

除了監控網絡,查看/修改數據以外,還能夠作不少事情,由於Stetho預留了兩種接口,爲了可持續的發展

自定義dumpapp插件

自定義插件是讓老司機dumpapp get新技能的首選方法,能夠很容易地在配置過程當中添加。只需以下代碼便可添加自定義插件:

Stetho.initialize(Stetho.newInitializerBuilder(context)
        .enableDumpapp(new DumperPluginsProvider() {
          @Override
          public Iterable<DumperPlugin> get() {
            return new Stetho.DefaultDumperPluginsBuilder(context)
                .provide(new HelloWorldDumperPlugin())
                .provide(new APODDumperPlugin(context.getContentResolver()))
                .finish();
          }
        })
        .enableWebKitInspector(new ExtInspectorModulesProvider(context))
        .build());

其中HelloWorldDumperPlugin和APODDumperPlugin是自定義的插件,具體內容能夠參考Stetho提供的sample程序 
執行dumpapp命令須要先從git取下最新的代碼,而後找到dumpapp腳本,並執行

$ git clone https://github.com/facebook/stetho.git
$ cd stetho
// 列舉出支持的命令(插件)
$ ./scripts/dumpapp -p com.facebook.stetho.sample -l

參照sample代碼編寫dumpapp插件,而後用dumpapp命令驗證插件的效果

Stetho對於JavaScript的支持

目前Stetho對於JavaScript腳本的支持是採用內嵌Mozilla研發的Rhino

第一種採用dumpapp的插件擴展方式雖然功能強大,無所不能,可是完成一件事情須要必定的技術和時間成本,必須經歷一系列的編寫,編譯,構建,安裝,調試,修改代碼,再下一個輪迴,迭代幾回後才能造成產出,這實際上是類c/c++/java這種非動態語言的一個缺陷,軟件的研發週期太長。那麼,若是有一種寫完即發佈的腳本語言可以支持起來,其實對於研發效率來講,是有很大提高的,好比lua/javascript/perl/python/groovy等等,這樣的語言輕巧,無需編譯,寫完就能夠發佈驗證,甚至能夠邊寫邊調試邊上線。

Chrome開發者工具原生支持JavaScript,因此Stetho也提供了JavaScript的支持。

Rhino是一個能夠運行在Java程序內部的JavaScript實現,由Mozilla開發併發布爲一個開源的項目

下面說說集成和使用方式

若是要讓APP支持Rhino, 首先是gradle配置

dependencies { 
    compile 'com.facebook.stetho:stetho-js-rhino:1.3.1' 
}

而後就能夠經過開發機上Chrome瀏覽器提供的開發者工具的"Console"老司機(任何工具都有老司機console)來發射JavaScript代碼了,參考代碼以下:

importPackage(android.widget);
importPackage(android.os);
var handler = new Handler(Looper.getMainLooper());
handler.post(function() { Toast.makeText(context, "Hello from JavaScript", Toast.LENGTH_LONG).show() });

運行效果以下:

若是你想經過APP傳遞一些變量,類,閉包和函數給JavaScript運行環境中,那麼你能夠在Stetho初始化的時候添加以下代碼:

Stetho.initialize(Stetho.newInitializerBuilder(context)
        .enableWebKitInspector(new InspectorModulesProvider() {
          @Override
          public Iterable<ChromeDevtoolsDomain> get() {
            return new DefaultInspectorModulesBuilder(context).runtimeRepl(
                new JsRuntimeReplFactoryBuilder(context)
                    // Pass to JavaScript: var foo = "bar";
                    .addVariable("foo", "bar")
                    .build()
            ).finish();
          }
        })
        .build());

更多玩法請移步Rhino on Stetho

參考資料

Stetho官方文檔 
Stetho源代碼 
Stetho: A new debugging platform for Android 
A First Glance at Stetho tool 
Remote Debugging on Android with Chrome 
Chrome DevTools Overview

https://segmentfault.com/a/1190000012075067

相關文章
相關標籤/搜索