微信小程序性能,行爲收集探針實現

小程序與普通網頁開發的區別

​小程序的主要開發語言是 JavaScript ,小程序的開發同普通的網頁開發相比有很大的類似性。對於前端開發者而言,從網頁開發遷移到小程序的開發成本並不高,可是兩者仍是有些許區別的。前端

​網頁開發渲染線程和腳本線程是互斥的,這也是爲何長時間的腳本運行可能會致使頁面失去響應,而在小程序中,兩者是分開的,分別運行在不一樣的線程中。網頁開發者可使用到各類瀏覽器暴露出來的 DOM API,進行 DOM 選中和操做。而如上文所述,小程序的邏輯層和渲染層是分開的,邏輯層運行在 JSCore 中,並無一個完整瀏覽器對象,於是缺乏相關的DOM API和BOM API。這一區別致使了前端開發很是熟悉的一些庫,例如 jQuery、 Zepto 等,在小程序中是沒法運行的。同時 JSCore 的環境同 NodeJS 環境也是不盡相同,因此一些 NPM 的包在小程序中也是沒法運行的。vue

​網頁開發者須要面對的環境是各式各樣的瀏覽器,PC 端須要面對 IE、Chrome、QQ瀏覽器等,在移動端須要面對Safari、Chrome以及 iOS、Android 系統中的各式 WebView 。而小程序開發過程當中須要面對的是兩大操做系統 iOS 和 Android 的微信客戶端,以及用於輔助開發的小程序開發者工具,小程序中三大運行環境也是有所區別的web

運行限制

基於安全考慮,小程序中不支持動態執行 JS 代碼,即:chrome

不支持使用 eval 執行 JS 代碼 不支持使用 new Function 建立函數小程序

​網頁開發者在開發網頁的時候,只須要使用到瀏覽器,而且搭配上一些輔助工具或者編輯器便可。小程序的開發則有所不一樣,須要通過申請小程序賬號、安裝小程序開發者工具、配置項目等等過程方可完成。api

小程序運行機制

小程序啓動

小程序啓動會有兩種狀況,一種是「冷啓動」,一種是「熱啓動」。瀏覽器

熱啓動:假如用戶已經打開過某小程序,而後在必定時間內再次打開該小程序,此時無需從新啓動,只需將後臺態的小程序切換到前臺,這個過程就是熱啓動;緩存

冷啓動:用戶首次打開或小程序被微信主動銷燬後再次打開的狀況,此時小程序須要從新加載啓動,即冷啓動。 小程序沒有重啓的概念。安全

前臺/後臺狀態

當用戶點擊右上角膠囊按鈕關閉小程序,或者按了設備 Home 鍵離開微信時,小程序並無直接銷燬,而是進入了後臺狀態;微信

當用戶再次進入微信或再次打開小程序,小程序又會從後臺進入前臺。

小程序銷燬

須要注意的是:只有當小程序進入後臺必定時間,或者系統資源佔用太高,纔會被真正的銷燬。

當小程序進入後臺,客戶端會維持一段時間的運行狀態,超過必定時間後(目前是5分鐘)小程序會被微信主動銷燬。 當小程序佔用系統資源太高,可能會被系統銷燬或被微信客戶端主動回收。 在 iOS 上,當微信客戶端在必定時間間隔內(目前是 5 秒)連續收到兩次及以上系統內存告警時,會主動進行小程序的銷燬,並提示用戶 「該小程序可能致使微信響應變慢被終止」。 建議小程序在必要時使用 wx.onMemoryWarning 監聽內存告警事件,進行必要的內存清理。

小程序更新機制

未啓動時更新

開發者在管理後臺發佈新版本的小程序以後,若是某個用戶本地有小程序的歷史版本,此時打開的可能仍是舊版本。微信客戶端會有若干個時機去檢查本地緩存的小程序有沒有更新版本,若是有則會靜默更新到新版本。總的來講,開發者在後臺發佈新版本以後,沒法馬上影響到全部現網用戶,但最差狀況下,也在發佈以後 24 小時以內下發新版本信息到用戶。用戶下次打開時會先更新最新版本再打開。

啓動時更新

小程序每次冷啓動時,都會檢查是否有更新版本,若是發現有新版本,將會異步下載新版本的代碼包,並同時用客戶端本地的包進行啓動,即新版本的小程序須要等下一次冷啓動纔會應用上。

若是須要立刻應用最新版本,可使用 wx.getUpdateManager API 進行處理。

小程序探針開發難點與重點

  • 沒法直接攔截/監聽請求

    微信請求統一經過微信API完成 ,請求模塊已被微信方封裝,且小程序的運行環境不是瀏覽器對象,不像web應用那樣重寫封裝很自如。

  • 三種運行環境的監控兼容性保證

    • Android 上,js運行環境是 X5 內核
    • iOS 上,js 運行環境是 JavaScriptCore
    • 開發工具上, j s運行環境是 nwjs(chrome內核)
  • 用戶行爲沒法直接監聽

    小程序邏輯層運行時沒法獲取DOM和BOM,沒法像傳統網頁開發同樣使用DOM事件API,沒法全局監聽事件.

  • sdk需輕量

    小程序包大小有限制,單包最大爲2M,分包狀況下,不能超過8M,因此sdk需輕量

  • 數據收集量大,儘可能減小性能損耗

    須要設計緩存池,制定上報策略

  • 不影響業務(基本需求)

探針緩存池與上報策略

探針收集到的數據主要分爲兩種,一種是基本數據,還有一種是事件特性數據.特性數據在下面關鍵事件中將會提到

基礎數據

基本數據是每條上報日誌都包含的數據。其中一部分,在初始化探針後就獲取到,而且不會改變.這部分數據,業務相關的由用戶配置,其他數據由探針內部生成或者調用wx.getSystemInfoSync API獲取

另外一部分,隨着用戶行爲,好比頁面切換、登錄,或者環境變化,如網絡變化時,將會改變.

network數據經過 wx.getNetworkType 與 wx.onNetworkStatusChange獲取 title部分在下面的關鍵事件有講到

事件特性數據

![](user-gold-cdn.xitu.io/2019/5/30/1…

上報策略

探針內部將會緩存對應日誌,防止小程序Storage清空時,遺失數據.

數據上報只要上報,就將緩存的日誌清空,防止上報失敗致使緩存的日誌越積越多

探針關鍵事件捕獲

關鍵事件類型

改寫App config

對於App類主要改寫config上的"onShow", "onHide", "onError", 'onLaunch'這幾個生命週期

緩存鉤子函數
給config上的方法掛上鉤子,對config中未配置對應生命週期,加上默認生命週期回調
對config包含了"onShow", "onHide", "onError",'onLaunch'生命週期函數,執行完原方法後再調用鉤子函數

  • 啓動事件(start)

    小程序啓動,獲取小程序啓動場景值.重寫App的config,經過onLaunch觸發. 獲取小程序啓動場景值scene,頁面路徑path,頁面search,經過頁面路徑與 __wxConfig對象獲取頁面title.

  • 退出到後臺(pause)

    小程序切換到後臺,重寫App的config,經過onHide觸發

  • 切回前臺(resume)

    小程序從後臺喚醒,獲取切回小程序場景值scene.重寫App的config,經過onShow觸發(第一次觸發onShow除外)

  • 異常捕獲

    因爲小程序的全局監聽方法wx.onError只有2.1.2及以上才支持,爲了兼容,須要重寫App的config,經過onError觸發.

改寫Page config

這一部分與改寫App config大同小異,主要看事件的獲取

  • 頁面停留(page_stay)

    onHide與onUnload時觸發,獲取用戶在當前頁面停留的時間.對於分享轉發頁面致使onHide觸發的場景,不進行頁面停留上報.

  • 頁面切換(page)

    每次切換頁面(onShow)時觸發,獲取當前頁面路徑,參數,title

  • 頁面初次渲染時長

    頁面首次打開或銷燬後首次打開,頁面渲染所花費的時間,重寫Page的config,經過onReady觸發.

  • 頁面分享(share)

    用戶分享轉發頁面時觸發,經過重寫Page的config,onShareAppMessage觸發.因爲頁面分享會觸發當前頁面的onShow,onHide生命週期,爲了數據準確,經過設置變量isPause來甄別.

用戶行爲捕獲

因爲用戶行爲老是與事件相關,對於事件,小程序沒法直接監聽dom事件,這裏採起的方案是對App、Page、Component、Behavior的config進行改寫,判斷,判斷config上的屬性是否爲函數,而且函數的形參是否爲事件源,若是是事件源,說明該函數與用戶行爲現關聯

對於Component、Behavior只需對其config.method上的方法進行hook

經過形參是否具備currentTarget屬性判斷當前是否爲事件函數

對於不存在自定義事件屬性的點擊事件,認定爲點擊事件,對於存在的,認定爲自定義事件

  • 點擊事件(click)

    因爲小程序的邏輯層與渲染層是分開的,邏輯層運行在JSCore中,沒有完整的瀏覽器對象,缺乏dom與bom相關api,沒法在body上設置全局的點擊事件監聽方法.

    爲了實現事件的監聽,探針經過改寫Page 、Component和Behavior的config,對config上的全部屬性進行區分,判斷當前屬性是否爲函數,而且該函數觸發時,形參上是否具備currentTargey屬性來區分形參是否爲事件對象,以此監聽頁面事件.對於tap與longpress事件,探針認定爲點擊事件.

    類型 觸發條件
    tap 手指觸摸後立刻離開
    longpress 手指觸摸後,超過350ms再離開,若是指定了事件回調函數並觸發了這個事件,tap事件將不被觸發

  • 自定義事件(log)

    直接在事件函數內調用探針暴露的自定義事件上報方法會致使業務代碼與探針耦合度太高.

    探針結合事件的監聽經過在綁定了事件的小程序標籤上添加自定義屬性,來實現自定義事件的上報.

    因爲事件觸發時的事件源經微信內部封裝過,自定義屬性的獲取目前只支持數據屬性data-xxx的形式獲取,因此在非手動調用時,能夠在觸發點擊事件的小程序標籤上增長data-event 與 data-log來添加低耦合的自定義事件代碼.

改寫wx對象實現api事件捕獲

  • api事件(api)

    覆寫wx對象,對wx.request方法的config進行重寫,獲取api(數據接口地址)、api_method(數據接口請求方式)、api_status(數據接口響應狀態碼)、api_response_time(數據接口響應時間(ms))、api_response_content_length (數據接口響應內容長度(byte))

    小程序的api基本都掛載在全局對象wx上,直接修改wx上面的屬性,將會報錯,直接賦值失敗(小程序內部對此作出了限制)

    thirdScriptError 
     sdk uncaught third Error 
     Cannot set property request of #<Object> which has only a getter 
     TypeError: Cannot set property request of #<Object> which has only a getter
    複製代碼

    替代方案

    使用Object.getOwnPropertyDescriptors獲取到wx對象的屬性描述符,將微信對象從新賦值爲空對象,循環屬性描述符,判斷當前描述符的鍵是否爲request,並進行改造request屬性描述符,其餘狀況使用Object.defineProperty方法定義屬性

    wx對象屬性描述符

    因爲for in循環獲取不到Symbol類型的鍵,爲了兼容wx對象未來引入Symbol做爲wx對象鍵的情景,使用Object.getOwnPropertySymbols方法獲取到屬性描述符中的Symbol,再從新定義屬性

    這一塊代碼太多了,很差截圖,直接上代碼吧

    // 重寫wx.request
      rewriteWxRequest() {
        const that = this;
        
        // return 
        // 重寫wx對象start
        const descriptorObj = Object.getOwnPropertyDescriptors(wx);
        let oldWx = this.oldWx = wx;
        wx = {};
        for (let i in descriptorObj) {
          if (i === 'request') {
            const desObj = descriptorObj[i];
            let oldGet = desObj.get;
            desObj.get = function(...args){
              let oldRequest = oldGet.apply(this, args);
              return function(params){
                const {
                  url,
                  method = 'GET',
                  success = function(){},
          
                } = params;
                // 檢查API請求是否在忽略的url中
                const ignoreUrls = that.conf.api_ignore_urls;
                if (url && isIgnoreApi(url, ignoreUrls)) {
                  return oldRequest.call(this, params);
                }
                // 處理自定義 api url trim func
                let apiTrimUrl = null;
                if (that.conf.api_property_cb) {
                  try {
                    apiTrimUrl = that.conf.api_property_cb(url) || null;
                  } catch (e) {
                    apiTrimUrl = null;
                  }
                }
    
                const timeStamp = Date.now();
                const apiData = {
                  api: apiTrimUrl || cutAPIUrl(url),
                  api_method: method.toUpperCase(),
                  api_status: undefined,
                  api_response_time: 0,
                  api_response_content_length: 0,
                }
                return oldRequest.call(this, {
                  ...params,
                  success (res) { // 成功回調
                    try {
                      const {
                        data,
                        statusCode
                      } = res
                      apiData.api_status = statusCode;
                      apiData.api_response_time = Date.now() - timeStamp;
                      if (data) {
                        let AB = {};
                        if(typeof ArrayBuffer !== undefined) {
                          AB = ArrayBuffer;
                        }
                        if (data instanceof AB && data.byteLength !== undefined) {
                          apiData.api_response_content_length = data.byteLength;
                        } else {
                          if (typeof data === 'string') {
                            apiData.api_response_content_length = data.length || 0;
                          } else {
                            apiData.api_response_content_length = JSON.stringify(data).length || 0;
                          }
                        }
                      } else {
                        apiData.api_response_content_length = 0;
                      }
                      that.reportApi(apiData)
                    } catch (e) {
                      that.consoleErr(e);
                    }
                    success.call(this, res);
                  },
                })
              }
            }
            Object.defineProperty(wx, i, desObj)
          } else {
            Object.defineProperty(wx, i, descriptorObj[i])
          }
        }
        // 對微信未來引入Symbol的狀況進行兼容,防止丟失以Symbol爲鍵的狀況
        if (Object.getOwnPropertySymbols && typeof Object.getOwnPropertySymbols === 'function') {
          Object.getOwnPropertySymbols(descriptorObj).forEach(val => {
            Object.defineProperty(wx, val, descriptorObj[val])
          })
        }
        // 重寫wx對象end
      }
    複製代碼

待優化

  • 錯誤異常沒法定位到源碼
  • 目前只支持原生框架和mpvue框架,而且不能適用微信第三方插件
  • 自定義事件沒法像web探針,在任意標籤上添加
相關文章
相關標籤/搜索