Stetho 原始碼分析

啊哈哈啊哈發表於2017-10-09

實現原理

整體流程

自定義命令流程

自定義命令流程
自定義命令流程

stetho-server流程

stetho-server流程分析
stetho-server流程分析

具體執行流程

1、初始化方法

public static void initializeWithDefaults(final Context context) {
    initialize(new Initializer(context) {
        @Override
        protected Iterable<DumperPlugin> getDumperPlugins() {
            return new DefaultDumperPluginsBuilder(context).finish();
        }
        @Override
        protected Iterable<ChromeDevtoolsDomain> getInspectorModules() {
            return new DefaultInspectorModulesBuilder(context).finish();
        }
    });
}

public static void initialize(final Initializer initializer) {
    ...
    initializer.start();
}複製程式碼

2、在 initializer.start()之後呼叫了 serverManager.start(),在這裡new Thred() 並開啟Socket建立連線

while (!Thread.interrupted()) {
    try {
        LocalSocket socket = mServerSocket.accept();
        // Start worker thread
        Thread t = new WorkerThread(socket, mSocketHandler);
        t.setName(
                WORKER_THREAD_NAME_PREFIX +
                        "-" + mFriendlyName +
                        "-" + mThreadId.incrementAndGet());
        t.setDaemon(true);
        t.start();
    } catch (SocketException se) {
    ...
    } 
}複製程式碼

3、在WorkerThread中執行 mSocketHandler.onAccepted(mSocket);這裡的mSocketHandler為 LazySocketHandler 類,由Stetho類傳遞過來,在LazySocketHandler的onAccepted方法中,真正處理邏輯的 SocketHandler 是由RealSocketHandlerFactory建立的ProtocolDetectingSocketHandler。

private class RealSocketHandlerFactory implements SocketHandlerFactory {
    @Override
    public SocketHandler create() {
        ProtocolDetectingSocketHandler socketHandler =
                new ProtocolDetectingSocketHandler(mContext);
        Iterable<DumperPlugin> dumperPlugins = getDumperPlugins();
        if (dumperPlugins != null) {
            Dumper dumper = new Dumper(dumperPlugins);
            socketHandler.addHandler(
                    new ProtocolDetectingSocketHandler.ExactMagicMatcher(
                            DumpappSocketLikeHandler.PROTOCOL_MAGIC),
                    new DumpappSocketLikeHandler(dumper));
            // Support the old HTTP-based protocol since it's relatively straight forward to do.
            DumpappHttpSocketLikeHandler legacyHandler = new DumpappHttpSocketLikeHandler(dumper);
            socketHandler.addHandler(
                    new ProtocolDetectingSocketHandler.ExactMagicMatcher(
                            "GET /dumpapp".getBytes()),
                    legacyHandler);
            socketHandler.addHandler(
                    new ProtocolDetectingSocketHandler.ExactMagicMatcher(
                            "POST /dumpapp".getBytes()),
                    legacyHandler);
        }
        Iterable<ChromeDevtoolsDomain> inspectorModules = getInspectorModules();
        if (inspectorModules != null) {
            socketHandler.addHandler(
                    new ProtocolDetectingSocketHandler.AlwaysMatchMatcher(),
                    new DevtoolsSocketHandler(mContext, inspectorModules));
        }
        return socketHandler;
    }
}複製程式碼

主要建立了兩個處理器,用於處理Chrome Dev Tools相關的 DevtoolsSocketHandler,以及處理自定義命令的DumpappSocketLikeHandler。

4、在 ProtocolDetectingSocketHandler 的 onSecured 方法中,通過MagicMatcher區分當前請求由哪個處理器處理

for (int i = 0, N = mHandlers.size(); i < N; i++) {
    HandlerInfo handlerInfo = mHandlers.get(i);
    leakyIn.mark(SENSING_BUFFER_SIZE);
    boolean matches = handlerInfo.magicMatcher.matches(leakyIn);
    leakyIn.reset();
    if (matches) {
        SocketLike socketLike = new SocketLike(socket, leakyIn);
        handlerInfo.handler.onAccepted(socketLike);
        return;
    }
}複製程式碼

mHandlers 為List 型別,在RealSocketHandlerFactory執行了addHandler(),新增了Chrome Dev Tools和自定義命令的處理器

5、在分發處理器之後,處理器接收到資訊執行onAccepted()方法,DevtoolsSocketHandler處理器呼叫LightHttpServer.serve()再次進行請求的分發處理(這裡先分析 DevtoolsSocketHandler)

while ((request = readRequestMessage(scratchRequest, reader)) != null) {
    final LightHttpResponse response = scratchResponse;
    response.reset();
    boolean keepGoing = dispatchToHandler(anotherSocketLike, request, response);
    if (!keepGoing) {
        break;
    }
    writeFullResponse(response, writer, output);
}複製程式碼

dispatchToHandler(anotherSocketLike, request, response)

private boolean dispatchToHandler(SocketLike socketLike, LightHttpRequest request, LightHttpResponse response)
        throws IOException {
    HttpHandler handler = mHandlerRegistry.lookup(request.uri.getPath());
    if (handler == null) {
    ...
    } else {
        try {
            return handler.handleRequest(socketLike, request, response);
        } catch (RuntimeException e) {
        ...
        }
    }
}複製程式碼

mHandlerRegistry.lookup(request.uri.getPath()) 通過path獲取不同的處理器

public synchronized HttpHandler lookup(String path) {
    for (int i = 0, N = mPathMatchers.size(); i < N; i++) {
        if (mPathMatchers.get(i).match(path)) {
            return mHttpHandlers.get(i);
        }
    }
    return null;
}複製程式碼

這裡mPathMatchers,mHttpHandlers維護了一個path和處理器的對應關係,

  • ChromeDiscoveryHandler對應的path為
    • /json
    • /json/version
    • /json/activate/ + PAGE_ID
  • WebSocketHandler對應path為:/inspector
  • DumpappLegacyHttpHandler對應的path為:/dumpapp

6、WebSocketHandler.handleRequest() 處理請求,接著會呼叫WebSocketSession.handle()方法,在這裡mReadHandler.readLoop(mReadCallback) 迴圈讀取WebSocket中的資料Frame,Frame具體結構可參照 RFC6455 文件。

public void readLoop(ReadCallback readCallback) throws IOException {
    Frame frame = new Frame();
    do {
        frame.readFrom(mBufferedInput);
        mCurrentPayload.write(frame.payloadData, 0, (int) frame.payloadLen);
        if (frame.fin) {
            byte[] completePayload = mCurrentPayload.toByteArray();
            readCallback.onCompleteFrame(frame.opcode, completePayload, completePayload.length);
            mCurrentPayload.reset();
        }
    } while (frame.opcode != Frame.OPCODE_CONNECTION_CLOSE);
}複製程式碼

在 readCallback.onCompleteFrame(frame.opcode, completePayload, completePayload.length);方法中再次進行分發處理

public void onCompleteFrame(byte opcode, byte[] payload, int payloadLen) {
    switch (opcode) {
        case Frame.OPCODE_CONNECTION_CLOSE:
            handleClose(payload, payloadLen);
            break;
        case Frame.OPCODE_CONNECTION_PING:
            handlePing(payload, payloadLen);
            break;
        case Frame.OPCODE_CONNECTION_PONG:
            handlePong(payload, payloadLen);
            break;
        case Frame.OPCODE_TEXT_FRAME:
            handleTextFrame(payload, payloadLen);
            break;
        case Frame.OPCODE_BINARY_FRAME:
            handleBinaryFrame(payload, payloadLen);
            break;
        default:
            signalError(new IOException("Unsupported frame opcode=" + opcode));
            break;
    }
}複製程式碼
private void handleTextFrame(byte[] payload, int payloadLen) {
    mEndpoint.onMessage(WebSocketSession.this, new String(payload, 0, payloadLen));
}複製程式碼

這裡 mEndpoint 為 ChromeDevtoolsServer,在ChromeDevtoolsServer中的onMessage()方法中,handleRemoteMessage(peer, message) 分為處理響應和請求:handleRemoteRequest()和handleRemoteResponse()

7、在handleRemoteRequest()中呼叫MethodDispatcher.dispatch(peer,request.method,request.params)進行具體的方法分發

public JSONObject dispatch(JsonRpcPeer peer, String methodName, @Nullable JSONObject params) throws JsonRpcException {
    MethodDispatchHelper dispatchHelper = findMethodDispatcher(methodName);
    ...
    try {
        return dispatchHelper.invoke(peer, params);
    } catch (InvocationTargetException e) {
    ...
    } 
}複製程式碼

在findMethodDispatcher(methodName)方法中首先buildDispatchTable()使用反射的方式維護了method和MethodDispatchHelper的對映關係,然後通過methodName獲取MethodDispatchHelper。

private static Map<String, MethodDispatchHelper> buildDispatchTable(ObjectMapper objectMapper, Iterable<ChromeDevtoolsDomain> domainHandlers) {
    Util.throwIfNull(objectMapper);
    HashMap<String, MethodDispatchHelper> methods = new HashMap<String, MethodDispatchHelper>();
    for (ChromeDevtoolsDomain domainHandler : Util.throwIfNull(domainHandlers)) {
        Class<?> handlerClass = domainHandler.getClass();
        String domainName = handlerClass.getSimpleName();
        for (Method method : handlerClass.getDeclaredMethods()) {
            if (isDevtoolsMethod(method)) {
                MethodDispatchHelper dispatchHelper = new MethodDispatchHelper(
                        objectMapper,
                        domainHandler,
                        method);
                methods.put(domainName + "." + method.getName(), dispatchHelper);
            }
        }
    }
    return Collections.unmodifiableMap(methods);
}複製程式碼

ChromeDevtoolsDomain的實現類包含:

  • DOM
  • DOMStorage
  • Worker
  • CSS
  • Debugger
  • Profiler
  • HeapProfiler
  • Console
  • Page
  • Database
  • Inspector
  • Runtime
  • Network

對應Chrome的除錯頁面,類的具體內容可參照Chrome DevTools Protocol協議,類封裝了具體的方法和請求體、返回體的引數,在這裡實現了對資料庫、SP等的具體操作方法,此處不做過多分析。

8、在 MethodDispatchHelper.invoke()方法中使用反射的方式執行了 ChromeDevtoolsDomain 中的方法,獲取結果後在handleRemoteRequest()封裝資料返回。

public JSONObject invoke(JsonRpcPeer peer, @Nullable JSONObject params)
        throws InvocationTargetException, IllegalAccessException, JSONException, JsonRpcException {
    Object internalResult = mMethod.invoke(mInstance, peer, params);
    if (internalResult == null || internalResult instanceof EmptyResult) {
        return new JSONObject();
    } else {
        JsonRpcResult convertableResult = (JsonRpcResult) internalResult;
        return mObjectMapper.convertValue(convertableResult, JSONObject.class);
    }
}複製程式碼

9、自定義命令的實現:

在第4步接收到請求分發處理器之後,DumpappSocketLikeHandler執行onAccepted()方法,執行dump()方法

static void dump(Dumper dumper, Framer framer, String[] args) throws IOException {
    try {
        int exitCode = dumper.dump(
                framer.getStdin(),
                framer.getStdout(),
                framer.getStderr(),
                args);
        framer.writeExitCode(exitCode);
    } catch (DumpappOutputBrokenException e) {
        ...
    }
}複製程式碼
public int dump(InputStream input, PrintStream out, PrintStream err, String[] args) {
    try {
        return doDump(input, out, err, args);
    } catch (ParseException e) {
        err.println(e.getMessage());
        dumpUsage(err);
        return 1;
    }複製程式碼
private int doDump(InputStream input, PrintStream out, PrintStream err, String[] args)
        throws ParseException, DumpException {
    CommandLine parsedArgs = mParser.parse(mGlobalOptions.options, args, true);
    if (parsedArgs.hasOption(mGlobalOptions.optionHelp.getOpt())) {
        dumpUsage(out);
        return 0;
    } else if (parsedArgs.hasOption(mGlobalOptions.optionListPlugins.getOpt())) {
        dumpAvailablePlugins(out);
        return 0;
    } else if (!parsedArgs.getArgList().isEmpty()) {
        dumpPluginOutput(input, out, err, parsedArgs);
        return 0;
    } else {
        // Didn't understand the options, spit out help but use a non-success exit code.
        dumpUsage(err);
        return 1;
    }
}複製程式碼

在使用 CommandLine(Apache Commons CLI庫)解析引數之後,根據引數返回使用說明、幫助或者在dumpPluginOutput()執行具體的命令

private void dumpPluginOutput(InputStream input, PrintStream out, PrintStream err, CommandLine parsedArgs) throws DumpException {
    List<String> args = new ArrayList(parsedArgs.getArgList());
    if (args.size() < 1) {
        throw new DumpException("Expected plugin argument");
    }
    String pluginName = args.remove(0);
    DumperPlugin plugin = mDumperPlugins.get(pluginName);
    if (plugin == null) {
        throw new DumpException("No plugin named '" + pluginName + "'");
    }
    DumperContext dumperContext = new DumperContext(input, out, err, mParser, args);
    plugin.dump(dumperContext);
}複製程式碼

mDumperPlugins 在第3步中由Dumper dumper = new Dumper(dumperPlugins)傳遞,dumperPlugins包含了所有自定義命令 name和DumperPlugin的對映關係,在獲取到DumperPlugin執行dump()方法,dump()方法在自定義DumperPlugin時具體實現。

命令列互動建立連線的實現在Stetho專案的/scripts/dumpapp中。

相關文章