Stetho,一个功能强大的 Android 应用调试桥

snvq8521 7年前
   <h2><strong>What is Stetho ?</strong></h2>    <p>Stetho 是一个功能强大的 Android 应用调试桥,起到桥梁的作用,连接 Android 应用和 Chrome,通过 Chrome 开发者工具调试 Android 应用,提供视图元素检查,网络监控,数据库动态交互,Dumpapp(可扩展的命令行交互接口),JavaScript Console 等功能。</p>    <p>当启用后,开发者可以通过 Chrome 桌面浏览器中的开发者工具访问本地应用。开发者也可以选择启用可选的 dumpapp 工具提供一个强大的应用内部命令行接口。</p>    <p>旦你完成了下面的设置说明,只要启动你电脑上的 Chrome 浏览器并输入 chrome://inspect 点击 "Inspect" 按钮即可开始调试。</p>    <h2><strong>配置说明</strong></h2>    <ol>     <li> <p>添加 stetho 主依赖</p> <p>在 Gradle 中包含 stetho</p> <pre>  <code class="language-java">// Gradle dependency on Stetho      dependencies {        compile 'com.非死book.stetho:stetho:1.4.1'      }</code></pre> <p>在 Maven 中包含 stetho</p> <pre>  <code class="language-java"><dependency>       <groupid>com.非死book.stetho</groupid>        <artifactid>stetho</artifactid>        <version>1.4.1</version>      </dependency></code></pre> </li>     <li> <p>只有 stetho 主依赖是必须的,但你可能还希望有一个网络助手</p> <pre>  <code class="language-java">dependencies {        compile 'com.非死book.stetho:stetho-okhttp3:1.4.1'      }</code></pre> <p>或者:</p> <pre>  <code class="language-java">dependencies {        compile 'com.非死book.stetho:stetho-okhttp:1.4.1'      }</code></pre> <p>或者:</p> <pre>  <code class="language-java">dependencies {        compile 'com.非死book.stetho:stetho-urlconnection:1.4.1'      }</code></pre> </li>    </ol>    <h2><strong>功能说明</strong></h2>    <h2><strong>Chrome DevTools</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/40200ea14d05a85ea568b2b08ba57e6b.png"></p>    <p>Stetho 为你的应用提供了 C/S 协议实现,所以你可以通过 Chrome 集成的前端开发工具访问你的应用。只要你的应用集成了 Stetho,只需导航到 chrome://inspect 并点击 "Inspect" 即可开始使用。</p>    <h2><strong>Network Inspection</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b304a3f051e674114882c2d6ccdeca6d.png"></p>    <p>使用 Chrome 开发者工具各种功能实现网络监控,包括图片预览,JSON 响应辅助工具,甚至把跟踪信息导出为 HAR 格式文件。</p>    <h2><strong>Database Inspection</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e4129e7663a7d155e9e11449c7409843.png"></p>    <p>SQLite 数据库可视化与交互,具备完全读写功能,</p>    <ul>     <li>Web SQL 下的是应用的数据库,点击数据库可以输入 SQL 语句对其进行操作</li>     <li>Local Storage 就是对应 Android 下的 SharedPreferences,可修改 SharedPreferences 中的值</li>    </ul>    <h2><strong>View Hierarchy</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/36c1d814cde431caf294fc0cf21e7121.png"></p>    <p>View Hierarchy 支持 API 15 或更高版本。使用 View Hierarchy 可以很方便的检查界面元素,比如:</p>    <ol>     <li>View Hierarchy 中包含界面所有元素的层次结构和属性</li>     <li>鼠标移动到 View Hierarchy 中某个 View,app 中对应的 View 会高亮显示</li>     <li>点击 View Hierarchy 左上角的搜索按钮,再点击 app 当前界面的控件,View Hierarchy 会显示该控件在层次中的位置</li>    </ol>    <h2><strong>dumpapp</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4e6fa16a91d25d3cf44ffc65176b6e6e.png"></p>    <p>Dumpapp 为应用提供了一个可扩展的命令行交互接口,提供了一组默认的插件,但是 dumpapp 的真正强大之处在于能够轻松创建自己的插件!</p>    <p>dumpapp 就在工程的 scripts/dumpapp 下,遗憾的是目前在 Windows 下还用不了,因为它只提供了 Linux/Mac 下的执行脚本。</p>    <p>常用命令(插件):</p>    <ul>     <li>列出所有 Plugin : ./scripts/dumpapp -p com.非死book.stetho.sample -l</li>     <li>打印 SharedPreferences : ./scripts/dumpapp prefs print</li>     <li>写 SharedPreferences : ./scripts/dumpapp prefs write <path> <key> <type> <value></li>    </ul>    <p>dumpapp 默认提供的插件就在 com.非死book.stetho.dumpapp.plugins.* ,具体使用方法可以参考源码中的说明。</p>    <h2><strong>JavaScript Console</strong></h2>    <p><img src="https://simg.open-open.com/show/175daa28f5287aef43de4c6e1504f725.png"></p>    <p>JavaScript Console 允许执行那些可以与应用或 Android SDK 交互的 JavaScript 代码。</p>    <p>Stetho 使用 Rhino 实现使用脚本方式调用 Java。</p>    <p>Rhino 是一个完全使用Java语言编写的开源JavaScript实现。Rhino通常用于在Java程序中,为最终用户提供脚本化能力。它被作为J2SE 6上的默认Java脚本化引擎。</p>    <h2><strong>集成说明</strong></h2>    <h3><strong>1. 初始化</strong></h3>    <p>在你的 Application 初始化时候调用 Stetho 的初始化方法:</p>    <pre>  <code class="language-java">public class MyApplication extends Application {    public void onCreate() {      super.onCreate();      Stetho.initializeWithDefaults(this);    }  }</code></pre>    <p>这将启用大多数默认配置,但不启用一些额外的钩子(启用网络监控需要注意)。 有关各个子系统的具体细节,请参见下文。</p>    <h3><strong>2. 启用网络监控</strong></h3>    <p>如果你使用的是 2.2.x+ 或 3.x 版本的 OkHttp 库,可以使用拦截器系统自动挂接到现有堆栈。 这是目前启用网络监控的最简单和最直接的方法。</p>    <p>For OkHttp 2.x</p>    <pre>  <code class="language-java">OkHttpClient client = new OkHttpClient();  client.networkInterceptors().add(new StethoInterceptor());</code></pre>    <p>For OkHttp 3.x</p>    <pre>  <code class="language-java">new OkHttpClient.Builder()      .addNetworkInterceptor(new StethoInterceptor())      .build();</code></pre>    <p>由于拦截器可以修改请求和响应,应该在其他拦截器之后添加 Stetho 拦截器以获取准确的网络交互视图。</p>    <p>如果你使用 HttpURLConnection ,可以使用 StethoURLConnectionManager 来帮助集成,但该方法有一些注意事项,比如你必须明确地添加 Accept-Encoding:gzip 到请求头,并手动处理压缩的响应,以便 Stetho 报告压缩的有效负载大小。具体可以参考 stetho-sample 中的 Networker 的实现。</p>    <p>Stetho 目前没有提供 HttpClient 网络监控支持,具体原因可以查看 issues 116 (HttpClient 在 Android5.0 已经被废弃,不建议再使用)。</p>    <p>OkHttp + Retrofit</p>    <p>一般开发中我们都会使用 OkHttp + Retrofit , OkHttp 用于 HTTP 网络交互, Retrofit 用于将 HTTP API 转换为 Java 接口。</p>    <p>默认情况下, Retrofit 会自己创建一个 OkHttpClient ,我们也可以在创建 Retrofit 的时候通过 client(OkHttpClient client) 方法提供一个 OkHttpClient 。</p>    <pre>  <code class="language-java">sRetrofit = new Retrofit.Builder()          .baseUrl(baseUrl)          .client(sClient)          .build();</code></pre>    <p>通过 client(OkHttpClient client) 方法设置共用一个 OkHttpClient ,监控 Retrofit 中的网络交互。</p>    <p>更多细节见 <a href="/misc/goto?guid=4959726434148822803" rel="nofollow,noindex">stetho-sample</a> 项目。</p>    <h3><strong>3. 自定义 dumpapp 插件</strong></h3>    <p>自定义插件主要是实现 DumperPlugin 接口中的 String getName() 和 void dump(DumperContext dumpContext) 方法。 getName() 方法返回插件的名称, dump(DumperContext dumpContext) 是命令行中调用该插件时的回调方法。其中, dumpContext.getStdout() 获取命令行输出, dumpContext.getArgsAsList() 获取命令行调用的参数列表。</p>    <pre>  <code class="language-java">public class MyDumperPlugin implements DumperPlugin {      private static final String XML_SUFFIX = ".xml";    private static final String NAME = "prefs";    private final Context mAppContext;      public MyDumperPlugin(Context context) {      mAppContext = context.getApplicationContext();    }      @Override    public String getName() {      return NAME;    }      @Override    public void dump(DumperContext dumpContext) throws DumpUsageException {      PrintStream writer = dumpContext.getStdout();      List<String> args = dumpContext.getArgsAsList();        String commandName = args.isEmpty() ? "" : args.remove(0);        if (commandName.equals("print")) {        doPrint(writer, args);      } else if (commandName.equals("write")) {        doWrite(args);      } else {        doUsage(writer);      }    }      // 省略部分代码    }</code></pre>    <p>然后把初始化调用替换如下:</p>    <pre>  <code class="language-java">Stetho.initialize(Stetho.newInitializerBuilder(context)      .enableDumpapp(new DumperPluginsProvider() {        @Override        public Iterable<DumperPlugin> get() {          return new Stetho.DefaultDumperPluginsBuilder(context)              .provide(new MyDumperPlugin())              .finish();        }      })      .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(context))      .build());</code></pre>    <h3><strong>4. 启用JavaScript Console</strong></h3>    <p>启用 JavaScript Console 只需在 build.gradle 中添加如下依赖即可:</p>    <pre>  <code class="language-java">compile "com.非死book.stetho:stetho-js-rhino:1.4.1"</code></pre>    <p>启动 app,在 Chrome 开发者工具的 Console 输入下面代码使 app 打印一个Toast:</p>    <pre>  <code class="language-java">importPackage(android.widget);  importPackage(android.os);  var handler = new Handler(Looper.getMainLooper());  handler.post(function() { Toast.makeText(context, "hello", Toast.LENGTH_LONG).show() });</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/435299b8247beb072150fe6b1e14ca7e.png"></p>    <p>importPackage(android.widget) 等于 java 中 import android.widget.*; ,JavaScript 中使用 var 定义变量,这段代码就是创建了一个 handler 并调用 post 方法在 ui 线程弹一个 Toast。</p>    <p>在 Toast.makeText 中的 context 是从哪里来的呢?</p>    <p>context 是在 com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder 的 initJsScope 方法中被绑定到 JSContext 的,下面是 initJsScope 方法的源码:</p>    <pre>  <code class="language-java">private @NonNull ScriptableObject initJsScope(@NonNull Context jsContext) {      // Set the main Rhino goodies      ImporterTopLevel importerTopLevel = new ImporterTopLevel(jsContext);      ScriptableObject scope = jsContext.initStandardObjects(importerTopLevel, false);        ScriptableObject.putProperty(scope, "context", Context.javaToJS(mContext, scope));        try {        importClasses(jsContext, scope);        importPackages(jsContext, scope);        importConsole(scope);        importVariables(scope);        importFunctions(scope);      } catch (StethoJsException e) {        String message = String.format("%s\n%s", e.getMessage(), Log.getStackTraceString(e));        LogUtil.e(e, message);        CLog.writeToConsole(Console.MessageLevel.ERROR, Console.MessageSource.JAVASCRIPT, message);      }        return scope;    }</code></pre>    <p>JsRuntimeReplFactoryBuilder 提供了一些方法可以传递自己的变量,类,包和函数到 JavaScript 环境。</p>    <p>添加变量,类,包和函数到 JavaScript 运行时</p>    <p>修改初始化代码如下:</p>    <pre>  <code class="language-java">Stetho.initialize(Stetho.newInitializerBuilder(context)          .enableWebKitInspector(new ExtInspectorModulesProvider(context))          .build());</code></pre>    <pre>  <code class="language-java">private static class ExtInspectorModulesProvider implements InspectorModulesProvider {        private Context mContext;      private final Handler handler = new Handler(Looper.getMainLooper());        ExtInspectorModulesProvider(Context context) {        mContext = context;      }        @Override      public Iterable<ChromeDevtoolsDomain> get() {        return new Stetho.DefaultInspectorModulesBuilder(mContext)            .runtimeRepl(new JsRuntimeReplFactoryBuilder(mContext)                // 添加变量                .addVariable("test", new AtomicBoolean(true))                // 添加类                .importClass(R.class)                // 添加包                .importPackage(MyApplication.class.getPackage().getName())                 // 添加方法到 javascript: void toast(String)                .addFunction("toast", new BaseFunction() {                  @Override                  public Object call(org.mozilla.javascript.Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {                      // javascript 传递的参数在 varags                    final String message = args[0].toString();                    handler.post(new Runnable() {                      @Override                      public void run() {                        Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();                      }                    });                      // 在 javascript 返回 undefined                    return org.mozilla.javascript.Context.getUndefinedValue();                  }                })                .build())            .finish();      }    }</code></pre>    <p>说明:Java原语类型将被自动装箱,只有对象可以传递到 JavaScript 运行时。</p>    <p>绑定完成后就可以在 JavaScript Console 中使用自己的变量,类,包和函数了。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c548ed23036d3dafc63db315be8c1a28.png"></p>    <p>注意: <strong>Rhino 对包名的检查是严格的</strong> ,必须是 com.** , org.** , net.** 之类比较正规的格式。假如:包名使用 linchaolong.stetho.demo ,在 importClasses(linchaolong.stetho.demo.R) 时, <a href="/misc/goto?guid=4959726434248200699" rel="nofollow,noindex"> ScriptRuntime </a> 会报 EcmaError</p>    <pre>  <code class="language-java">Failed to import class: linchaolong.stetho.demo.R  com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder$StethoJsException: Failed to import class: linchaolong.stetho.demo.R      at com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder.importClasses(JsRuntimeReplFactoryBuilder.java:195)      at com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder.initJsScope(JsRuntimeReplFactoryBuilder.java:173)      at com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder.initJsScope(JsRuntimeReplFactoryBuilder.java:158)      at com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder.access$000(JsRuntimeReplFactoryBuilder.java:45)      at com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder$1.newInstance(JsRuntimeReplFactoryBuilder.java:146)      at com.非死book.stetho.inspector.protocol.module.Runtime$Session.getRepl(Runtime.java:271)      at com.非死book.stetho.inspector.protocol.module.Runtime$Session.evaluate(Runtime.java:260)      at com.非死book.stetho.inspector.protocol.module.Runtime.evaluate(Runtime.java:158)      at java.lang.reflect.Method.invoke(Native Method)      at java.lang.reflect.Method.invoke(Method.java:372)      at com.非死book.stetho.inspector.MethodDispatcher$MethodDispatchHelper.invoke(MethodDispatcher.java:96)      at com.非死book.stetho.inspector.MethodDispatcher.dispatch(MethodDispatcher.java:67)      at com.非死book.stetho.inspector.ChromeDevtoolsServer.handleRemoteRequest(ChromeDevtoolsServer.java:129)      at com.非死book.stetho.inspector.ChromeDevtoolsServer.handleRemoteMessage(ChromeDevtoolsServer.java:111)      at com.非死book.stetho.inspector.ChromeDevtoolsServer.onMessage(ChromeDevtoolsServer.java:87)      at com.非死book.stetho.websocket.WebSocketSession$1.handleTextFrame(WebSocketSession.java:176)      at com.非死book.stetho.websocket.WebSocketSession$1.onCompleteFrame(WebSocketSession.java:136)      at com.非死book.stetho.websocket.ReadHandler.readLoop(ReadHandler.java:44)      at com.非死book.stetho.websocket.WebSocketSession.handle(WebSocketSession.java:45)      at com.非死book.stetho.websocket.WebSocketHandler.doUpgrade(WebSocketHandler.java:117)      at com.非死book.stetho.websocket.WebSocketHandler.handleRequest(WebSocketHandler.java:83)      at com.非死book.stetho.server.http.LightHttpServer.dispatchToHandler(LightHttpServer.java:84)      at com.非死book.stetho.server.http.LightHttpServer.serve(LightHttpServer.java:61)      at com.非死book.stetho.inspector.DevtoolsSocketHandler.onAccepted(DevtoolsSocketHandler.java:52)      at com.非死book.stetho.server.ProtocolDetectingSocketHandler.onSecured(ProtocolDetectingSocketHandler.java:63)      at com.非死book.stetho.server.SecureSocketHandler.onAccepted(SecureSocketHandler.java:33)      at com.非死book.stetho.server.LazySocketHandler.onAccepted(LazySocketHandler.java:36)      at com.非死book.stetho.server.LocalSocketServer$WorkerThread.run(LocalSocketServer.java:167)  Caused by: org.mozilla.javascript.EcmaError: ReferenceError: "linchaolong" is not defined. (chrome#1)      at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3949)      at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3927)      at org.mozilla.javascript.ScriptRuntime.notFoundError(ScriptRuntime.java:4012)      at org.mozilla.javascript.ScriptRuntime.name(ScriptRuntime.java:1849)      at org.mozilla.javascript.Interpreter.interpretLoop(Interpreter.java:1558)      at org.mozilla.javascript.Interpreter.interpret(Interpreter.java:815)      at org.mozilla.javascript.InterpretedFunction.call(InterpretedFunction.java:109)      at org.mozilla.javascript.ContextFactory.doTopCall(ContextFactory.java:393)      at org.mozilla.javascript.ScriptRuntime.doTopCall(ScriptRuntime.java:3280)      at org.mozilla.javascript.InterpretedFunction.exec(InterpretedFunction.java:120)      at org.mozilla.javascript.Context.evaluateString(Context.java:1191)      at com.非死book.stetho.rhino.JsRuntimeReplFactoryBuilder.importClasses(JsRuntimeReplFactoryBuilder.java:193)      ... 27 more</code></pre>    <h2><strong>只在debug模式下使用 Stetho</strong></h2>    <p>修改 dependencies 配置,只在 debug 模式下编译 stetho 和 stetho-js-rhino</p>    <pre>  <code class="language-java">dependencies {    // Debug    debugCompile "com.非死book.stetho:stetho:${stetho}"    compile "com.非死book.stetho:stetho-okhttp3:${stetho}"    debugCompile "com.非死book.stetho:stetho-js-rhino:${stetho}"  }</code></pre>    <p>说明: ${stetho} 是 stetho 的版本号。</p>    <p>在 src/debug/java 目录下新建一个 DebugApplication 继承自 MyApplication ,并把初始化 Stetho 的代码移到 DebugApplication</p>    <pre>  <code class="language-java">public class DebugApplication extends MyApplication{      @Override public void onCreate() {      super.onCreate();      Stetho.initializeWithDefaults(this);    }</code></pre>    <p>在 src/debug 目录下创建一个 AndroidManifest.xml ,并添加 debug 模式下需要的权限和修改 application 节点 android:name 值为 DebugApplication(使用 tools:replace 覆盖 android:name 字段)</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <manifest xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:tools="http://schemas.android.com/tools"      package="com.非死book.stetho.sample">      <uses-permission android:name="android.permission.READ_CALENDAR" />      <application        tools:replace="android:name"        android:name=".DebugApplication" />    </manifest></code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bbaa59c6b0012a93fb8f0a604e68defa.png"></p>    <p>完成上面处理后,Stetho 只在 debug 版本下起作用,不影响 release 版本。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/38d8324b126a</p>    <p> </p>