用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 查看內容

湖人vs马刺赛程:微信小程序性能,行為收集探針實現

湖人vs爵士季后赛 www.nbgeh.club Rolan 2019-6-3 09:52

?小程序的主要開發語言是 JavaScript ,小程序的開發同普通的網頁開發相比有很大的相似性。對于前端開發者而言,從網頁開發遷移到小程序的開發成本并不高,但是二者還是有些許區別的。?網頁開發渲染線程和腳本線程 ...

?湖人vs爵士季后赛的主要開發語言是 JavaScript ,小程序的開發同普通的網頁開發相比有很大的相似性。對于前端開發者而言,從網頁開發遷移到小程序的開發成本并不高,但是二者還是有些許區別的。

?網頁開發渲染線程和腳本線程是互斥的,這也是為什么長時間的腳本運行可能會導致頁面失去響應,而在小程序中,二者是分開的,分別運行在不同的線程中。網頁開發者可以使用到各種瀏覽器暴露出來的 DOM API,進行 DOM 選中和操作。而如上文所述,小程序的邏輯層和渲染層是分開的,邏輯層運行在 JSCore 中,并沒有一個完整瀏覽器對象,因而缺少相關的DOM API和BOM API。這一區別導致了前端開發非常熟悉的一些庫,例如 jQuery、 Zepto 等,在小程序中是無法運行的。同時 JSCore 的環境同 NodeJS 環境也是不盡相同,所以一些 NPM 的包在小程序中也是無法運行的。

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

運行限制

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

不支持使用 eval 執行 JS 代碼 不支持使用 new Function 創建函數

?網頁開發者在開發網頁的時候,只需要使用到瀏覽器,并且搭配上一些輔助工具或者編輯器即可。小程序的開發則有所不同,需要經過申請小程序帳號、安裝小程序開發者工具、配置項目等等過程方可完成。

小程序運行機制

小程序啟動

小程序啟動會有兩種情況,一種是「冷啟動」,一種是「熱啟動」。

熱啟動:假如用戶已經打開過某小程序,然后在一定時間內再次打開該小程序,此時無需重新啟動,只需將后臺態的小程序切換到前臺,這個過程就是熱啟動;

冷啟動:用戶首次打開或小程序被微信主動銷毀后再次打開的情況,此時小程序需要重新加載啟動,即冷啟動。 小程序沒有重啟的概念。

前臺/后臺狀態

當用戶點擊右上角膠囊按鈕關閉小程序,或者按了設備 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方法定義屬性

    由于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探針,在任意標簽上添加
分享至 : QQ空間
收藏
原作者: 鮑康霖 來自: 掘金