2019-03-29 21 WebView性能优化

WebView性能优化

  • WebView耗时优化
    • WebView预加载, WebView池
    • IO异步化(资源、网络请求与WebView初始化加载分离)
  • HTML页面耗时优化
    • 静态直出, CDN直接输出可展示交互HTML(HTML不包含展示内容; 动态构建DOM开销)
    • 动态直出(NodeJs) + 动态缓存(Sonic动态缓存)
  • 网络请求优化
    • 离线预推
      • diff增量推送
    • DNS预解析
    • 渲染采用chunk编码
  • 性能监控
    • 监控指标
      • 白屏时间

Sonic源码流程解析

核心思想利用webview初始化时间进行数据请求

  • 无缓存模式启动流程
  ...
  // 1. 创建初始化Session
  sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());

  ...
  // 2. 设置拦截资源加载
  webView.setWebViewClient(new WebViewClient() {
            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
                if (sonicSession != null) {
                    // 拦截资源加载, 如果有对应的桥接流, 则返回让webview进行加载
                    return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
                }
                return null;
            }
        });

  ...

  ...
  // 3. webview已初始化完成
  sonicSessionClient.bindWebView(webView);
  sonicSessionClient.clientReady();
  • 初始化Session
  // SonicEngine.java
  public synchronized SonicSession createSession(@NonNull String url, @NonNull SonicSessionConfig sessionConfig) {
        ...
        sonicSession = internalCreateSession(sessionId, url, sessionConfig);
        ...
    }

    // 创建Session实例, 并把sonic启动
    private SonicSession internalCreateSession(String sessionId, String url, SonicSessionConfig sessionConfig) {
          ...
          sonicSession = new QuickSonicSession(sessionId, url, sessionConfig);
          ...
          // SonicSession#start()
          sonicSession.start();
      }
  • SonicSession#start(), 启动加载流程
  // SonicSession
  public void start() {
    ...
    SonicEngine.getInstance().getRuntime().postTaskToSessionThread(new Runnable() {
        @Override
        public void run() {
            runSonicFlow(true);
        }
    });
    ...
  }

  // 首次加载, firstRequest为true.
  private void runSonicFlow(boolean firstRequest) {
        String cacheHtml = null;
        // 获取会话数据(eTag)等
        SonicDataHelper.SessionData sessionData;
        sessionData = getSessionData(firstRequest);

        if (firstRequest) {
            // 获取本地缓存
            cacheHtml = SonicCacheInterceptor.getSonicCacheData(this);
            ...
            // 1. 参考handleFlow_LoadLocalCache实现
            handleFlow_LoadLocalCache(cacheHtml); // local cache if exist before connection
        }

        boolean hasHtmlCache = !TextUtils.isEmpty(cacheHtml) || !firstRequest;

        final SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
        ...
        // 和服务器进行连接, 获取会话数据
        // 参考handleFlow_Connection实现
        handleFlow_Connection(hasHtmlCache, sessionData);
        ...
    }
  • SonicSession#handleFlow_LoadLocalCache()
  // handleFlow_LoadLocalCache实现
  protected void handleFlow_LoadLocalCache(String cacheHtml) {
    // 发送CLIENT_CORE_MSG_PRE_LOAD消息
      Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_PRE_LOAD);
      if (!TextUtils.isEmpty(cacheHtml)) {
          msg.arg1 = PRE_LOAD_WITH_CACHE;
          msg.obj = cacheHtml;
      } else {
          ...
          msg.arg1 = PRE_LOAD_NO_CACHE;
      }
      mainHandler.sendMessage(msg);
      ...
  }

  // 找到CLIENT_CORE_MSG_PRE_LOAD消息对应的处理逻辑
  // 有缓存的情况下调用loadDataWithBaseUrlAndHeader, 让webview直接加载
  // 无缓存情况下调用#loadUrl
  private void handleClientCoreMessage_PreLoad(Message msg) {
        switch (msg.arg1) {
            case PRE_LOAD_NO_CACHE: {
              ...
              sessionClient.loadUrl(srcUrl, null);
              ...
            }
            break;
            case PRE_LOAD_WITH_CACHE: {
                ...
                String html = (String) msg.obj;
                sessionClient.loadDataWithBaseUrlAndHeader(srcUrl, html, "text/html",
                        SonicUtils.DEFAULT_CHARSET, srcUrl, getCacheHeaders());
                ...
            }
            break;
        }
  }
  • 继续看SonicSession#handleFlow_Connection()实现流程
protected void handleFlow_Connection(boolean hasCache, SonicDataHelper.SessionData sessionData) {
      ...

      server = new SonicServer(this, createConnectionIntent(sessionData));

      // 1. 连接服务器
      int responseCode = server.connect();
      ...

      // 后端通过http response header返回的sonic-link, 即需要预加载的资源, 参考预加载子资源逻辑
      String preloadLink = server.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_LINK);
      ...
      handleFlow_PreloadSubResource();
      ...

      ...
      // 处理304逻辑
      handleFlow_NotModified();
      ...

      // When cacheHtml is empty, run First-Load flow
      if (!hasCache) {
          // 无缓存
          handleFlow_FirstLoad();
          return;
      }

      ...
  }

  • QuickSonicSession#handleFlow_FirstLoad()
  protected void handleFlow_FirstLoad() {
      // 桥接流
      pendingWebResourceStream = server.getResponseStream(wasInterceptInvoked);
      ...

      String htmlString = server.getResponseData(false);
      boolean hasCompletionData = !TextUtils.isEmpty(htmlString);
      ...

      mainHandler.removeMessages(CLIENT_CORE_MSG_PRE_LOAD);
      // 发送消息, 由QuickSonicSession#handleClientCoreMessage_FirstLoad()处理逻辑
      Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_FIRST_LOAD);
      msg.obj = htmlString;
      msg.arg1 = hasCompletionData ? FIRST_LOAD_WITH_DATA : FIRST_LOAD_NO_DATA;
      mainHandler.sendMessage(msg);
      ...
      // 发送保存html缓存消息, FILE_THREAD_SAVE_CACHE_ON_SESSION_FINISHED, 逻辑查看SonicSession#doSaveSonicCache()
      postTaskToSaveSonicCache(htmlString);
      ...
  }
  • QuickSonicSession#handleClientCoreMessage_FirstLoad()
// 服务返回数据
private void handleClientCoreMessage_FirstLoad(Message msg) {
       switch (msg.arg1) {
           ...
           case FIRST_LOAD_WITH_DATA: {
               ...
               sessionClient.loadDataWithBaseUrlAndHeader(srcUrl, (String) msg.obj, "text/html",
                       getCharsetFromHeaders(), srcUrl, getHeaders());
               ...
           }
           break;
       }
   }
  • SonicSession#doSaveSonicCache(),保存html缓存
    protected void doSaveSonicCache(SonicServer sonicServer, String htmlString) {
         ...
         // 保存html缓存
         SonicUtils.saveSonicData(id, eTag, templateTag, newHtmlSha1, htmlSize, headers);
         ...
    
     }
    
  • 拦截请求WebViewClient#shouldInterceptRequest()
  // 请求资源
  public Object requestResource(String url) {
      ...
      session.onClientRequestResource(url);
      ...
  }
  • SonicSession#onClientRequestResource()
  // 如果有桥接流或者子资源缓存, 返回WebResourceResponse
  public final Object onClientRequestResource(String url) {
     ...
     // 主域请求逻辑#onRequestResource(), 子域请求逻辑resourceDownloaderEngine#onRequestSubResource()
     Object object = isMatchCurrentUrl(url)
             ? onRequestResource(url)
             : (resourceDownloaderEngine != null ? resourceDownloaderEngine.onRequestSubResource(url, this) : null);
    ...
 }
  • SnoicSession#onRequestResource()
protected Object onRequestResource(String url) {
      ...
      // 首次启动时, 如果已和服务器链接, 则使用服务器的桥接流
      if (null != pendingWebResourceStream) {
            Object webResourceResponse;
            if (!isDestroyedOrWaitingForDestroy()) {
                String mime = SonicUtils.getMime(srcUrl);
                webResourceResponse = SonicEngine.getInstance().getRuntime().createWebResourceResponse(mime,
                        getCharsetFromHeaders(), pendingWebResourceStream, getHeaders());
            } else {
                webResourceResponse = null;
                ...

            }
            ...
            return webResourceResponse;
        }
      ...
  }
  • SonicDownloadEngine#onRequestSubResource()
  

2.15.4 参考文档