目錄
一. GCD和OperationQueue
二. CADisplayLink、NSTimer使用注意
三. 內(nèi)存布局
四. Tagged Pointer
五. copy和mutableCopy
六. OC對象的內(nèi)存管理
七. AutoreleasePool自動釋放池
八. 圖片的解壓縮到渲染過程
九. 應用卡頓的原因以及優(yōu)化
十. APP的啟動
一. GCD和NSOperationQueue
GCD 可用于多核的并行運算妓湘;
GCD 會自動利用更多的 CPU 內(nèi)核(比如雙核温兼、四核)甘晤;
GCD 會自動管理線程的生命周期(創(chuàng)建線程柠辞、調(diào)度任務鲸阔、銷毀線程)订晌;
程序員只需要告訴 GCD 想要執(zhí)行什么任務,不需要編寫任何線程管理代碼饿自。
NSOperation汰翠、NSOperationQueue 是基于 GCD 更高一層的封裝,完全面向?qū)ο笳汛啤5潜?GCD 更簡單易用复唤、代碼可讀性也更高。
- 可添加完成的代碼塊烛卧,在操作完成后執(zhí)行佛纫。
- 添加操作之間的依賴關系,設定操作執(zhí)行的優(yōu)先級总放,方便的控制執(zhí)行順序呈宇;設置最大并發(fā)數(shù)。
- 可以很方便的取消一個操作的執(zhí)行间聊。
- 使用 KVO 觀察對操作執(zhí)行狀態(tài)的更改:isExecuteing攒盈、isFinished抵拘、isCancelled哎榴。
同步和異步主要影響:能不能開啟新的線程
同步:在當前線程中執(zhí)行任務,不具備開啟新線程的能力
異步:在新的線程中執(zhí)行任務僵蛛,具備開啟新線程的能力
并發(fā)和串行主要影響:任務的執(zhí)行方式
并發(fā):多個任務并發(fā)(同時)執(zhí)行
串行:一個任務執(zhí)行完畢后尚蝌,再執(zhí)行下一個任務
二. CADisplayLink、NSTimer使用注意
- CADisplayLink充尉、NSTimer會對target產(chǎn)生強引用飘言,如果target又對它們產(chǎn)生強引用,那么就會引發(fā)循環(huán)引用驼侠。解決辦法是使用代理對象NSProxy姿鸿。
- NSTimer依賴于RunLoop,如果RunLoop的任務過于繁重倒源,可能會導致NSTimer不準時苛预。而GCD的定時器會更加準時。
三. 內(nèi)存布局
- 棧區(qū)(heap):由系統(tǒng)去管理笋熬。地址從高到低分配热某。先進后出。會存一些局部變量,函數(shù)跳轉(zhuǎn)跳轉(zhuǎn)時現(xiàn)場保護(寄存器值保存于恢復)昔馋,這些系統(tǒng)都會幫我們自動實現(xiàn)筹吐,無需我們干預。所以大量的局部變量秘遏,深遞歸丘薛,函數(shù)循環(huán)調(diào)用都可能耗盡棧內(nèi)存而造成程序崩潰 。
- 堆區(qū)(stack):需要我們自己管理內(nèi)存邦危,alloc申請內(nèi)存release釋放內(nèi)存榔袋。創(chuàng)建的對象也都放在這里。 地址是從低到高分配铡俐。堆是所有程序共享的內(nèi)存凰兑,當N個這樣的內(nèi)存得不到釋放,堆區(qū)會被擠爆审丘,程序立馬癱瘓吏够。這就是內(nèi)存泄漏。
- 全局區(qū)/靜態(tài)區(qū)(staic):全局變量和靜態(tài)變量的存儲是放在一塊的滩报,初始化的全局變量和靜態(tài)變量在一塊區(qū)域锅知, 未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域。程序結束后有系統(tǒng)釋放脓钾。
- 常量區(qū):常量字符串就是放在這里的售睹,還有const常量。
- 代碼區(qū):存放App代碼可训,App程序會拷貝到這里昌妹。
四. Tagged Pointer
- 從64bit開始,iOS引入了Tagged Pointer技術握截,用于優(yōu)化NSNumber飞崖、NSDate、NSString等小對象的存儲
- 在沒有使用Tagged Pointer之前谨胞, NSNumber等對象需要動態(tài)分配內(nèi)存固歪、維護引用計數(shù)等,NSNumber指針存儲的是堆中NSNumber對象的地址值
- 使用Tagged Pointer之后胯努,NSNumber指針里面存儲的數(shù)據(jù)變成了:Tag + Data牢裳,也就是將數(shù)據(jù)直接存儲在了指針中
- 當指針不夠存儲數(shù)據(jù)時,才會使用動態(tài)分配內(nèi)存的方式來存儲數(shù)據(jù)
- objc_msgSend能識別Tagged Pointer叶沛,比如NSNumber的intValue方法蒲讯,直接從指針提取數(shù)據(jù),節(jié)省了以前的調(diào)用開銷
- 如何判斷一個指針是否為Tagged Pointer恬汁?
iOS平臺伶椿,最高有效位是1(第64bit)辜伟;Mac平臺,最低有效位是1
五. copy和mutableCopy
六. OC對象的內(nèi)存管理
- 在iOS中脊另,使用引用計數(shù)來管理OC對象的內(nèi)存导狡。
- 一個新創(chuàng)建的OC對象引用計數(shù)默認是1,當引用計數(shù)減為0偎痛,OC對象就會銷毀旱捧,釋放其占用的內(nèi)存空間。
- 調(diào)用retain會讓OC對象的引用計數(shù)+1踩麦,調(diào)用release會讓OC對象的引用計數(shù)-1枚赡。
- 當調(diào)用alloc、new谓谦、copy贫橙、mutableCopy方法返回了一個對象,在不需要這個對象時反粥,要調(diào)用release或者autorelease來釋放它卢肃;想擁有某個對象,就讓它的引用計數(shù)+1才顿;不想再擁有某個對象莫湘,就讓它的引用計數(shù)-1。
七. AutoreleasePool自動釋放池
AutoreleasePool(自動釋放池) 是OC中的一種內(nèi)存自動回收機制郑气,在釋放池中的調(diào)用了autorelease方法的對象都會被壓在該池的頂部(以棧的形式管理對象)幅垮。當自動釋放池被銷毀的時候,在該池中的對象會自動調(diào)用release方法來釋放資源尾组,銷毀對象忙芒。以此來達到自動管理內(nèi)存的目的。
__AtAutoreleasePool 實際是一個結構體演怎,在內(nèi)部首先執(zhí)行objc_autoreleasePoolPush()匕争,然后在調(diào)用objc_autoreleasePoolPop(atautoreleasepoolobj)。
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
//構造函數(shù)爷耀,在創(chuàng)建結構體時調(diào)用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
//析構函數(shù),在結構體銷毀的時候調(diào)用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
AutoreleasePoolPage的結構
- 每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存拍皮,除了用來存放它內(nèi)部的成員變量歹叮,剩下的空間用來存放autorelease對象的地址。
- 所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起铆帽。
- 調(diào)用push方法會將一個POOL_BOUNDARY入棧咆耿,并且返回其存放的內(nèi)存地址
- 調(diào)用pop方法時傳入一個POOL_BOUNDARY的內(nèi)存地址,會從最后一個入棧的對象開始發(fā)送release消息爹橱,直到遇到這個POOL_BOUNDARY
- id *next指向了下一個能存放autorelease對象地址的區(qū)域
Autorelease何時釋放萨螺?
1、手動調(diào)用AutoreleasePool的釋放方法(drain方法)
2、Autorelease對象是在當前的runloop迭代結束時釋放的慰技,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop
Runloop和Autorelease的關系
- App啟動后椭盏,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()吻商。
- 第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)掏颊,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其 order 是 -2147483647艾帐,優(yōu)先級最高乌叶,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
- 第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池柒爸;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池准浴。這個 Observer 的 order 是 2147483647,優(yōu)先級最低捎稚,保證其釋放池子發(fā)生在其他所有回調(diào)之后兄裂。
八. 圖片的解壓縮到渲染過程
- 假設我們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片,這個時候的圖片并沒有解壓縮阳藻;
- 然后將生成的 UIImage 賦值給 UIImageView 晰奖;
- 接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化;
- 在主線程的下一個 runloop 到來時腥泥,Core Animation 提交了這個隱式的 transaction 匾南,這個過程可能會對圖片進行 copy 操作,而受圖片是否字節(jié)對齊等因素的影響蛔外,這個 copy 操作可能會涉及以下部分或全部步驟:
- 分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作蛆楞;
- 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中;
- 將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式夹厌,這是一個非常耗時的 CPU 操作豹爹;
- 最后 Core Animation 中CALayer使用未壓縮的位圖數(shù)據(jù)渲染 UIImageView 的圖層。
- CPU計算好圖片的Frame,對圖片解壓之后.就會交給GPU來做圖片渲染
- 渲染流程
- GPU獲取獲取圖片的坐標
- 將坐標交給頂點著色器(頂點計算)
- 將圖片光柵化(獲取圖片對應屏幕上的像素點)
- 片元著色器計算(計算每個像素點的最終顯示的顏色值)
- 從幀緩存區(qū)中渲染到屏幕上
總結:圖片渲染到屏幕的過程: 讀取文件->計算Frame->圖片解碼->解碼后紋理圖片位圖數(shù)據(jù)通過數(shù)據(jù)總線交給GPU->GPU獲取圖片F(xiàn)rame->頂點變換計算->光柵化->根據(jù)紋理坐標獲取每個像素點的顏色值(如果出現(xiàn)透明值需要將每個像素點的顏色*透明度值)->渲染到幀緩存區(qū)->渲染到屏幕
九. 應用卡頓的原因以及優(yōu)化
CPU: 計算視圖frame矛纹,文本計算和排版臂聋,圖片解碼,需要繪制紋理圖片通過數(shù)據(jù)總線交給GPU或南。
GPU: 紋理混合孩等,頂點變換與計算,像素點的填充計算,渲染到幀緩沖區(qū)采够。
平時所說的“卡頓”主要是因為在主線程執(zhí)行了比較耗時的操作肄方,
可以添加Observer到主線程RunLoop中,通過監(jiān)聽RunLoop狀態(tài)切換的耗時蹬癌,以達到監(jiān)控卡頓的目的权她。
-
1. 屏幕呈像原理
-
2. 卡頓產(chǎn)生的原因
在 VSync 信號到來后虹茶,系統(tǒng)圖形服務會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內(nèi)容隅要,比如視圖的創(chuàng)建蝴罪、布局計算、圖片解碼拾徙、文本繪制等洲炊。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進行變換尼啡、合成暂衡、渲染。隨后 GPU 會把渲染結果提交到幀緩沖區(qū)去崖瞭,等待下一次 VSync 信號到來時顯示到屏幕上狂巢。由于垂直同步的機制,如果在一個 VSync 時間內(nèi)书聚,CPU 或者 GPU 沒有完成內(nèi)容提交唧领,則那一幀就會被丟棄,等待下一次機會再顯示雌续,而這時顯示屏會保留之前的內(nèi)容不變斩个。這就是界面卡頓的原因。
從上面的圖中可以看到驯杜,CPU 和 GPU 不論哪個阻礙了顯示流程受啥,都會造成掉幀現(xiàn)象。所以開發(fā)時鸽心,也需要分別對 CPU 和 GPU 壓力進行評估和優(yōu)化滚局。
-
3. 卡頓優(yōu)化
CPU
- 盡量用輕量級的對象,比如用不到事件處理的地方顽频,可以考慮使用CALayer取代UIView
- 不要頻繁地調(diào)用UIView的相關屬性藤肢,比如frame、bounds糯景、transform等屬性嘁圈,盡量減少不必要的修改
- 盡量提前計算好布局,在有需要時一次性調(diào)整對應的屬性莺奸,不要多次修改屬性
- Autolayout會比直接設置frame消耗更多的CPU資源
- 圖片的size最好剛好跟UIImageView的size保持一致
- 控制一下線程的最大并發(fā)數(shù)量
- 盡量把耗時的操作放到子線程:文本處理(尺寸計算丑孩、繪制)、圖片處理(解碼灭贷、繪制)等
GPU
- 盡量避免短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進行顯示
- GPU能處理的最大紋理尺寸是4096x4096略贮,一旦超過這個尺寸甚疟,就會占用CPU資源進行處理仗岖,所以紋理盡量不要超過這個尺寸
- 盡量減少視圖數(shù)量和層次
- 減少透明的視圖(alpha<1),不透明的就設置opaque為YES
- 盡量避免出現(xiàn) 離屏渲染
十. APP的啟動
APP的冷啟動可以概括為3大階段:dyld览妖、runtime轧拄、main
-
1. dyld
dyld(dynamic link editor),Apple的動態(tài)鏈接器讽膏,可以用來裝載Mach-O文件(可執(zhí)行文件檩电、動態(tài)庫等)。
啟動APP時府树,dyld所做的事情有:
- 裝載APP的可執(zhí)行文件俐末,同時會遞歸加載所有依賴的動態(tài)庫.
- 當dyld把可執(zhí)行文件、動態(tài)庫都裝載完畢后奄侠,會通知Runtime進行下一步的處理.
-
2. runtime
啟動APP時卓箫,runtime所做的事情有:
- 調(diào)用map_images進行可執(zhí)行文件內(nèi)容的解析和處理
- 在load_images中調(diào)用call_load_methods,調(diào)用所有Class和Category的+load方法
- 進行各種objc結構的初始化(注冊Objc類 垄潮、初始化類對象等等)
- 調(diào)用C++靜態(tài)初始化器和attribute((constructor))修飾的函數(shù)
-
3. main
接下來就是UIApplicationMain函數(shù)烹卒,AppDelegate的application:didFinishLaunchingWithOptions:方法
-
4. APP啟動優(yōu)化
推薦學習資料:
Swift從入門到精通
每周一道算法題
戀上數(shù)據(jù)結構與算法(一)
戀上數(shù)據(jù)結構與算法(二)
如果需要跟我交流的話:
※ 簡書:http://www.reibang.com/u/e15d1f644bea