貨拉拉出行iOS使用者端啟動優化實踐

語言: CN / TW / HK

一. 引言

我們通過埋點發現部分使用者啟動耗時可以達到10秒左右,有的甚至可以達到20秒左右,主要集中在中低端機型(iPhone6iPhone7iPhone8系列);試想一個場景:你和女朋友約會馬上要遲到了,於是決定打車,打開出行App,結果啟動了十幾秒,當時會是什麼心態。那麼如何提升App的啟動速度呢?接下來我將用我的實踐過程來和大家探討。

二.優化效果

針對啟動耗時埋點的統計資料,進行分析總結,得出是有必要針對啟動耗時做一定優化,尤其在低版本機型,低記憶體情況下,提升這部分使用App使用者的體驗效果,增加使用者的留存率。

這是我們做了幾期優化後的成果

優化前:

優化後:

從最新的1.4.0版本啟動時長的統計資料看:

  • 小於3s的佔比達到了99.77%
  • 3s-4s的佔比為0.13%
  • 4s-5s的佔比為0.04%
  • 大於5s的佔比為0.06%

從優化效果看,相對還算比較不錯,因此這裡對啟動相關的優化做一下總結,主要從如下幾方面來分析:

  • 專案中哪些方法導致啟動耗時長
  • 為什麼這些方法會導致啟動耗時長
  • 怎麼優化這些方法減少啟動耗時

而至於怎麼查詢導致啟動耗時長的方法和後續的治理、監控機制、告警機制等,可以檢視之前其他專案組釋出的這篇文章:貨拉拉使用者端體驗優化--啟動優化篇

三. 詳細優化過程

優化的細節比較多,總體可以分為三期:

  • 第一期:主要對耗時長的函式做治理以及低版本機型做了特殊處理;
  • 第二期:深入業務,對整個啟動過程中的業務流程做優化;
  • 第三期:重點解決被動啟動場景下的耗時影響。

第一期:耗時長的函式與低版本機型做處理

第一期優化的時候,首先通過計算方法耗時以及 InstrumentApp Launch,Time Profiler等工具,在低版本的機型比如iPhone6、iPhone7,低記憶體(App啟動是系統記憶體小於100M的情況下),找到所有函式耗時大於100MS以上的函式,然後將這些函式進行整理。

發現低版本低記憶體情況:如下函式存在著嚴重的耗時:

  • 啟動的launchView,耗時大約在150ms-250ms之間
  • 神策的初始化函式,耗時大約在300ms - 650ms之間
  • 地圖SDK初始化函式,耗時大約在250ms-500ms之間
  • toastView初始化,耗時大約在100ms-260ms之間
  • 首頁高德地圖初始化, 耗時大約在600ms-1200ms之間
  • 首頁選址欄背景框動效圖載入,耗時大約在300ms-600ms之間

同時針對這些在低版本機型低記憶體情況下耗時長的函式,統計這些函式在高版本機型比如說iPhone11, iPhone12等機型上,低記憶體情況下的耗時進行對比。

發現在高版本低記憶體情況下,以上的函式耗時情況如下:

  • 啟動的launchView,耗時大約在50ms-100ms之間
  • 神策的初始化函式,耗時大約在100ms - 200ms之間
  • 地圖SDK初始化函式,耗時大約在50ms-150ms之間
  • toastView初始化,耗時大約在30ms-80ms之間
  • 首頁高德地圖初始化, 耗時大約在200ms-350ms之間
  • 首頁選址欄背景框動效圖載入,耗時大約在80ms-200ms之間

從以上函式在高低版本機型耗時可以看出,耗時函式之間的差別還是比較大,因此對這些函式進行進一步分析,同時應該針對手機的高低版本採取不同的處理方案。

  1. launchView 優化

專案中的launchView每次啟動的時候,都會生成,但是隻有在首次安裝,彈出隱私彈框的時候才會用到。而launchView載入的圖片是放在專案資料夾裡面,沒有用Assets.catalog來進行管理,同時圖片檔案較大,可以進行無失真壓縮。

原因分析

之所以將launchView載入的圖片,進行無失真壓縮,是為了減小圖片的大小,這樣圖片資料二進位制相對也減少,讀取的時間也能減少。雖然放到Assets.catalog管理的時候,Assets.catalog會再次對圖片進行內部壓縮處理,但通過對比ipa包解析出來的圖片檔案大小,可以看出無失真壓縮前後,ipa的對應的launchView的圖片大小,還是有減少。

而之所以選擇Assets.catalog來管理:

一方面是Assets.catalog管理的圖片,由於Assets.catalog在編譯後,生成.car檔案,.car檔案裡面儲存圖片相關各種屬性以及圖片的二進位制資料,在圖片進行載入的時候,可以通過mmap載入.car檔案,解析.car獲取到圖片相關基礎屬性,然後通過BOM快速找到圖片二進位制,並進行載入,載入時間相對放在資料夾的圖片載入時間,有至少一個量級的減少,同時Assets.catalog對圖片資源的採用蘋果內部壓縮演算法,在解壓縮和解碼等方面效率更高,有利於加快圖片的顯示。

另一方面Assets.catalog會識別具有相似屬性的影象,例如透明度、顏色空間、色域等,並且能夠把它們組織到一個較大的圖集中,這樣就無需儲存額外相同的元資料了,同樣在提交AppStore後,會進行Slicing@2x,@3x的圖片進行分割到不同裝置上,從而減小包體積大小,

優化方案

因為launchView只在首次安裝彈出隱私彈框的時候才用到,因此對launchView的生成新增判斷,只在首次安裝才生成launchView,同時將launchView載入的圖片,先進行無失真壓縮,然後放到Assets.catalog進行管理。

優化效果

在低版本機型低記憶體情況,經優化啟動的launchView,耗時大約在80ms-180ms之間,耗時縮短了60ms左右。

  1. 神策的初始化函式

神策函式初始化的主要耗時在於兩方面:

  • 設定神策埋點公共引數裡面獲取wifi地址的埋點
  • 上報神策啟用事件裡面的WKWebViewuserAgent埋點

原因分析

  • 獲取wifi地址之所以耗時在於,該方法需要讀取Wifi列表檔案涉及到IOcopy操作。

+ (NSString *)wifiBssid{ NSString *bssid = @""; NSArray *interFaces = CFBridgingRelease(CNCopySupportedInterfaces()); NSDictionary *info; for (NSString *ifname in interFaces) { info = CFBridgingRelease(CNCopyCurrentNetworkInfo(( __bridge CFStringRef)ifname)); if (info && [info count]) { break; } } if ([info.allKeys containsObject:@"BSSID"]) { bssid = info[@"BSSID"]; } return bssid; }

  • 獲取WKWebViewuserAgent耗時在於: WKWebView的首次建立,因為WKWebView是一個多程序元件,WKWebView的建立過程如下:

image.png

WKWebView的建立過程我們知道,首次建立WKWebView,需要外部單獨建立一個WKWebView程序來處理網路請求、內容載入/渲染,之後還需要將WKWebView程序與App裡面的WKWebView記憶體物件進行關聯,這裡外部建立WKWebView程序,相對耗時較大。

優化方案

神策 埋點 公共引數裡面獲取 wifi 地址耗時優化方案:

  • 因為獲取Wifi地址,只有在當前手機網路為Wifi情況下才能獲取到,如果當前手機是行動網路,獲取到的必然為空,所以行動網路情況下是沒有必要執行獲取Wifi地址操作的,因此獲取Wifi地址之前新增一層網路判斷,只有在Wifi網路才去獲取Wifi地址。
  • 因為神策初始化是在啟動的時候,所以這裡獲取網路狀態可以用Alamofire庫的NetworkReachabilityManager去獲取當前網路狀態,因為NetworkReachabilityManager在首次初始化會去獲取一次,因此可以保證使用的時候已經獲取到了網路狀態。而AFNetworkReachabilityManager因為建立初始化的時候預設網路狀態是AFNetworkReachabilityStatusUnknown,而是等到監聽回撥才去更新網路狀態,無法保證首次使用的時候已經獲取到網路狀態了。

上報神策啟用事件裡面的 WKWebView userAgent 埋點 耗時優化方案:

  • 因為神策啟用事件埋點的上報跟產品確定了後,是可以稍微延遲上報的,而且WKWebViewuserAgent,只跟目前手機系統的版本和手機型號有關,手機型號是固定的,因此可以通過系統的版本號進行磁碟快取。因此整體優化邏輯如下:

image.png

優化效果

  • 神策埋點公共引數裡面獲取wifi地址優化後,因為我們是出行產品,打車的時候大部分是在行動網路下,因此這個wifi造成的啟動耗時和卡頓大幅減小。
  • 上報神策啟用事件裡面的WKWebViewuserAgent埋點優化後,啟動耗時在低版本低記憶體情況下,耗時減小了大約200ms上下。

  • 地圖SDK初始化

地圖SDK是我們地圖部封裝的一個二方庫,裡面除了必要的地圖初始化外,還有關於開啟位置上報和配置IQKeyboardManager兩個操作。

原因分析

  • 這裡開啟位置上報因為涉及到地圖側AB快取讀取等操作,導致在低版本手機上耗時相對較長
  • IQKeyboardManager的配置,因為IQKeyboardManager單例的呼叫,觸發IQKeyboardManager的初始化,這裡也會有base64圖片資源轉換為UIImage等操作,在低版本手機上耗時也相對較長。
  • 跟地圖側研發確認後,這兩者都是可以延遲到首頁的viewDidAppear即啟動完成後再去執行。

優化方案

因為這兩者都可以延遲到啟動統計完成之後再去執行,而且只在低版本機型上耗時才比較大,因此新增機型判斷,如果是低版本機型就做延遲執行操作,如果是高版本機型則保留執行順序。

優化效果

在低版本機型上做了延遲載入,地圖SDK初始化操作的整體耗時,可以縮短差不多80-200ms,左右。

  1. toastView初始化

這裡的toastView是一個二方庫使用到的toastView,主要是支付,IM這些統一的元件二方庫,而非業務側自定義的toastView

原因分析

toastView是個單例,初始化的時候,會觸發裡面toastView圖示的載入,涉及到IO操作在低版本低記憶體機型上耗時較大。

優化方案

因為該toastView是二方庫才用到,而像支付,IM等二方庫,都是在啟動完成之後,點選進入不同頁面才有可能用到toastView,因此是可以延遲到啟動統計完成之後再去執行,而且只在低版本機型上耗時才比較大,因此新增機型判斷,如果是低版本機型就做延遲執行操作,如果是高版本機型則保留執行順序。

優化效果

在低版本機型上做了延遲載入,地圖SDK初始化操作的整體耗時,可以縮短差不多100-260ms

  1. 首頁高德地圖初始化

a.背景

首頁高德地圖建立,在低版本機型上呼叫MAMapView建立方法耗時就可以達到800ms左右,而在高版本機型上耗時就只有200ms左右

b. 原因分析

因為看不到高德原始碼,因此將這個問題反饋給高德地圖,但對方一直沒有進行優化。

c.優化方案

因為在低版本機型上耗時與高版本機型差異較大,而首頁地圖展示在低版本機型比如iPhone6, iPhone7等上面延遲放到首頁viewDidAppear之後,對比起來效果也沒差太多,因此跟領導商量後決定將首頁地圖在低版本機型上延遲到啟動統計完成之後再去執行,如果是高版本機型則保留之前執行順序。

d.優化效果

在低版本機型上對首頁高德地圖初始化做了延遲載入,使得整體的啟動耗時,可以縮短差不多600ms左右。

  1. 首頁選址欄背景框動效圖載入

首頁地址選址欄的背景動效,地圖側那邊用的是140張選址背景圖迴圈播放來做的動效,這裡涉及大量圖片的IO操作,總體耗時比較大。

原因分析

這裡本來想用lottie動畫來做選址背景動效,但由於lottie動畫iOS平臺有部分屬性不支援,導致動畫效果不理想,後來採用了多張圖片滾動播放做動效的效果。但這裡涉及大量圖片IO且都在主執行緒上,耗時比較大,因此將該問題反饋給地圖側。

優化方案

後面經過地圖側內部商量後,找UI減少了圖片數量,然後先顯示一張背景佔位圖,非同步去載入相關圖片,載入完成之後,在回到主執行緒顯示背景動效。

優化效果

在低版本機型上統計通過該優化,使得整體耗時,差不多可以縮短300ms左右。

  1. 其他優化

a.非同步序列佇列執行的任務

以上就是第一期最主要的優化,同樣第一期也將幾個操作放入到非同步序列佇列去執行:

  • 初始化HDID
  • 初始化ASA廣告
  • 初始化安全SDK

這三者本來耗時就不高,之所以只放這幾個操作,是因為之前將一些三方庫初始化,放到非同步序列佇列執行,會出現一些異常情況,比如將微信分享SDK的初始化在啟動時非同步執行,會偶現個別手機微信分享會卡頓,放回主執行緒初始化就沒問題,也在微信開放平臺提了工單,但都沒有得到可靠回覆。因此出於穩定性考慮,只放了這三個初始化到非同步序列佇列去執行。

而第一期裡面有一個核心內容就是啟動管理器,上文提到的啟動任務延遲執行和啟動任務非同步執行,這些都是放到啟動管理器裡面去管理。

b.啟動管理器

啟動器主要做了如下三方面事情:

  1. 判斷當前手機裝置型號是否為低版本機型

首先我們看一下裝置型號組成:裝置名稱 + 大版本 + 小版本。

首先獲取裝置的型號,然後獲取裝置型號裡面的大版本號,然後跟指定的型號版本(預設為11)做對比,低於指定版本,為低端機型。

``` // 低型號 手機 (iPhoneX及其以下) private class func isLowerPhoneDevice(_ lowPhoneVersionLimit: Int) -> Bool { let tipStr = "iPhone" var isLowMdapAssignDeviceVersion = false let deviceName = phoneDeviceName() if deviceName.contains(tipStr) { let array = deviceName.components(separatedBy: ",") if array.count == 2 { let phoneVersionName = (array.first ?? "") as NSString if phoneVersionName.length > tipStr.count { let tipRange = NSRange(location: tipStr.count, length: phoneVersionName.length - tipStr.count) let version = phoneVersionName.substring(with: tipRange) let deviceVersion = Int(version) ?? 0 if deviceVersion < lowPhoneVersionLimit { isLowMdapAssignDeviceVersion = true } } } } return isLowMdapAssignDeviceVersion }

/// 裝置的名字
private class func phoneDeviceName() -> String {
    var systemInfo = utsname()
    uname(&systemInfo)
    let machineMirror = Mirror(reflecting: systemInfo.machine)
    let identifier = machineMirror.children.reduce("") { identifier, element in             guard let value = element.value as? Int8, value != 0 else { return identifier }
        return identifier + String(UnicodeScalar(UInt8(value)))
    }
    return identifier
}

```

  1. 任務非同步執行

建立非同步執行序列佇列,新增非同步任務,並非同步執行

``` // 新增 非同步執行 任務 public func addAsynQueueTaskBlock(taskBlock: FJFLaunchTaskBlock?) { self.addTaskBlock(taskBlock, .asynQueue) }

// 依據 任務 所屬 型別 新增 任務
public func addTaskBlock(_ taskBlock: FJFLaunchTaskBlock?, _ blongType: FJFLaunchTaskBlongType) {
    let task_item = DispatchWorkItem {
        taskBlock?()
    }
    switch(blongType) {
    case .mainQueue:
        task_item.perform()
    case .asynQueue:
        self.asyn_queue.async(execute: task_item)
    }
}

```

  1. 主執行緒任務延遲執行

依據手機型號版本判斷是否將需要延期執行,如果為低版本機型,則將任務新增到延遲執行陣列,對任務依據優先順序進行排序;若為高版本直接執行任務。

``` // 依據 手機版 判斷 是否 需要延期任務 public func checkTaskBlockNeedDelay(_ taskBlock: FJFLaunchTaskBlock?, _ blongType: FJFLaunchTaskBlongType, _ taskPriorityType: FJFLaunchTaskPriorityType = .defaultPriority) { if self.isLowMdapAssignPhoneVersion { self.addDelayTaskBlock(taskBlock, blongType, taskPriorityType) } else { self.addTaskBlock(taskBlock, blongType) } }

// 依據 任務 所屬 型別 新增 延遲執行 任務
public func addDelayTaskBlock(_ taskBlock: FJFLaunchTaskBlock?,
                              _ blongType: FJFLaunchTaskBlongType,
                              _ taskPriorityType: FJFLaunchTaskPriorityType = .defaultPriority) {
    self.delayLaunchTaskArray.append(FJFLaunchTask.init(taskBlock, blongType, taskPriorityType))
    self.delayLaunchTaskArray = self.delayLaunchTaskArray.sorted(by: { (obj1: FJFLaunchTask, obj2: FJFLaunchTask) -> Bool in             return obj1.taskPriorityType.rawValue > obj2.taskPriorityType.rawValue
    })
}

```

執行延遲任務的時候,監聽runloop週期,當runloop進入即將進入休眠或者退出時候,去執行延遲任務,任務執行完畢後,關閉runloop監聽。

``` // 執行 延遲 任務 public func execDelayTask() { self.startRunloopIdleMonitor() }

/// 開啟 主執行緒 runloop 空閒 監聽
public func startRunloopIdleMonitor() {
    //獲取當前RunLoop
    let runLoop: CFRunLoop = CFRunLoopGetMain()
    //定義一個觀察者
    let activities = CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue

    mainObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, true, 0) { [weak self] (_, _) in             guard let self = self else {
            return             }

        if let tmpTask = self.delayLaunchTaskArray.first {
            self.addTaskBlock(tmpTask.taskBlock, tmpTask.taskBlongType)
            self.delayLaunchTaskArray.removeFirst()
        }

        if self.delayLaunchTaskArray.count == 0 {
            self.endRunloopIdleMonitor()
        }
    }

    if let tmpObserver = mainObserver {
        //添加當前RunLoop的觀察者
        CFRunLoopAddObserver(runLoop, tmpObserver, .commonModes)
    }
}

```

這裡之所以將延遲執行的任務放到runloop空閒的時候去執行,主要原因是為了避免卡頓,因為延遲執行的任務,一般是耗時任務,如果多個任務在同一個runloop週期執行,會導致當前runloop週期過期繁忙,主執行緒無法響應其他操作,因此進行對延遲執行任務進行分發,分發到不同的runloop空閒時候去執行,可以有效防止卡頓。

經過第一個版本的優化後,啟動耗時佔比3s以內的佔比差不多在98上線波動:

第二期:對業務流程做優化

因為在第一期優化的同時,也進行了相關函式耗時的上報和統計,針對上報的資料,對不同版本機型和記憶體情況的方法耗時進行整理,同時也跟其他同事的卡頓和包體積優化一起整合,進行了第二期的優化。第二期優化主要有幾點:

  • 啟動流程中無用的類和流程優化
  • 包體積優化:二方庫的動態庫改為靜態庫, 無用類和三方庫刪除等
  • 卡頓治理優化: 對城市列表介面優化,對資料處理優化,對UI佈局優化等。

  • 啟動流程中無用的類和流程優化

啟動流程這裡,隨著迭代進行,有很多類和流程是可以優化的。

原因分析

比如首頁UI的改造,有些類已經沒有用到,而有些類是可選項,特定場景才用到,但是為了使用方便,設定了預設值,因此預設值生成的時候也有一定耗時;而有些流程只在登入或者未登入情況下才使用,但未做狀態區分,比如一鍵登入預取號邏輯,只需要在未登入情況下呼叫。因此針對這些流程進行整理優化。

優化方案

  • 對無用的類和程式碼,找對應負責人進行確認,確認無用後,刪除。
  • 對於特定場景用到的類,宣告為可選項,使用的時候新增判斷,不設定預設值,不使用懶載入
  • 對於區分登入或未登入狀態的流程,進行校對,新增狀態判斷,減少不必要的呼叫。

優化效果

因為去掉了無用類的生成和區分的一些狀態,減小了不必要的函式呼叫,整體耗時,縮短了80ms-150ms

  1. 包體積優化:二方庫的動態庫改為靜態庫, 無用類和三方庫刪除等

其他同事進行包體積優化,將一些二方庫改為靜態庫,同時刪除了無用的類和三方庫,以及將圖片放到Asset.catalog裡面管理等操作來減小包體積。

原因分析

  • 將二方庫從動態庫改為靜態庫,比如支付元件,IM元件等,減少了啟動連結裡面的rebasebind操作,也相應減少了啟動耗時
  • 無用類和無用三方庫的刪除,減少了啟動階段,類的載入,相應也減少了啟動耗時
  • 圖片資原始檔壓縮以及Asset.catalog來管理,加快了圖片的載入時間,也減少啟動耗時

優化方案

  • 通過修改對應二方庫的podspec檔案,將庫型別指定為靜態庫s.static_framework = true
  • 通過WBBlades工具對swift程式碼進行掃描,找出無用的類和程式碼,然後再進行二次確認刪除無用的類。
  • 通過對三方,二方庫依賴關係進行分析和確認,去掉無用的庫
  • 通過imageOptiom對資源的無失真壓縮,然後採用Asset.catalog管理圖片

優化效果

因為包體積優化是在同事分支上,合併到迭代整合分支的時候,測試了下,整體耗時縮短了60ms-150ms左右。

  1. 卡頓治理優化

其他同事進行卡頓治理的時候,順帶將啟動流程中的統計到的卡頓也進行治理了,比如拉取城市列表資料處理,對廣告介面資料處理,對安全中心等佈局的優化等。

原因分析

啟動耗時優化其實算是卡頓治理的一個子集,因此其他同事在做卡頓治理的時候,涉及到啟動相關的治理,也相應的能減少啟動耗時。

優化方案

  • 城市列表資料是每次啟動都會依據版本號去拉取,拉取回來後會進行省市等處理,然後進行快取,因為城市列表資料量較大,處理都在主執行緒上,因此在低版本機型會偶爾產生卡頓,尤其是第一次安裝拉取,沒有版本快取的時候,這裡同事建立了專門用來處理卡頓的序列佇列,非同步去執行處理流程和儲存流程,處理完成後再到主執行緒回撥。
  • 對於首頁廣告列表等同樣較大資料量的請求處理,同事也是放到專門用來處理卡頓的序列佇列,非同步去執行處理流程,處理完成後再到主執行緒回撥。
  • 對於安全中心、輪播圖裡面存在的佈局頻繁更新等問題,則是跟對應負責人討論後,減少了呼叫頻次,以及處理了進入後臺定時器依賴執行等問題,對啟動耗時也有一定降低。

優化效果

這邊關於啟動流程的卡頓的優化,也從整體上減小了啟動耗時。

經過第二個版本的優化後,啟動耗時佔比3s以內的佔比差不多在98.6上線波動:

第三期:被動啟動優化

第三期最主要的優化是一個被動啟動的優化。

因為我們專案是一個出行專案,會有對應的定位更新功能,因此會存在由於位置更新導致的被動啟動,而且佔比相對較大,從神策統計資料統計資料看,被動啟動佔比可以達到30%多。

什麼是被動啟動

我們把AppiOS系統觸發、App仍然處於後臺的啟動,稱之為App的被動啟動。

iOS7之後,蘋果新增了後臺應用程式重新整理功能,該功能允許作業系統在一定的時間間隔內(這個時間間隔根據使用者不同的操作習慣而有所不同,可能是幾個小時,也可能是幾天)拉起應用程式並同時讓其保持在後臺,以便應用程式可以獲取最新的資料並更新相關內容,從而可以確保使用者在開啟應用程式的時候可以第一時間檢視到最新的內容。

例如:新聞或者社交媒體型別的應用程式,可以使用這個功能在後臺獲取到最新的資料內容,在使用者開啟應用程式是可以縮短應用程式啟動和獲取內容展示的等待時間,最終提升產品的使用者體驗。

應用程式的被動啟動,應用程式的第一個頁面(UIViewController)也會被載入,也會觸發啟動耗時的統計。

但這裡使用者並沒有開啟應用程式,更沒有瀏覽第一個頁面,整個後臺應用重新整理的過程,對於使用者而言,完全是透明的,無感知的。

專案哪些功能會觸發被動啟動

使用Xcode建立新的應用程式,預設情況下後臺重新整理功能是關閉的,我們可以在Capabilities標籤中開啟Background Modes,然後就可以勾選所需要功能了。

目前專案裡面勾選瞭如下三種功能:

  • Location updates:

這種模式下,按照蘋果文件的官方說法,如果你在Xcodebackground modes中開啟了Location updates,並且使用者授權了Always訪問位置許可權,而且專案程式碼裡面也註冊了region monitoring或者significant change service,那麼你把App 殺掉後,系統仍然可以喚醒你的App,但此次喚醒大概就給你10s的時間處理地址位置資料,如果你執行處理地理位置是個長時間任務你需要向系統申請額外的時間進行處理:beginBackgroundTaskWithName:expirationHandler:`

Location and Maps Programming Guide

  • Background fetch

開啟該選項,需要設定一個時間間隔,從而讓iOS在一定間隔時間內在後臺啟動該應用,執行指定資料的獲取工作,而此過程最多隻能執行30s。

雖然開啟該選項,預設情況下iOS是不進行後臺獲取的,minimumBackgroundFetchInterval的預設值UIApplicationBackgroundFetchIntervalNever,你可以將值設定為UIApplicationBackgroundFetchIntervalMinimum,要求系統儘可能頻繁地去呼叫。

  • Remote notifications

開啟該選項是支援靜默推送,它有別於一般的推送,應用收到此類推送後,不會有任何的介面提示,而當應用退出或者掛起時收到此類推送,iOS 也會啟動或者喚醒對應的應用。例如一個閱讀應用,使用者訂閱的部落格更新了,那麼可以先發一個靜默推送,應用收到此種推送後,可以先把使用者訂閱的部落格內容都下載好,再通知使用者,這樣使用者一開啟應用就可以馬上開始閱讀。收到靜默推送,會回撥對應的回撥方法,而此回撥方法最多隻能執行 30 秒鐘。

被動啟動對啟動耗時影響

App的被動啟動的時候App此時仍然是處於後臺的,不像正常啟動App已經回到前臺。

App處於後臺的時候,App的任務優先順序相對處於前臺的程序是低的,各種資源比如CPU時間片,記憶體等的分配等優先順序也相對低,經測試當App被動啟動的時,如果前臺有其他App在執行,比如高德地圖這種記憶體佔比大的程序,這時候被動啟動的App,在執行比如圖片載入、WKWebView等耗時任務的時間消耗相對比正常啟動時候來得大。

這種情況來說啟動統計的耗時也相對較大,而且也容易產生卡頓,因此從App穩定性來說是有必要針對被動啟動進行優化的。

優化方案

  • 針對Background Modes裡面的會導致被動啟動的選項,根據專案需要去掉Background fetch選項,減少被動啟動次數
  • 針對App的啟動,通過applicationState判斷是否為UIApplicationStateBackground,如果是判斷為被動啟動,可以在啟動耗時統計的時候,將被動啟動的統計資料不上報。

優化效果

對被動啟動的啟動耗時統計資料過濾不進行上報後,整體啟動耗時3s以內佔比,可以達到99.7左右。

img_v2_e0c7f848-eaf6-49b4-878d-c2a20c61abeg.jpg

四. 總結

在這個啟動耗時優化的過程中,可以總結出如下四點相關的經驗:

  • 最關鍵的點:找到耗時高的函式。先治理耗時相對最高的那些函式,分析耗時高的本質原因,然後採取對應的方法進行優化,優化之後進行codeReview,跟同事一起探討優化操作可能產生的影響。
  • 測試環境一致:測試時應儘量在最差環境下測試。比如低版本機型,低記憶體情況下來統計啟動耗時,像我會寫一個可以不斷消耗記憶體的Demo程序,然後將手機記憶體消耗到100M以下,甚至更低,然後分別去測試低版本機型的啟動耗時情況,在這種情況下得到的統計資料,再去推測本次優化可以達到的預期,相對來說比較準確。
  • 要有降級方案:啟動耗時優化,之所以稱為優化,就代表這是一項錦上添花的操作。因此針對每次版本優化,最好有降級方案,防止本次優化帶來其他額外問題的時候,可以及時回退到以前方案,保證App整體的穩定性。
  • 防劣化建設: 要想長期保持優秀的啟動資料,防劣化建設必不可少。首先codereview關注程式碼和業務邏輯對穩定性產生的影響;接著對二方庫、三方庫的升級和引入進行審查和評估;然後每次迭代整合包的時候,測試和研發都會通過移動測試平臺-MTC,對各種機型跑一遍效能測試並輸出測試報告,而我自身電腦也安裝了appium自動化測試,也會對iPhone6等低版本機型、低記憶體下,跑下啟動時長資料,得到最小值、最大值、均值,並和上次迭代做對比,如果差距比較大,就去分析此次迭代程式碼,找到原因並聯系相關開發人員修復。最後上線後關注每日啟動時長資料,並設定告警機制,當某天統計資料突然劣化,就通過相關埋點和日誌資料分析原因,並優化。