影像成像原理
- CPU:物件的建立與銷燬,物件屬性的調整、佈局計算、文字的計算與排版,圖片的格式轉換和解碼、影像的繪製
- GPU:紋理的渲染
iOS採用了雙快取頁交換技術 + 垂直同步技術
-
垂直同步:防止造成畫面撕裂、跳幀的現象(就是這次還沒有渲染完就顯示到螢幕上了)。也就是當一次VSync訊號(按照60FPS來算,就是16ms有一次VSync訊號)來的時候,從螢幕緩衝區還讀不到,那麼這一幀就會等到下一次VSync訊號來的時候再顯示,也就是掉幀。
-
雙快取頁交換技術
iOS的雙快取編碼(CGBitmapContextCreate)
- 所謂“螢幕雙緩衝”是指在記憶體中建立一個“圖形裝置上下文的快取”,所有的繪圖操作都在這個“圖形上下文快取”上進行,在需要顯示這個“圖形上下文”的時候,再次把它更新到螢幕裝置上。
卡頓的解決思路
- 儘可能減少CPU、GPU資源消耗
- 儘量使用輕量級的物件,比如不需要事件處理的地方,可以用CALayer代替UIView
- 不要頻繁的調整UIView的相關屬性,如frame、bounds、transform等屬性
- 提前計算好佈局,在有需要時一次性調整對應的屬性
- Autolayout會比直接訊息frame消耗更多的資源
- 圖片的size最好和UIImageView的size一樣
- 控制一下執行緒的最大併發數量
- 將耗時操作放到子執行緒
- 文字處理(尺寸計算、繪製)
- 圖片處理(解碼、繪製)
- 儘量避免短時間內大量圖片的顯示,儘可能將多張圖片合成一張進行展示
- GPU能處理的最大紋理是4096*4096,如果超過這個尺寸,就會佔用CPU去處理
- 儘量減少檢視的數量和層次
- 減少透明的檢視,不透明就設定opaque = YES就可以了
離屏渲染
在OpenGL中,GPU有2種渲染方式 在屏渲染:在當前用於顯示的螢幕緩衝區進行渲染操作 離屏渲染:在當前螢幕快取區外新開闢一個緩衝區進行渲染操作
- 為什麼消耗效能?
- 需要建立新的緩衝區
- 多次切換上下文環境,在屏緩衝區切換到離屏緩衝區,離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到螢幕上,又切換回在屏緩衝區。
- 哪些操作會觸發?
- layer.shouldRasterize = true
- layer.mask
- layer.masksToBounds = true && layer.cornerRadius > 0(可以通過CG自己繪製裁剪圓角,或者叫美工提供圓角圖片)
- layer.shadowXXX(但是如果設定了layer.shadowPath就不會產生離屏渲染了)
卡頓檢測
- 利用Observer到主執行緒中的RunLoop中,通過監聽RunLoop狀態切換的耗時,以達到監測的目的
耗電的主要來源
- CPU
- 網路
- 定位
- 影像
耗電優化
-
少用定時器
-
優化I/O操作
- 儘量不要頻繁寫入小資料,最好指量一次性寫入
- 讀取大量重要資料時,考慮用dispatch_io,提供了基於GCD的非同步操作檔案I/O的API。系統會優化磁碟的訪問
- 資料量比較大時,建議使用資料庫
-
網路優化
- 減少、壓縮網路資料
- 如果多次請求的結果是相同的,儘量使用快取
- 使用斷點續傳,否則網路不穩定時可能多次傳輸相同的內容
- 批量傳輸
-
定位優化
- 如果只是需要快速確定使用者位置,最好用CLLocationManager的requestLocation方法。定位完成後,會自動讓定位硬體斷電
- 不需要實時更新位置,定位完畢就關掉定位服務
- 儘量降低定位的精度
- 需要後臺定位時,儘量設定pausesLocationUpdatesAutomatically為yes,如果使用者不太可能移動的時候,系統就會自動暫停位置更新
APP的啟動
- www.cocoachina.com/articles/24…
- 冷啟動:從0啟動
- 熱啟動:app已經在記憶體中,在後臺存活著,啟動
- 通過新增環境變數可以列印出App的啟動時間分析
DYLD_PRINT_STATISTICS設定為1
或者更加詳細的
DYLD_PRINT_STATISTICS_DETAILS設定為1(一般在400ms以內就是不錯的了)
複製程式碼
-
冷啟動分為2大階段
- pre-main:App開始啟動到系統呼叫main函式的這一段時間
- main:main函式到主UI框架的viewDidAppear函式呼叫的這一段時間
-
app啟動具體流程
- ①解析 info.plist
- 載入相關資訊,例如閃屏
- 沙箱建立、許可權檢查
- ②Mach-O載入
- 如果是胖二進位制檔案,尋找合適當前CPU架構的部分
- 載入所有依賴的Mach-O檔案(遞迴呼叫Mach-O載入的方法)
- 定位內部、外部指標引用
- 執行宣告__attribute__((constructor))的C函式
- 載入類擴充套件(Category)中的方法
- C++靜態物件載入,呼叫Objc的+load函式
- ③程式執行
- 呼叫main()
- 呼叫UIApplicationMain()
- 呼叫applicationWillFinishLaunching
dyld(蘋果的動態連結器)
系統先讀取App的可執行檔案,從裡面獲得dyld的路徑,然後載入dyld,dyld去初始化執行環境,開啟快取策略,載入程式相關依賴庫,並對這些庫進行連結,最後呼叫每個依賴庫的初始化方法,在這一步,runtime被初始化。
當所有依賴庫的初始化後,輪一最後一位進行初始化,在這時runtime會對專案中所有類進行類結構初始化,然後呼叫所有的load方法。最後dyld返回main函式地址,main函式被呼叫。
當載入一個Mach-O檔案時,動態連結器首先會檢查共享快取看看是否存在其中,如果存在,那麼就直接從共享快取中拿出來使用。每一程式都把這個共享快取對映到了自己的地址空間中。這個方法大大優化了iOS上程式的啟動時間。
Executable(程式的可執行檔案,Mach-O其中的一種格式)
- Mach-O被劃分成一些segement,大小為頁的整數。arm64下一面是16KB,其餘為4KB。
- 幾乎都包含__TEXT,__DATA和__LINKEDIT三個segment
- __Text包含Mach header,被執行的程式碼和只讀常量。只讀可執行
- __DATA包含全域性變數,靜態變數。可讀寫
- __LINKEDIT包含了載入程式的後設資料。比如函式的名稱和地址。只讀
ASLR:地址空間佈局隨機化,映象會在隨機的地址上載入
程式碼簽名:為了在執行時驗證Mach-O檔案的簽名,並不是每次重複讀入整個檔案,而是把每頁內容都生成一個單獨的加密雜湊值,並儲存在__LINKEDIT中。這使得檔案每頁的內容都能及時被校驗確保不被纂改。
載入 Dylib
從主執行檔案的 header 獲取到需要載入的所依賴動態庫列表,而 header 早就被核心對映過。然後它需要找到每個 dylib,然後開啟檔案讀取檔案起始位置,確保它是 Mach-O 檔案。接著會找到程式碼簽名並將其註冊到核心。然後在 dylib 檔案的每個 segment 上呼叫 mmap()。應用所依賴的 dylib 檔案可能會再依賴其他 dylib,所以 dyld 所需要載入的是動態庫列表一個遞迴依賴的集合。一般應用會載入 100 到 400 個 dylib 檔案,但大部分都是系統 dylib,它們會被預先計算和快取起來,載入速度很快。
載入系統的 dylib 很快,因為有優化(因為作業系統自己要用部分framework所以在作業系統開機後就已經快取了?)。但載入內嵌(embedded)的 dylib 檔案很佔時間,所以儘可能把多個內嵌 dylib 合併成一個來載入,或者使用 static archive。使用 dlopen() 來在執行時懶載入是不建議的,這麼做可能會帶來一些問題,並且總的開銷更大。
在每個動態庫的載入過程中, dyld需要:
- 分析所依賴的動態庫
- 找到動態庫的mach-o檔案
- 開啟檔案
- 驗證檔案
- 驗證檔案
- 在系統核心註冊檔案簽名
- 對動態庫的每一個segment呼叫mmap()
針對這一步驟的優化有:
- 減少非系統庫的依賴;
- 使用靜態庫而不是動態庫;
- 合併非系統動態庫為一個動態庫;
Rebase && Binding
- rebase: 在映象內部調整指標的指向。
Slide = actual_address - preferred_address
然後就是重複不斷地對 __DATA 段中需要 rebase 的指標加上這個偏移量。這就又涉及到 page fault 和 COW。這可能會產生 I/O 瓶頸,但因為 rebase 的順序是按地址排列的,所以從核心的角度來看這是個有次序的任務,它會預先讀入資料,減少 I/O 消耗。
- binding:將指標指向映象外部的內容,binding就是將這個二進位制呼叫的外部符號進行繫結的過程。
lazyBinding就是在載入動態庫的時候不會立即binding, 當時當第一次呼叫這個方法的時候再實施binding。 做到的方法也很簡單: 通過dyld_stub_binder這個符號來做。lazyBinding的方法第一次會呼叫到dyld_stub_binder, 然後dyld_stub_binder負責找到真實的方法,並且將地址bind到樁上,下一次就不用再bind了。
針對這一步驟的優化有:
-
減少Objc類數量, 減少selector數量,把未使用的類和函式都可以刪掉
-
減少C++虛擬函式數量
-
轉而使用swift stuct
ObjC SetUp
主要做以下幾件事來完成Objc Setup:
- 讀取二進位制檔案的 DATA 段內容,找到與 objc 相關的資訊
- 註冊 Objc 類,ObjC Runtime 需要維護一張對映類名與類的全域性表。當載入一個 dylib 時,其定義的所有的類都需要被註冊到這個全域性表中;
- 讀取 protocol 以及 category 的資訊,把category的定義插入方法列表 (category registration),
- 確保 selector 的唯一性
ObjC 是個動態語言,可以用類的名字來例項化一個類的物件。這意味著 ObjC Runtime 需要維護一張對映類名與類的全域性表。當載入一個 dylib 時,其定義的所有的類都需要被註冊到這個全域性表中。
Objc的load函式和C++的靜態建構函式採用由底向上的方式執行,來保證每個執行的方法,都可以找到所依賴的動態庫
-
dyld開始將程式二進位制檔案初始化
-
交由ImageLoader讀取image,其中包含了我們的類、方法等各種符號
-
由於runtime向dyld繫結了回撥,當image載入到記憶體後,dyld會通知runtime進行處理
-
runtime接手後呼叫mapimages做解析和處理,接下來loadimages中呼叫 callloadmethods方法,遍歷所有載入進來的Class,按繼承層級依次呼叫Class的+load方法和其 Category的+load方法
-
呼叫C++靜態初始化器和__attribute__((constructor))修飾的函式
到此為止,可執行檔案和動態庫中所有的符號(Class,Protocol,Selector,IM,...)都已經按照一定的格式被裝載進記憶體,被runtime管理
整個事件由dyld主導,完成執行環境的初始化後,配合ImageLoader 將二進位制檔案按格式載入到記憶體,動態連結依賴庫,並由runtime負責載入成objc 定義的結構,所有初始化工作結束後,dyld呼叫真正的main函式
這一步可以做的優化有:
-
使用 +initialize 來替代 +load
-
不要使用 attribute((constructor)) 將方法顯式標記為初始化器,而是讓初始化方法呼叫時才執行。比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用時才初始化,推遲了一部分工作耗時。也儘量不要用到C++的靜態物件。
main階段
總體原則無非就是減少啟動的時候的步驟,以及每一步驟的時間消耗。
main階段的優化大致有如下幾個點:
-
減少啟動初始化的流程,能懶載入的就懶載入,能放後臺初始化的就放後臺,能夠延時初始化的就延時,不要卡主執行緒的啟動時間,已經下線的業務直接刪掉;
-
優化程式碼邏輯,去除一些非必要的邏輯和程式碼,減少每個流程所消耗的時間;
-
啟動階段使用多執行緒來進行初始化,把CPU的效能儘量發揮出來;
-
使用純程式碼而不是xib或者storyboard來進行UI框架的搭建,尤其是主UI框架比如TabBarController這種,儘量避免使用xib和storyboard,因為xib和storyboard也還是要解析成程式碼來渲染頁面,多了一些步驟;