此為高性能iOS應用開發的筆記。
此書很不錯,看了多次決定記一下筆記。
有些我覺得是廢話的,我就懶得記下了。
卡頓優化 或 內存優化,簡單的優化入手方面有:
頁面重疊部分、圓角部分、對象釋放部分、static使用部分(如單列對象)、UI刷新前的數據處理部分、頁面刷新次數、通知和監聽的移除、數據的不合理讀取(如cell里面訪問數據庫就不行)。
代碼優化:
各頁面的接口請求都封裝到一個類中,所有的類放到統一的文件夾中。
返回對象數據全部用Model。
建立一個所有ViewController都繼承的基層控制器。
View里誰的任務交給誰去處理,Model負責存儲數據,Controller負責處理與傳遞數據。
不要太死板。
程序猿皆知的DRY原則。
工程與代碼:
好的工程與代碼應符合三點:
1.能輕松地構建和發布應用
2.可測試性。確保代碼能同時在模擬數據和真實數據上工作,其中包括模擬的環境
3.可跟蹤性。能夠快速找到問題所在并處理
注意收集崩潰報告信息
可使用Flurry,或者騰訊Bugly
應用埋點的三個時期
當應用進入前臺、當應用進入后臺、當應用受到低內存警告
日志與埋點:埋點應用于特定階段、分析特定情況的時候,日志則用于整個APP的生命周期。日志可用CocoaLumberjack
內存消耗
RAM的消耗主要分為兩部分:棧大小和堆大小。
棧大小
- 應用中新創建的每個線程都有專用的棧空間,該空間由保留的內存和初始提交的內存組成。
- 棧可以在線程存在期間自由使用。
線程的最大棧空間很小,所以會有一些限制。如:
1.可被遞歸調用的最大方法數
每個方法都有其自己的棧幀,并會消耗整體的棧空間。
2.一個方法中最多可以使用的變量個數
所有的變量都會載入方法的棧幀中,并消耗一定的棧空間
3.視圖層級中可以嵌入的最大視圖深度
渲染復合視圖將在整個視圖層級樹中遞歸的調用layoutSubViews和drawRect方法。如果層級過深,可能會導致棧溢出。
堆大小
- 每個進程的所有線程共享同一個堆。一個應用可以使用的堆大小通常遠遠小于設備的RAM值。
- 應用不能控制分配給它的堆,只有操作系統才能管理堆。
- 使用NSString、載入圖片、創建或使用JSON/XML數據、使用視圖等都會消耗大量的堆內存。
與通過類創建的對象相關的所有數據都存放在堆中。
類可能包含屬性或值類型的實例變量(iVars),如 int、char或struct。但因為對象是在堆內創建的,所以它們只消耗堆內存。
當對象被創建并被賦值時,數據可能會從棧復制到堆。類似地,當值僅在方法內部使用時,它們也可能會被從堆復制到棧。
數據從棧復制到堆,是將一個局部變量(如方法所需的參數)賦值給一個對象的屬性時,必須被復制到堆中(如NSInteger)。但若屬性是Copy類型時,則值是被復制或克隆到堆中(也就是深拷貝與淺拷貝)。
而數據從堆復制到棧中,則就是將對象的屬性賦值給局部屬性。
每個線程都會被分配一個棧,而棧則是通過操作棧幀運行各個程序的方法,棧幀中保存著變量和其他調用方法的引用,還有操作數棧。
而直接定義的局部變量都是放在棧中的,對象的存儲放在堆中,全局和靜態的對象是放在數據段的靜態存儲區。
內存管理模型
如果一個對象正處于被持有的狀態,那它占用的內存就不能被回收。
當一個對象創建于某個方法的內部時,那該方法就持有這個對象了。
如果這個對象從方法返回,則調用者聲稱建立了持有關系。
這個值可以賦值給其他變量,對應的變量同樣會聲稱建立了持有關系。
如果一個對象沒有被引用,就會被釋放。內存被回收。
自動釋放對象
自動釋放對象讓你能夠放棄對一個對象的持有關系,但延后對它的銷毀。
當在方法中創建一個對象并需要將其返回時,自動釋放就顯得非常有用。
可用于MRC中管理對象的生命周期。
自動釋放池塊:@autoreleasepool
自動釋放池塊允許你放棄對一個對象的持有關系、但可避免它立即被回收。
在塊內創建的對象會在塊完成時被回收。
常用于從方法返回對象時。
可用來盡早地釋放其中的對象,從而使內存用量保持在較低的水平。
autorelease可以嵌套使用,塊中收到過autorelease消息的所有對象都會在autorelease塊結束時收到release消息。
而且是每個autorelease調用都會發送一個release消息。所以一個對象收到多次autorelease消息,也會收到多次release消息。
整個應用都是在一個autorelease塊中。
常用于:
1.創建了很多的臨時對象的循環。
創建了很多的臨時對象的循環時用自動釋放池,可避免一次性過多的持有對象。
2.創建一個線程時。
創建一個線程時,每個線程都將有它自己的autoreleasepool塊棧。主線程用自己的autoreleasepool啟動,因為他來自統一生成的代碼。然而,對于任何自定義的線程,必須創建自己的autoreleasepool。
也就是線程調用的方法內部,放一個autoreleasepool。
ARC的規則:開發人員不能直接進行內存管理。如不遵守,在編譯時期報錯而不是運行時崩潰。
1.不能實現或調用retain、release、autorelease或retainCount方法。對象、選擇器都不能用。所以,[obj release]或@selector(retain)是編譯時的錯誤。
2.dealloc方法可以實現,但不能調用它們。包括父類的也不能調用。
但是CoreFoundation類型的對象可以調用CFRetain、CFRelease等相關方法。
3.不能調用NSAllocateObject 和 NSDeallocateObject 方法。應使用alloc方法創建對象,運行時負責回收對象。
4.不能再C語言的結構體內使用對象指針。
5.不能在id類型和void * 類型之間自動轉換。如果需要,必須做顯示轉換。
6.不能使用NSAutoreleasePool,要替換使用autoreleasepool塊
7.不能使用NSZone內存區域。
8.屬性的訪問器名稱不能以new開頭,以確保與MRC的互操作性。
9.MRC和ARC可以混合使用。
ARC新增引用類型:弱引用
變量限定符
_strong、_weak、__unsafe_unretained、_autoreleasing
屬性限定符
strong、weak、assign(ARC之前,其是默認的持有關系限定符。ARC之后,表示__unsafe_unretained)、copy、retain (指定了__strong關系)、unsafe_unretained(制定了__unsafe_unretained)
assign和unsafe_unretained只進行值復制而沒有實質性的檢查,所以它們只應該用于值類型(Bool、NSInteger、NSUInteger,等等)。應避免用于引用類型,尤其是指針類型。如果NSString * 和 UIView *
僵尸對象:用于捕捉內存錯誤的調試功能。
但是開啟僵尸對象會占用大量的內存,所以應只在需要排查的時刻去開啟。
開啟:Product -> Scheme -> Edit Scheme 。選擇左側的Run, 然后在右側選取Diagnostics標簽頁。選中Enable Zombie Objects選項。
內存管理規則
1.你擁有所有自己創建的對象,如new、alloc、copy或mutableCopy
2.你可以用MRC中的Retain或者ARC中__strong 引用來擁有任何對象的而持有關系。
3.在MRC中,當不需要某個對象時須release該對象。而ARC無需特殊操作。持有關系會在對象失去最后的引用時被拋棄,如方法中的最后一行代碼。
4.一定不能拋棄原本并不存在持有關系的對象。
循環引用
循環引用就是A引用B,B引用A,進而導致內存泄露。
避免循環引用的方式:
1.對象不應該持有它的父對象,而是用weak引用指向它的父對象。
簡單說,a包含b,b屬于a,所以a中有屬性指向b,b中有屬性指向a。那么b指向 a應是weak引用。
2.作為必然的結果,一個層級體系中的子對象應該保留祖先對象。
3.連接對象不應該持有它們的目標對象。目標對象的角色是持有者。連接對象包括:
(1)使用委托的對象。委托應該被當作目標對象,即持有者。
(2)包含目標和action的對象,這是由上一條規則推理得到的。列如,UIButton會調用它的目標對象上的action方法。按鈕不應該保留它的目標。
(3)觀察者模式中被觀察的對象。觀察者就是持有者,并會觀察發生在被觀察對象上的變化。
像是delegate對象是weak,避免A持有B,B持有C,C持有A
4.使用專用的銷毀方法中斷引用
常見的循環引用
代理、Block、線程、計時器
我發現計時器方面的常被忽視,所以單獨說下計時器常見泄露情況
self.timer = [NSTimer scheduledTimerWithTimeInterval:120
target:self selector:@selector(updateFeed:) userInfo:nil repeats:YES];
}
self 持有timer , timer持有self 所以需要[self.timer invalidate]斷開循環
但是self不會被釋放,所以dealloc中銷毀是不會成功的,應該在跳轉其他頁面或運行停止時調用
鍵 - 值觀察
鍵 - 值觀察方法 addObserver:forKeyPath:options:context: 不會維持觀察對象、
被觀察對象及上下文對象的強引用。如有必要,你需要自行維護對它們的強引用。
簡單來說,鍵值觀察不會影響到對象的引用計數。
返回錯誤
當用某個方法接收NSError *參數,并在發生錯誤時填充錯誤變量,則必須使用 __autoreleasing 限定符:
NSError __autoreleasing *error;
接收參數: error:(NSError * __autoreleasing *) error
//處理
//如果發生錯誤
*error = [[NSError alloc] initWithDomain:@"transpose" code:123 userInfo:nil];
弱類型:id
應避免使用id,盡量用具體的類型取而代之。
單列
創建之后,會存在于整個程序的運行期間存活。所以若無必要,盡量不要用。有句俗話:占著茅坑不拉x
常用于日志、埋點、某些緩存操作、線程池或連接池等。
獲取一個對象的所有引用
通過關閉ARC,計算retain來獲取。
先禁用ARC,而后在自定義類中添加代碼:
#if !_has_feature(objc_arc) -(id) retain
{
DDLogInfo(@"%s %@", _PRETTY_FUNCTION, [NSThread callStackSymbols]); return [super retain];
} #endif
這段代碼記錄retain方法的調用情況,將調用棧打印出來。因此會獲得調用次數 與 精確的調用明細。
但是就目前的個人接觸來說,可以直接使用僵尸對象檢測。而且Block等內存泄露的情況下,XCode也會有警告提示,只要注意一下、不是故意的,通常會避免。
實踐總結
1.避免大量的單列。
2.對子對象使用__strong
3.對父對象使用__weak
4.對使引用圖閉合的對象(如委托、Block)使用__weak
5.對數值屬性(NSInteger、SEL、CGFloat等)而言,使用assign限定符
6.對于塊屬性,使用copy限定符
7.當聲明使用NSError ** 參數的方法時,使用__autoreleasing,并要注意用正確的語法:NSError * __autoreleasing *
8.避免在塊內直接引用外部的變量。在外部對self使用weak ,內部用self
9.注意以下清理:銷毀計時器、移除觀察者、解除回調(強引用的委托置為nil)
生產環境的內存使用
要想在不同的情境中分析應用,可以使用埋點。
尤其是內存超出閾值時(如規定數值,或收到內存警告時)配合一些輔助定位信息(如內存的使用及統計信息)。
或者是一定間隔后在本地記錄,上報給服務器。
能耗
除 CPU 外,耗電量高、值得關注的硬件模塊還包括:網絡硬件、藍牙、GPS、麥克風、加 速計、攝像頭、揚聲器和屏幕。
應用計算得越多,消耗的電量就越多。在完成相同的基本操作時,老一代的設備會消耗更多的電量。
計算量的消耗取決于不同的因素:
1.對數據的處理(如:文本格式化)。
2.待處理的數據大小 — 更大的顯示屏允許軟件在單個視圖中展示更多的信息,但這也意味著要處理更多的數據。
3.處理數據的算法和數據結構。
4.執行更新的次數,尤其是在數據更新后,觸發應用的狀態或UI進行更新(應用收到的推送通知也會導致數據更新,如果此時用戶正在使用應用,你還需要更新UI)。
實踐建議:
1.針對不同的情況選擇優化的算法。(我的天。。)
2.如果應用從服務器接收數據,盡量減少需要在客戶端進行的處理。
3.優化靜態編譯(AOT)處理。
(動態編譯處理的缺點在于它會強制用戶等待操作完成。但是激進的AOT處理則會導致計算資源的浪費。需要根據應用和設備選擇精確定量的AOT處理。)
4.分析電量消耗。測量用戶設備上的電量消耗,而后找到高消耗想法降低。
網絡
蜂窩無線系統(LTE、4G、3G等)對電量的消耗遠大于WiFi信號。
在進行網絡操作之前,先檢查合適的網絡連接是否可用。
持續監視網絡的可用性,并在鏈接狀態發生變化時給予適當的反饋。
蘋果公司提供了示例代碼(http://apple.co/1Q3gRKL),以檢查和監聽網絡狀態的變化
若使用CocoaPods,可使用Reachabilitypod
NSOperationQueue 不會暫停或掛起任何執行中的操作。一個掛起的隊列僅僅意味著 后續操作在其恢復之前不會被執行。
操作只有完成后才會從隊列中移除,所以要先啟動操作用于完成執行。而隊列被掛起不會啟動任何新的操作,所以也不會移除任何正在排隊且未被執行的操作(包括那些已經取消的操作)。
使用基于隊列的網絡請求以避免服務器被多個同時發起的請求所轟炸。至少使用兩個隊列:一個用于通常不是很關鍵的大量圖片下載。 一個用于關鍵數據的請求。
定位管理器和GPS
定位服務需要大量的電量。
使用GPS計算坐標需要確定兩點信息:
1.時間鎖
每個GPS衛星每毫秒廣播唯一一個1023位隨機數,因而數據傳播速率是1.024Mbits/s。GPS的接收芯片必須正確的與衛星的時間鎖槽對齊。
2.頻率鎖
GPS接收器必須計算由接收器與衛星的相對運動導致的多普勒偏移帶來的信號誤差。
通常情況下,鎖定一顆衛星至少需要30秒。必須鎖定接受范圍內的所有衛星。確定的衛星越多,取得的定位坐標就越精確。
計算坐標會不斷地使用CPU 和 GPS的硬件資源,因此會迅速的消耗電池電量。
最佳初始化:
1.distanceFilter,設備的移動超過了最小距離,距離過濾器就會導致管理器對委托對象的locationManager: didUpdateLocations: 事件通知發生變化。該距離使用公知單位(米)。
它并不會有助于減少GPS接收器的使用,但會影響應用的處理速度,從而直接減少CPU的使用
2.desiredAccuracy
精度參數的使用直接影響了使用天線的個數,進而影響了對電池的消耗。精度級別的選取取決于應用的具體用途。按降序排列,精度由以下常量定義。
- kCLLocationAccuracyBestForNavigation:用于導航的最佳精度級別
- kCLLocationAccuracyBest:設備可能達到的最佳精度級別
- kCLLocationAccuracyNearestTenMeters:精度接近10米。如果對用戶所走的每一米并不感興趣,不妨使用這個值。
- kCLLocationAccuracyHudredMeters:精度接近100米(計算距離時,值需要乘以100米)
- kCLLocationAccuracyKilometer:精度在千米范圍。這在粗略測量兩個距離數百千米的興趣點時非常有用。(列如從中國的北京到日本的東京)
- KCLLocationAccuracyThreeKilometers:精度在三千米范圍內。在距離真的很遠時使用這個值(好比 北極到南極)
距離過濾器只是軟件層面的過濾器(但會影響數據的處理,進而直接影響到CPU使用),而精度級別會影響物理天線的使用。
當委托的回調方法locationManager:didUpdateLocations:被調用時,使用距離范圍更廣的過濾器只會影響間隔。另一方面,更高的精度級別意味著更多的活動天線,這會消耗更多的能量。
關閉無關緊要的特性
判斷何時需要跟蹤位置的變化。在需要跟蹤時調用startUpdatingLocation方法,無需跟蹤時調用stopUpdatingLocation方法。
假設用戶需要用一個消息類的應用與朋友分享位置。如果該應用只是發送城市的名稱,則只需要一次性的獲取地理位置信息,然后就可以通過調用stopUpdatingLocation關閉位置跟蹤。在一定的時間間隔后可以再次開啟定位。你可以設置固定的間隔(如30S),也可以動態的計算時間間隔(如 根據之前獲取的坐標和速度,估算穿過城市的時間上限)。
當應用在后臺運行或用戶沒有與別人聊天時,也應該關閉位置跟蹤。
向終端用戶提供關閉非必要功能的選項是一個更好的解決方案。
只在必要時使用網絡
為了提高電量的使用效率,iOS總是盡可能地保持無線網絡關閉。當應用需要建立網絡連接時,iOS會利用這個機會向后臺應用分享網絡會話,以便一些優先級的事件能夠被處理,如推送,收取電子郵件等。
關鍵在于每當應用建立網絡連接時,網絡硬件都會在連接完成后多維持幾秒的活動時間。每次集中的網絡通信都會消耗大量的電量。
所以為減輕這個危害,應該定期集中短暫的使用網絡,而不是持續著保持活動的數據流。
后臺定位服務
CLLocationManager提供了一個替代的方法來監聽位置的更新。startMonitoringSignificantLocationChanges可以幫助你在更遠的距離跟蹤運動。精確的值由內部決定,且與distanceFilter無關。
使用這一模式可以在應用進入后臺后繼續跟蹤運動。
典型的做法是在應用進入后臺時執行startMonitoringSignificantLocaitonChanges方法,而當應用回到前臺時執行startUpdatingLocation。
NSTimer、NSThread和定位服務
當應用位于后臺時,任何定時器或線程都會掛起。但如果你在應用位于后臺狀態時申請了定位,那么應用會在每次收到更新后被短暫喚醒。在此期間,線程和計時器都會被喚醒。
若在此期間做了任何網絡操作,則會啟動所有相關的天線。想控制這種情況,最佳選擇使用NSURLSession類。
在應用關閉后重啟
在其他應用需要更多資源時,后臺應用可能會被關閉。
在此情況下,一旦發生位置變化,應用會被重啟,因而需重新初始化監聽過程。
若發生這種情況,application:didFinishLaunchingWithOptions:方法會收到鍵值為UIApplicationLaunchOptionsLocationKey的條目。(launchOptions[UIApplicationLaunchOptionsLocationKey]有值)
屏幕
屏幕越大越費電(廢話。)
動畫
明智的使用動畫,列如前臺時使用動畫,后臺暫停。
視頻播放
視屏播放時強制保持屏幕常亮。使用UIApplication 對象的 idleTimerDisabled屬性來實現這個目的。一旦設置為 YES,它會阻止屏幕休眠,從而實現常亮。也可以通過響應應用的通知來釋放和獲取鎖。
多屏幕
設備連接外部顯示設備,除了系統默認行為外(顯示設備投影) 還可以做別的更多操作。
如電影播放或運行動畫,從設備屏幕挪到外部屏幕。
處理方式:
1.啟動器件檢測屏幕數量,若大于1,則進行切換。
2.監聽屏幕在連接和斷開時的通知,若有新屏幕加入則切換,若外部屏幕都移除則恢復到默認顯示。
http://apple.co/1jauUnu 蘋果公司提供的外部屏幕使用示列
在一個屏幕上顯示卻在另一個屏幕上控制,感覺挺笨拙但實際也不錯。尤其對于播放、暫停、恢復這種標準操作。
其他硬件:
當應用進入后臺時,應釋放對這些硬件的鎖定:
藍牙、相機、揚聲器(除非是音樂類)、麥克風
別太死板。
電池電量與代碼感知
可以通過UIDevice實例獲取batteryLevel 和 batteryState(充電狀態)
使用電量級別和充電狀態進行條件處理,參數是電量百分比
-(BOOL)shouldProceedWithMinLevel:(NSUInteger)minLevel {
UIDevice *device = [UIDevice currentDevice];
device.batteryMonitoringEnabled = YES;
UIDeviceBatteryState state = device.batteryState; if(state == UIDeviceBatteryStateCharging ||
state == UIDeviceBatteryStateFull) { //充電或電池已充滿
return YES; }
NSUInteger batteryLevel = (NSUInteger) (device.batteryLevel * 100); //獲取當前電量,范圍是0.00~1.00
if(batteryLevel >= minLevel) {
return YES; }
return NO; }
類似的,還可以獲取到CPU使用情況
應用對 CPU 的使用率
-(float)appCPUUsage { kern_return_t kr;
task_info_data_t info;
mach_msg_type_number_t infoCount = TASK_INFO_MAX;
kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)info, &infoCount);
if (kr != KERN_SUCCESS) { return -1;
}
thread_array_t thread_list; mach_msg_type_number_t thread_count; thread_info_data_t thinfo; mach_msg_type_number_t thread_info_count; thread_basic_info_t basic_info_th;
kr = task_threads(mach_task_self(), &thread_list, &thread_count); if (kr != KERN_SUCCESS) {
}
return -1; float tot_cpu = 0;
}
int j;
for (j = 0; j < thread_count; j++) {
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
(thread_info_t)thinfo, &thread_info_count); if (kr != KERN_SUCCESS) {
return -1;
basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) { tot_cpu += basic_info_th->cpu_usage /
(float)TH_USAGE_SCALE * 100.0;
} }
vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
return tot_cpu;
}
分析電量使用
使用專業點的設備, Monsoon Solutions 的電源監控器
使用方法:
(1) 拆開 iOS 設備的外殼,找到電池后面的電源針腳。 (2) 連接電源監控器的設備針腳。
(3) 運行應用。
(4) 測量電量消耗。
最佳實踐
1.盡可能晚的使用硬件,并在任務完成時立即結束使用。
2.進項密集型任務前,檢查電池電量和充電狀態
3.電量低時,提示用戶是否確定要執行任務,并在用戶同意后再執行。
4.可以提供讓用戶定義電量的閾值,以便某些操作前(如執行密集型操作)提示用戶
小結
低電量也是挺重要的,畢竟用戶不是每次都攜帶移動電源的。
任務復雜性不能降低(處理圖片或畫圖等),不妨提供對電池電量保持敏感的方案并在適當的時機提示用戶。