系統(tǒng)作的內存方面優(yōu)化
NSString
NSString的類型分為三種:
-
__NSCFConstantString
:字符串常量啦逆,存儲在字符串常量區(qū)翎承。引用計數(shù)很大蛉鹿。 -
__NSCFString
:運行時類型取劫,存儲在堆上巷帝。引用計數(shù)+1. -
NSTaggedPointerString
:小對象忌卤,存儲在常量區(qū),既包含指針楞泼,也包含值驰徊。小對象指針不再是簡單的地址笤闯,而是地址 + 值.優(yōu)點是占用空間小 節(jié)省內存.
taggedPoint小地址是值和指針地址放在了一起,存儲在常量區(qū)棍厂,不進行retain颗味,release管理,能夠直接釋放回收牺弹,效率更高浦马,創(chuàng)建更快,將近100倍张漂。iOS14之后還對taggedPointer進行了混淆晶默。最高位是taggedPointer的類型。
NSString * te = @“12345678912111”;這種方式不論多長鹃锈,都是__NSCFConstantString
類型荤胁,存儲在常量區(qū)。
通過stringWithFormat
或alloc
并且長度小于一定值時才為taggedPoint屎债。
Nonpointer_isa
SideTables
即散列表仅政,散列表中主要有引用計數(shù)表
和弱引用表
。
Nonpointer_isa為非指針類型的isa盆驹,可以存儲更多類的信息圆丹,包括引用計數(shù)。當引用計數(shù)存儲到一定值時躯喇,并不會再存儲到Nonpointer_isa
的位域的extra_rc
中辫封,而是會存儲到引用計數(shù)表
中。
id objc_retain(id obj)
{
if (obj->isTaggedPointerOrNil()) return obj;
return obj->retain();
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
通過對retain源碼的分析可以得知:
當引用計數(shù)存儲到一定值時廉丽,并不會再存儲到Nonpointer_isa
的位域的extra_rc
中倦微,而是除以二(RC_HALF
)的部分存儲到SideTables
散列表中。
SideTables是一個真機長度8個元素正压,其他64個長度的hash數(shù)組欣福,本質是一個哈希表
,集合了數(shù)組和鏈表的長處焦履,增刪改查都比較方便拓劝,里面存儲了SideTable。SideTables的hash鍵值就是一個對象obj的address嘉裤。為什么sidetables最多有8張呢郑临,而不是一張呢
? 因為所有對象的引用技術全放在一張散列表中不安全,假如只訪問一個而可以拿到全部的屑宠,并且每次訪問都要加鎖解鎖厢洞,對于性能和安全性都不高,所以分了8張表。
retain做了什么:簡單回答是計數(shù)值加1犀变,更加底層應該這么回答
:
- 首先判斷
是不是
taggedpointer妹孙,如果是,就立馬返回获枝。因為taggedpointer
不需要內存管理蠢正,taggedPointer存儲在常量區(qū)。 - 判斷如果
不是nonpointerisa
省店,直接操作
散列表sidetable的引用計數(shù)嚣崭,每次操作散列表會開鎖等耗時耗性能,所以表會有多張懦傍,為的是更安全雹舀。 - 如果
是nonpointerisa
,表示isa聯(lián)合體開啟了指針優(yōu)化粗俱,isa可以存儲更多信息说榆,那么就操作isa.bits中的extra_rc的引用計數(shù)+1。 - 如果extra_rc
滿了
寸认,就和散列表對半開签财,各存一半,以提高性能偏塞。//這么操作的目的在于提高性能唱蒸,因為如果都存在散列表中,當需要release-1時灸叼,需要去訪問散列表神汹,每次都需要開解鎖,比較消耗性能古今。extra_rc存儲一半的話屁魏,可以直接操作extra_rc即可,不需要操作散列表,性能會提高很多捉腥。 - alloc創(chuàng)建的nopointerisa對象引用計數(shù)
為0
蚁堤,包括sideTable,uintptr_t rc = 1 + bits.extra_rc
;所以對于alloc來說,是 0+1=1但狭,這也是為什么通過retaincount獲取的引用計數(shù)為1的原因
。alloc創(chuàng)建的對象實際的引用計數(shù)為0撬即,其引用計數(shù)打印結果為1立磁,是因為在底層rootRetainCount方法中,引用計數(shù)默認+1了剥槐,但是這里只有對引用計數(shù)的讀取操作唱歧,是沒有寫入操作的,簡單來說就是:為了防止alloc創(chuàng)建的對象被釋放(引用計數(shù)為0會被釋放),所以在編譯階段颅崩,程序底層默認進行了+1操作
几于。實際上在extra_rc中的引用計數(shù)仍然為0。 - 在dealloc流程匯總會判斷是否有isa沿后、cxx沿彭、關聯(lián)對象、弱引用表尖滚、引用計數(shù)表喉刘。
引用計數(shù)分別保存在
isa.extra_rc
和sidetable
中,當isa.extra_rc溢出
時漆弄,將一半
計數(shù)轉移至sidetable
中睦裳,而當其下溢
時,又會將計數(shù)轉回
撼唾。當二者都為空時廉邑,會執(zhí)行釋放
流程 。
項目可以進行的優(yōu)化
界面優(yōu)化:
- cell數(shù)據(jù)預加載倒谷,數(shù)據(jù)請求下來后提前計算高度存儲在model中蛛蒙,tableview代理中直接賦值高度。如果cell布局用的是autolayout恨锚,可以開啟tableView的預估高度機制Self-Sizing 宇驾。
- 按需加載:scrollviewwillEndDragging里判斷加載的是否是前后三行。
- 圖片渲染:圖片最終能展示的都是
bitmap位圖
猴伶。不管是 JPEG 還是 PNG 圖片炬称,都是一種壓縮
的位圖圖形格式。只不過 PNG 圖片是無損壓縮
掰邢,并且支持 alpha 通道界弧,而 JPEG 圖片則是有損壓縮
,可以指定 0-100% 的壓縮比办桨。解壓縮后的圖片大小 = 圖片的像素寬 * 圖片的像素高 * 每個像素所占的字節(jié)數(shù) 4
筹淫。將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式,這是一個非常耗時的 CPU 操作呢撞,并且它默認是在主線程中
執(zhí)行的损姜。imageView.image = image這一步會做解碼,并且是主線程
上殊霞。在將磁盤中的圖片渲染到屏幕之前摧阅,必須先要得到圖片的原始像素數(shù)據(jù),才能執(zhí)行后續(xù)的繪制操作
绷蹲,這就是為什么需要對圖片解壓縮的原因棒卷。所以可以用SDWebImage預解碼
顾孽,異步在子線程強制解壓縮
。強制解壓縮的原理就是對圖片進行重新繪制
比规,得到一張新的解壓縮后的位圖若厚。其中,用到的最核心的函數(shù)是CGBitmapContextCreate
蜒什。當頭像要切圓角時测秸,下載下來后,會在后臺線程將頭像預先渲染為圓形并單獨保存到一個 ImageCache 中去吃谣。 - 復雜界面的渲染可以合成一張圖后然后再進行渲染乞封,具體可以參考美團的
Graver
框架。 - main函數(shù)前后階段優(yōu)化
冷啟動:系統(tǒng)里沒有任何進程的緩存信息岗憋,典型的是重啟手機后直接啟動 App
熱啟動:如果把 App 進程殺了肃晚,然后立刻重新啟動,這次啟動就是熱啟動仔戈,因為進程緩存還在关串。
main函數(shù)之前的階段pre-main階段的啟動時間
其實就是dyld加載過程
的時間。針對main函數(shù)之前的啟動時間监徘,蘋果提供了內建的測量方法晋修,在Edit Scheme -> Run -> Arguments ->Environment Variables
添加環(huán)境變量 DYLD_PRINT_STATISTICS
設為 1),然后運行就能看到打印耗時的日志了凰盔。
Total pre-main time: 2.7 seconds (100.0%)
dylib loading time: 450.07 milliseconds (15.6%) 主要是`加載`動態(tài)庫
rebase/binding time: 210.8 milliseconds (8.5%) (偏移修正/符號綁定耗時) ALSR rebase /binding符號 (偏移地址+ALSR = 運行時執(zhí)行地址) 外部`綁定`的動態(tài)庫越多越耗時
ObjC setup time: 921.22 milliseconds (33.5%) (OC類注冊的耗時):OC類越多墓卦,越耗時。Swift耗時就會少很多户敬。
initializer time: 1113.94 milliseconds (45.3%) 執(zhí)行l(wèi)oad和構造函數(shù)的耗時
pre-main階段優(yōu)化建議
:
蘋果官方建議自定義的動態(tài)庫
最好不要超過6個
落剪。
二進制重排
:主要是大項目 二進制重排優(yōu)化都適用,主要目的是將啟動時刻需要調用的方法排列在一起。啟動時刻會出現(xiàn)大量的缺頁異常PageFault
尿庐,當進程訪問一個虛擬內存Page而對應的物理內存卻不存在時忠怖,會觸發(fā)一次缺頁中斷(Page Fault),分配物理內存抄瑟,有需要的話會從磁盤mmap讀人數(shù)據(jù)凡泣。發(fā)生Page Fault的時候線程是被blocked
。一般項目會有0.5-1s的page fault皮假。導致Page Fault次數(shù)過多的根本原因
是啟動時刻需要調用的方法鞋拟,處于不同的Page導致的。因此惹资,我們的優(yōu)化思路就是:將所有啟動時刻需要調用的方法严卖,排列在一起,即放在一個頁中布轿,這樣就從多個
Page Fault變成了一個
Page Fault。這就是二進制重排的核心原理。
利用xcode自帶工具Instrument中的
SystemTrace
就能看到項目的PageFault
次數(shù)汰扭,即圖中的File Backed Page In
稠肘。
工程Build Setting打開link map,記錄了二進制文件的布局萝毛,可以看到方法的執(zhí)行順序项阴,路徑在.app上一級同級的文件再依次往下找。
打開xcode的Instrument中的system trace笆包,用來展示啟動pageFault次數(shù)环揽。
工程路徑中創(chuàng)建一個.order文件,在這個order文件里排列你想加載的順序庵佣,然后在build setting里搜order file歉胶,把.order路徑加進去就可以了。
那么那么多方法巴粪,哪個方法才算是啟動期間調用的呢通今。在方法的啟動耗時中,需要去 Hook objc_msgSend 來達到監(jiān)控所有 ObjC 方法的目的肛根。
hook objc_msgsend
:
- 絕大部分Objective C的方法在編譯后會走
objc_msgSend
辫塌,所以通過[fishhook](https://github.com/facebook/fishhook)
hook這一個C函數(shù)即可獲得Objective C符號。由于objc_msgSend是變長參數(shù)
派哲,所以hook代碼需要用匯編
來實現(xiàn),對開發(fā)人員要求較高臼氨。而且也只能拿到OC 和 swift中@objc 后的方法. - 所以用
clang插樁
來拿到所有方法,做到100%覆蓋符號芭届。進入clang官網(wǎng)储矩,會有示例代碼.主要是trace_pc_guard追蹤各種方法、函數(shù)喉脖、block等的調用椰苟。需要在Build Settings中的Other C Flags,輸入-fsanitize-coverage=trace-pc-guard树叽,則Clang
就在讀代碼時候生成中間代碼IR時插入一行調用自己函數(shù)方法的代碼舆蝴,在xcode中實現(xiàn)對應的函數(shù)方法就可以拿到了。這屬于匯編插樁
题诵。 - dlfcn.h里的方法dladdr可以根據(jù)上面clang的函數(shù)方法返回的函數(shù)地址來獲取到對應的方法名sname
main階段之后的優(yōu)化建議
:
- 多線程加載業(yè)務邏輯,充分利用多線程性锭。能放后臺初始化的放后臺赠潦,盡量不要占用主線程的啟動時間。減少啟動初始化的流程草冈,能懶加載的懶加載她奥,能延遲的延遲瓮增。
- 不用的代碼和類去除(2w個類大約800毫秒)
- 盡量主頁面不要用xib等。
- swift盡量使用struct結構體哩俭,因為結構體是值類型绷跑,保存在棧中,效率比堆上更高凡资。椩夷螅空間地址分配的過程中是
從高到低
的(先分配0x0010再分配0x0001)
瘦身
官方 App Thinning
- App Thinning 會專門針對不同的設備來選擇只適用于當前設備的內容以供下載。大部分工作都是由 Xcode 和 App Store 來幫你完成的隙赁,你只需要通過 Xcode 添加 xcassets 目錄垦藏,然后將圖片添加進來即可。
- Xcode 默認會開啟
DEAD_CODE_STRIP
選項伞访,C/C++/Swift 等靜態(tài)語言
編譯器會在 link 的時候移除未使用的代碼掂骏,但是對于 Objective-C 等動態(tài)語言
是無效的,因為動態(tài)語言是建立在運行時
上面的。 - 編譯期優(yōu)化參數(shù):
GCC_OPTIMIZATION_LEVEL
定義了 clang 用什么優(yōu)化等級
進行編譯優(yōu)化咐扭。 Xcode 默認的 Debug 使用-O0
, Release 使用-Os
芭挽。更為激進的-Oz
會減小相同代碼存在多份
問題,但是也會使得的函數(shù)調用存在更深的調用棧
蝗肪,會影響性能袜爪。Oz 的核心原理是對重復的連續(xù)機器指令外聯(lián)成函數(shù)進行復用,和“內聯(lián)函數(shù)”的原理正好相反薛闪。因此辛馆,開啟 Oz,能減小二進制的大小豁延,但同時理論上會帶來執(zhí)行效率的額外消耗昙篙。對性能(CPU)敏感的代碼使用需要評估。 - 鏈接期優(yōu)化參數(shù): LLVM 提供鏈接期編譯優(yōu)化诱咏,通過設置工程中的
Link-Time Optimization
進行控制苔可,其本質是開啟生成LTO
等優(yōu)化格式。調試期不建議開啟
袋狞,會增加編譯時間焚辅。開啟 LTO 之后對于 Objc Runtime 需要的一些結構 比如方法簽名的literal string, protocol
的結構等有比較大的優(yōu)化苟鸯。
己方
- 使用頻率高且小的圖片放到 Asset.car 中同蜻,Asset.car 能保證其加載和渲染的速度最優(yōu)。而大的圖片比如背景圖之類的早处,長寬尺寸就有上千個像素湾蔓,而這種放到 Asset.car 中會大大的增加安裝包的大小。
- 無用圖片資源刪除砌梆,推薦庫LSUnusedResources
- 有用圖片瘦身:圖片大小超過了 100KB默责,你可以考慮使用 WebP贬循;將圖片轉成 WebP格式,推薦iSparta.在顯示圖片時使用 libwebp 進行解析傻丝。WebP 在 CPU 消耗和解碼時間上會比 PNG 高兩倍甘有。所以需要在性能和體積上做取舍。
- 對PNG圖片無損壓縮來優(yōu)化包大小沒有效果的葡缰,因為Xcode 會通過自己的壓縮算法重新對圖片進行處理,只能壓縮其尺寸大小忱反。Xcode 中泛释,構建 Asset Catalog 的工具 actool 會首先對 Asset Catalog 中的 png 圖片進行解碼,得到 Bitmap 數(shù)據(jù)温算,然后再運用 actool 的編碼壓縮算法進行編碼壓縮處理怜校。無損壓縮通過變換圖片的編碼壓縮算法減少大小,但是不會改變 Bitmap 數(shù)據(jù)注竿。對于 actool 來說茄茁,它接收的輸入沒有改變,所以無損壓縮無法優(yōu)化 Assets.car 的大小巩割。對于放入 Asset.car 中的圖片如果圖片沒有半透明效果裙顽,使用 70% 的有損壓縮
JPEG
是一個不錯的方式,既能保證圖片清晰度的同時獲得更小的大小宣谈。 - 代碼瘦身:LinkMap 來獲得所有的代碼類和方法的信息愈犹。Mach-O 文件的 __objc_selrefs、__objc_classrefs 和 __objc_superrefs可以獲取用過的方法闻丑,類漩怎,父類。但是Objective-C 是門動態(tài)語言嗦嗡,方法調用可以寫成在運行時動態(tài)調用勋锤,這樣就無法收集全所有調用的方法和類,還要
二次確認
侥祭。例如+load
方法會被系統(tǒng)調用叁执,但也能檢查為未使用類。推薦Appcode
工具卑硫。最簡單的靜態(tài)分析:基于 otool dump 最終產(chǎn)物中的 __objc_class_list & __objc_class_refs 做差集
找到未使用的 Objc
類徒恋。 -
Assets.car
和Mach-O
是占用空間最大的兩個文件。目前市場上最低支持的 iOS 系統(tǒng)版本一般為iOS 9
欢伏。然而入挣,大部分 Pod 庫的 Podspec 文件中指定的deployment_target
(最低支持版本)由于未及時修改,依然還是 iOS 8硝拧,這就導致了這些 Pod 庫中指定的resource_bundles
在構建出 Assets.car 時径筏,是以iOS 8
為最低支持版本的葛假。統(tǒng)一改成iOS 9這樣會多出一些優(yōu)化空間。 -
符號裁剪
:符號解釋滋恬, - 減少 Block 的使用
我們知道 Block 是一個特殊的 OC 對象聊训。需要占用部分二進制空間來表征一個 Block 對象。所以在非必要使用 Block 的場景恢氯。去掉 Block 實現(xiàn)可以優(yōu)化不少包大小带斑,常見的比如 Masonry 通過 Block 實現(xiàn)的鏈式調用。由此可見越是方便開發(fā)工作量勋拟,對性能就越是一個考驗勋磕。大部分問題都能轉化為空間和時間的取舍問題。
實際用到了但被掃描成無用類:
* 一個類確實沒有被其他地方使用敢靡, 但是本身邏輯依賴 +load 挂滓、+initialize、__attribute__((constructor)) 在啟動時調用
* 通過 string 動態(tài)調用
* 抽象基類啸胧、基類等會被認為是無用類
* 通過運行時動態(tài)生成的代碼引用了某個類
* 一個類專門作為通知處理類
* MTLModel 等赶站,通過運行時消息機制 assign value 的無法通過 classref 統(tǒng)計
* 典型的 DI 場景。如果一個類聲明遵循了某個 Protocol纺念,外部使用的時候使用了這個 Protocol 進行方法調用
實際沒用到但被認為有用到:
* 某個對象被另外一個對象引用贝椿,但是另外一個對象本身未被使用到。這時候會遺漏掉這個對象所屬 Class 的檢查
電量
-
CPU
:要避免讓 CPU 做多余的事情柠辞。對于大量數(shù)據(jù)的復雜計算团秽,應該把數(shù)據(jù)傳到服務器去處理,如果必須要在 App 內處理復雜數(shù)據(jù)計算叭首,可以通過 GCD 的dispatch_block_create_with_qos_class
方法指定隊列的 Qos 為QOS_CLASS_UTILITY
习勤,將計算工作放到這個隊列的 block 里。在QOS_CLASS_UTILITY
這種 Qos 模式下焙格,系統(tǒng)針對大量數(shù)據(jù)的計算图毕,以及復雜數(shù)據(jù)處理專門做了電量優(yōu)化。 -
I/O
:將碎片化的數(shù)據(jù)磁盤存儲操作延后眷唉,先在內存中聚合予颤,然后再進行磁盤存儲。碎片化的數(shù)據(jù)進行聚合冬阳,在內存中進行存儲的機制蛤虐,可以使用系統(tǒng)自帶的NSCache
來完成。NSCache
是線程安全的肝陪,NSCache 會在到達預設緩存空間值時清理緩存驳庭,這時會觸發(fā)cache:willEvictObject:
方法的回調,在這個回調里就可以對數(shù)據(jù)進行 I/O 操作,達到將聚合的數(shù)據(jù) I/O 延后的目的饲常。I/O 操作的次數(shù)減少了蹲堂,對電量的消耗也就減少了。SDWebImage
圖片加載框架贝淤,在圖片的讀取緩存處理時沒有直接使用 I/O柒竞,而是使用了 NSCache
。SDWebImage 將獲取的圖片數(shù)據(jù)都放到了 NSCache 里播聪,利用 NSCache 緩存策略進行圖片緩存內存的管理朽基。每次讀取圖片時,會檢查 NSCache 是否已經(jīng)存在圖片數(shù)據(jù):如果有离陶,就直接從 NSCache 里讀炔染А;如果沒有枕磁,才會通過 I/O 讀取磁盤緩存圖片。 - 蘋果專門維護了一個電量優(yōu)化指南术吝,分別從
CPU计济、設備喚醒、網(wǎng)絡排苍、圖形沦寂、動畫、視頻淘衙、定位传藏、加速度計、陀螺儀彤守、磁力計毯侦、藍牙
等多方面因素提出了電量優(yōu)化方面的建議。大家可以瞅瞅具垫。
卡頓檢測
卡頓檢測原理是通過子線程
對主runloop添加runloopObserver
監(jiān)控即將休眠
和喚醒
兩個狀態(tài)間的時間間隔大于2s
左右則認為卡頓侈离,這里確切的說是執(zhí)行souce
和進入休眠
兩個狀態(tài)更為精確。這里通過開啟一個子線程筝蚕,用while代碼循環(huán)持續(xù)loop卦碾,用定義一個超時2s左右的信號量,超過這個時間往下執(zhí)行的時候判斷信號量若不等于0則認為卡頓起宽,然后將 BeforeSources
和AfterWaiting
這兩個狀態(tài)區(qū)間上傳調用棧洲胖,并像微信卡頓監(jiān)聽方案matrix
那樣利用退火算法
,保證重復的卡頓調用棧信息不會被上傳坯沪。線程數(shù)超出64
個時會導致主線程卡頓绿映,如果卡頓是由于線程多造成的,那么就沒必要通過獲取主線程堆棧去找卡頓原因了屏箍,根據(jù) matrix-iOS
的實測绘梦,每隔 50 毫秒獲取主線程堆棧會增加 3% 的 CPU 占用橘忱,可以忽略不計。 卡頓的類型有線程過多卸奉、CPU滿負荷钝诚、繪制過度、IO操作榄棵、搶鎖
文件 dump:如果內存 dump 的堆棧跟上次捕捉到的不一樣凝颇,則 dump 到文件中;否則按照斐波那契數(shù)列
將檢查時間遞增(1疹鳄,1拧略,2,3瘪弓,5垫蛆,8…)直到?jīng)]有遇到卡頓或卡頓堆棧不一樣。這樣能夠避免同一個卡頓寫入多個文件的情況腺怯,也能避免檢測線程圍著同一個卡頓空轉的情況袱饭。
離屏渲染
只要裁剪(透明度/陰影)的內容需要畫家算法未完成之前的內容參與就會觸發(fā)offscreenrendering
。
正常的顯示是從幀緩存區(qū)FrameBuffer
去取呛占,而當我們設置了 cornerRadius 以及 masksToBounds 進行圓角 + 裁剪+陰影+高斯模糊
時虑乖,如前文所述,masksToBounds 裁剪屬性會應用到所有的 sublayer 上晾虑。這也就意味著所有的 sublayer 必須要重新被應用一次圓角+裁剪疹味,這也就意味著所有的 sublayer 在第一次被繪制完之后,并不能立刻被丟棄
帜篇,而必須要被保存在 Offscreen buffer
中等待下一輪圓角+裁剪糙捺,注意這時候并沒有顯示到屏幕上,多個圖層都在離屏緩存區(qū)等待被一一裁剪圓角(這里是每個圖層都要被裁剪坠狡,并不只是某個)继找,這也就誘發(fā)了離屏渲染。幀緩存區(qū)是展示完了就丟棄的
逃沿。離屏渲染并不都是壞的婴渡,因為對于頻繁顯示的復雜的,離屏會提高性能效率凯亮。