iOS 面試總結


APP崩潰

啟動秒退

在新 iOS 上正常的應用,到了老版本 iOS 上秒退最常見原因是系統動態鏈接庫或Framework無法找到。這種情況通常是由於 App 引用了一個新版操作系統里的動態庫(或者某動態庫的新版本)或只有新 iOS 支持的 Framework,而又沒有對老系統進行測試,於是當 App 運行在老系統上時便由於找不到而秒退。解決辦法是等開發人員發現這個問題后升級程序,或由用戶自行升級其操作系統。

還有一種常見的秒退是程序在升級時,修改了本地存儲的數據結構,但是對用戶既存的舊數據沒有做好升級,結果導致初始化時因為無法正確讀取用戶數據而秒退。這類問題通常只需刪除程序后重新安裝一遍就能解決。但缺點是用戶的既存數據會丟失——就算有備份可能也無濟於事,因為備份下來的舊數據還是無法被正確升級。

還有一類秒退或是用到 App 里某個功能后必退的原因,是開發時用到了只有新版操作系統才支持的某個方法,而又沒有對該方法是否存在於老系統中做出判斷。例如程序啟動時用到了 Game Center,而沒有判斷用戶的機器是否支持 Game Center,於是就秒退了。

訪問的數據為空或者類型不對

這類情況是比較常見的,后端傳回了空數據,客戶端沒有做對應的判斷繼續執行下去了,這樣就產生了crash。或者自己本地的某個數據為空數據而去使用了。還有就是訪問的數據類型不是期望的數據類型而產生崩潰。比如,我期望服務端返回string類型,但是后台給我返回的是NSNumber類型,那么我操作時候用到的是string的方法。結果因為找不到對應的方法而崩潰。解決辦法:1、服務端都加入默認值,不返回空內容或無key。或者是在客戶端進行非空判斷。2、對容易出錯的地方提前進行類型判斷。

點擊事件方法處理不當

這類情況比較常見,比如我的點擊事件需要對傳入的參數做處理,但是點擊時,我傳入的對象類型或者傳入為空,等到參數進行處理的時候,由於方法不當,產生crash。

數組越界

當客戶端嘗試對數組中的數據進行操作的時候,數組為空或者所做的操作index 超過數組本身范圍,就會引起崩潰。

下拉刷新時崩潰

使用下拉刷新時,如果下拉的距離長了就會崩潰。原因是,在下拉刷新請求數據之前就將本地數組清空了。分析,下拉刷新的邏輯:1、下拉 2、下拉達到臨界值時觸發網絡請求 3、等待數據加載到本地以后才更新datasource 4、tableview reloadData。 如果先清空數組再下拉請求,后果就是往下拉的距離超過一個 cell 的高度時,table view 的幾個委托方法就會被調用,由於 data source 已經被清空,造成錯誤的內存訪問(包括數組越界,訪問已銷毀的對象)導致 crash。

操作了不該操作的對象、野指針

iOS中有空指針和野指針兩種概念。

空指針是沒有存儲任何內存地址的指針。如Student s1 = NULL;和Student s2 = nil;

而野指針是指指向一個已刪除的對象("垃圾"內存既不可用內存)或未申請訪問受限內存區域的指針。野指針是比較危險的。因為野指針指向的對象已經被釋放了,不能用了,你再給被釋放的對象發送消息就是違法的,所以會崩潰。

野指針訪問已經釋放的對象crash其實不是必現的,因為dealloc執行后只是告訴系統,這片內存我不用了,而系統並沒有就讓這片內存不能訪問。

所以野指針的奔潰是比較隨機的,你在測試的時候可能沒發生crash,但是用戶在使用的時候就可能發生crash了。

注意:arc環境比非arc環境更少出現野指針。

1、對象釋放后內存沒被改動過,原來的內存保存完好,可能不Crash或者出現邏輯錯誤(隨機Crash)。

2、對象釋放后內存沒被改動過,但是它自己析構的時候已經刪掉某些必要的東西,可能不Crash、Crash在訪問依賴的對象比如類成員上、出現邏輯錯誤(隨機Crash)。

3、對象釋放后內存被改動過,寫上了不可訪問的數據,直接就出錯了很可能Crash在objc_msgSend上面(必現Crash,常見)。

4、對象釋放后內存被改動過,寫上了可以訪問的數據,可能不Crash、出現邏輯錯誤、間接訪問到不可訪問的數據(隨機Crash)。

對象釋放后內存被改動過,寫上了可以訪問的數據,但是再次訪問的時候執行的代碼把別的數據寫壞了,遇到這種Crash只能哭了(隨機Crash,難度大,概率低)!!

5、對象釋放后再次release(幾乎是必現Crash,但也有例外,很常見)

內存處理不當

用instruments排查內存泄露問題

主線程UI長時間卡死,被系統殺掉

主線程被卡住是非常常見的場景,具體表現就是程序不響應任何的UI交互。這時按下調試的暫停按鈕,查看堆棧,就可以看到是到底是死鎖、死循環等,導致UI線程被卡住。

多線程之間切換訪問引起的crash

多線程引起的崩潰大部分是因為使用數據庫的時候多線程同時讀寫數據庫而造成了crash。

內存緊張

這個現在很少遇到了。

ARC內存泄漏

block系列

在 ARC 下,當 block 獲取到外部變量時,由於編譯器無法預測獲取到的變量何時會被突然釋放,為了保證程序能夠正確運行,讓 block 持有獲取到的變量,向系統顯明:我要用它,你們千萬別把它回收了!然而,也正因 block 持有了變量,容易導致變量和 block 的循環引用,造成內存泄露!

對於 block 中的循環引用通常有兩種解決方法:

1、將對象置為 nil ,消除引用,打破循環引用;

(這種做法有個很明顯的缺點,即開發者必須保證 _networkFetecher = nil; 運行過。若不如此,就無法打破循環引用。

但這種做法的使用場景也很明顯,由於 block 的內存必須等待持有它的對象被置為 nil 后才會釋放。所以如果開發者希望自己控制 block 對象的生命周期時,就可以使用這種方法。)

2、將強引用轉換成弱引用,打破循環引用;

(__weak __typeof(self) weakSelf = self;如果想防止 weakSelf 被釋放,可以再次強引用 __typeof(&*weakSelf) strongSelf = weakSelf;代碼 __typeof(&*weakSelf) strongSelf 括號內為什么要加 &* 呢?主要是為了兼容早期的 LLVM

block 的內存泄露問題包括自定義的 block,系統框架的 block 如 GCD 等,都需要注意循環引用的問題。

有個值得一提的細節是,在種類眾多的 block 當中,方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API ,如

1
2
- enumerateObjectsUsingBlock:
- sortUsingComparator:

這一類 API 同樣會有循環引用的隱患,但原因並非編譯器做了保留,而是 API 本身會對傳入的 block 做一個復制的操作。

delegate系列

1
@property (nonatomic, weak) id  delegate;

說白了就是循環使用的問題,假如我們是寫的strong,那么 兩個類之間調用代理就是這樣的啦

1
2
3
4
5
6
7
8
9
10
BViewController *bViewController = [[BViewController alloc] init];
bViewController.delegate = self;  //假設 self 是AViewController
[self.navigationController pushViewController:bViewController animated:YES];
 
/**
  假如是 strong 的情況
     bViewController.delegate ===> AViewController (也就是 A 的引用計數 + 1)
     AViewController 本身又是引用了  ===> delegate 引用計數 + 1
  導致: AViewController  Delegate ,也就循環引用啦
  */
  • Delegate創建並強引用了 AViewController;(strong ==> A 強引用、weak ==> 引用計數不變)

所以用 strong的情況下,相當於 Delegate 和 A 兩個互相引用啦,A 永遠會有一個引用計數 1 不會被釋放,所以造成了永遠不能被內存釋放,因此weak是必須的。

performSelector 系列

performSelector 顧名思義即在運行時執行一個 selector,最簡單的方法如下

1
- (id)performSelector:(SEL)selector;

這種調用 selector 的方法和直接調用 selector 基本等效,執行效果相同

1
2
[object methodName];
[object performSelector:@selector(methodName)];

但 performSelector 相比直接調用更加靈活

1
2
3
4
5
6
7
8
9
SEL selector;
if  ( /* some condition */ ) {
     selector = @selector(newObject);
else  if  ( /* some other condition */ ) {
     selector = @selector(copy);
else  {
     selector = @selector(someProperty);
}
id ret = [object performSelector:selector];

這段代碼就相當於在動態之上再動態綁定。在 ARC 下編譯這段代碼,編譯器會發出警告

1
warning: performSelector may cause a leak because its selector is unknow [-Warc-performSelector-leak]
  • 正是由於動態,編譯器不知道即將調用的 selector 是什么,不了解方法簽名和返回值,甚至是否有返回值都不懂,所以編譯器無法用 ARC 的內存管理規則來判斷返回值是否應該釋放。因此,ARC 采用了比較謹慎的做法,不添加釋放操作,即在方法返回對象時就可能將其持有,從而可能導致內存泄露。

    以本段代碼為例,前兩種情況(newObject, copy)都需要再次釋放,而第三種情況不需要。這種泄露隱藏得如此之深,以至於使用 static analyzer 都很難檢測到。如果把代碼的最后一行改成

    [object performSelector:selector];

    不創建一個返回值變量測試分析,簡直難以想象這里居然會出現內存問題。所以如果你使用的 selector 有返回值,一定要處理掉。

  • 還有一種情況就是performSelector的延時調用[self performSelector:@selector(method1:) withObject:self.myView afterDelay:5];,performSelector關於內存管理的執行原理是這樣的,當執行[self performSelector:@selector(method1:) withObject:self.myView afterDelay:5];的時候,系統將myView的引用計數加1,執行完這個方法之后將myView的引用計數減1,而在延遲調用的過程中很可能就會出現,這個方法被調用了,但是沒有執行,此時myView的引用計數並沒有減少到0,也就導致了切換場景的dealloc方法沒有被調用,這也就引起了內存泄漏。

NSTimer

NSTimer會造成循環引用,timer會強引用target即self,在加入runloop的操作中,又引用了timer,所以在timer被invalidate之前,self也就不會被釋放。

所以我們要注意,不僅僅是把timer當作實例變量的時候會造成循環引用,只要申請了timer,加入了runloop,並且target是self,雖然不是循環引用,但是self卻沒有釋放的時機。如下方式申請的定時器,self已經無法釋放了。

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(commentAnimation) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

解決這種問題有幾個實現方式,大家可以根據具體場景去選擇:

  • 增加startTimer和stopTimer方法,在合適的時機去調用,比如可以在viewDidDisappear時stopTimer,或者由這個類的調用者去設置。

  • 每次任務結束時使用dispatch_after方法做延時操作。注意使用weakself,否則也會強引用self。

1
2
3
4
5
6
7
- (void)startAnimation
{
     WS(weakSelf);
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
         [weakSelf commentAnimation];
     });
}
  • 使用GCD的定時器,同樣注意使用weakself。

1
2
3
4
5
6
7
WS(weakSelf);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
   [weakSelf commentAnimation];
});
dispatch_resume(timer);

內存泄漏的檢測方式

1.靜態分析

使用XCode分析功能,Product->Analyze

使用靜態檢測可以檢查出一些明顯的沒有釋放的內存,包括NSObject和CF開頭的內存泄漏,最常見問題有2種,這些問題都不復雜,需要的是細心:

  • MRC的文件,經常遺漏release或者autorelease。

  • C方式申請的內存,忘記釋放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1
static inline NSString* iphone_device_info(){
   size_t size;
   sysctlbyname( "hw.machine" , NULL, &size, NULL, 0);
   char *machine = (char*)malloc(size);
   sysctlbyname( "hw.machine" , machine, &size, NULL, 0);
   NSString *platform = [NSString stringWithCString:machine encoding:NSASCIIStringEncoding];
   ...
}
//2
if  (alpha != 1) {
   CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
   CGColorRef color = CGColorCreate(colorSpaceRef, (CGFloat[]){255, 255, 255, 0.3});
   [btn.layer setBorderColor:color];
}

不過在修改時應該注意:

  • 這些場景是否真的泄漏了,以免重復釋放。

  • 注意該文件是MRC還是ARC,需要不同的內存處理方式。

  • 如果是C申請的內存,注意new delete, malloc free的配對處理。

比如我們的代碼中會遇到這樣的問題。

1
2
3
4
if  ([self.itemOutput hasNewPixelBufferForItemTime:currentTime]) {
       [self displayPixelBuffer:[self.itemOutput copyPixelBufferForItemTime:currentTime itemTimeForDisplay:NULL]];
       [_program useGlProgram];
   }

進行靜態檢測時會報copyPixelBufferForItemTime內存泄漏,copy后的對象需要進行釋放,可事實上,在“displayPixelBuffer”函數中已經對傳入對內存進行了釋放,我們姑且不論這樣對寫法是否合理,只是切記在修改時注意結合上下文處理需要釋放的內存。

2.動態檢測 使用instruments

在Allocation中我們主要關注的是Persistent和Persistent Bytes,分別表示當前時間段,申請了但是還沒釋放的內存數量和大小。

記住當前這兩個值,然后進入某個新頁面,退出該頁面,觀察這兩個值是否增加。需要注意的是,由於有些圖片調用本身是有緩存的,如果是用SDWebImage管理,則網絡圖片都會緩存在內存中。因此退出頁面后內存有增加是正常的,而且還有些單例的內存也是不會釋放的,我們可以再次進入同一個頁面,在圖片都加載過的情況下,反復進入退出查看內存狀況,如果持續增加,則說明有泄漏。

詳細使用:

Xcode之Instruments使用

第三方工具MLeaksFinder

1、使用簡單,不侵入業務邏輯代碼,不用打開 Instrument

2、不需要額外的操作,你只需開發你的業務邏輯,在你運行調試時就能幫你檢測

3、內存泄露發現及時,更改完代碼后一運行即能發現(這點很重要,你馬上就能意識到哪里寫錯了)

4、精准,能准確地告訴你哪個對象沒被釋放

具體特點,原理和集成方式可以參考如下博客的內容:

MLeaksFinder:精准 iOS 內存泄露檢測工具

MLeaksFinder 新特性

界面卡頓

一、離屏渲染

OpenGL中,GPU屏幕渲染有以下兩種方式:

一.On-Screen Rendering

意為當前屏幕渲染,指的是GPU的渲染操作是在當前用於顯示的屏幕緩沖區中進行。當前屏幕渲染是不需要額外創建額外的緩存,也不需要開啟新的上下文,相較於離屏渲染,性能更好。

但是受當前屏幕渲染的局限因素限制(只有自身上下文,屏幕緩存有限等),很多圖形渲染,當前屏幕渲染是解決不了的,這時必須使用到離屏渲染。

二.Off-Screen Rendering

離屏渲染,指的是GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。

特殊的離屏渲染:CPU渲染

如果我們重寫了drawRect方法,並且使用任何Core Graphics的技術進行了繪制操作,就涉及到了CPU渲染,整個渲染過程有CPU在APP內同步的完成,渲染得到的bitmap(位圖)最后再交由GPU用於顯示。

  • 注意:CoreGraphics通常是線程安全的,所以可以進行一步繪制,顯示的時候再放回主線程,一個簡單的異步繪制過程大致如下

1
2
3
4
5
6
7
8
9
10
11
-(void)display {
  dispatch_async(backgroundQueue, ^{
      CGContextRef ctx = CGBitmapContextCreate(...);
      // draw in context...
      CGImageRef img = CGBitmapContextCreateImage(ctx);
      CFRelease(ctx);
      dispatch_async(mainQueue, ^{
          layer.contents = img;
      });
  });
}

普通的離屏渲染

相比於當前屏幕渲染,離屏渲染的代價是很高的,主要體現在兩個方面:

  • 1 創建新的緩沖區

想要進行離屏渲染,首先要創建一個新的緩沖區。

  • 2 上下文切換

離屏渲染的整個過程,需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上有需要將上下文回家從離屏切換到當前屏幕。而上下文環境的切換時要付出很大代價的。

設置了以下屬性時,都會觸發離屏繪制:

1.shouldRasterize(光柵化)

2 masks(遮罩)

3 shadows (陰影)

4 edge antialiasing (抗鋸齒)

5 group opacity (不透明)

其中shouldRasterize(光柵化)是比較特殊的一種:

光柵化概念:將圖轉化為一個個柵格組成的圖像。

光柵化特點:每個元素對應幀緩沖區中的一像素。

shouldRasterize = YES 在其他屬性觸發離屏渲染的同時, 會將光柵化后的內容緩存起來,如果對應的layer 及其sublayers沒有發生改變,在下一幀的時候可以直接復用。shouldRasterize = YES, 這將隱式的創建一個位圖,各種陰影遮罩等效果也會保存到位圖中並緩存起來,從而減少渲染的頻度(不是矢量圖)。

相當於光柵化是把GPU的操作轉到CPU上了,生成位圖緩存,直接讀取復用。

當你使用光柵化時,你可以開啟"Color Hits Green And Misses Red"來檢查該場景下光柵化操作是否是一個號的選擇。綠色表示緩存被復用,紅色表示緩存在被重復創建。

如果光柵化的層變紅的太頻繁那么光柵化對優化可能沒有多少用處,位圖緩存從內存中刪除又重新創建的太過頻繁,紅色表明緩存重建的太遲。可以針對性的選擇某個較小而深的層結構進行光柵化,來嘗試減少渲染時間。

注意:

對於經常變動的內容, 這個時候不要開啟,否則會造成性能的浪費。

例如我們日常經常打交道的TableViewCell, 因為TableViewCell 的重繪是很頻繁的(因為cell的復用),如果cell的內容不斷變化,則cell需要不斷重繪,如果此時設置了cell.layer可光柵化,則會造成大量的離屏渲染,降低圖形性能。

為什么使用離屏渲染

當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制,所以就需要屏幕外渲染被喚起。

屏幕外渲染並不意味着軟件繪制,但是它意味着圖層必須在被顯示在一個屏幕外上下文中被渲染(不論死CPU還是GPU)。

所以當使用離屏渲染的時候會很容易造成性能消耗,因為在OpenGL里離屏渲染會單獨在內存中創建一個屏幕外緩沖區並進行渲染,而屏幕外緩沖區跟當前屏幕緩沖區上下文切換也是很耗性能的。

如何選擇

擺在我們面前的有三個選擇:當前屏幕渲染、離屏渲染、CPU渲染。該使用哪個呢?

  • 盡量使用當前屏幕渲染

    鑒於離屏渲染、CPU渲染可能帶來的性能問題,一般情況下,我們要盡量使用當前屏幕渲染。

  • 離屏渲染、CPU渲染

    由於GPU的浮點運算能力比CPU強,CPU渲染的效率可能不如離屏渲染;但如果僅僅是實現一個簡單的效果,直接使用CPU渲染的效率又可能比離屏渲染好,畢竟普通的離屏渲染要涉及到緩沖區創建和上下文切換等耗時操作。普通的離屏繪制是發生在繪制服務(是獨立的處理過程)並且同時通過GPU執行。當OpenGL的繪制程序在繪制每個layer的時候,有可能因為包含多子層級關系而必須停下來把他們合成到一個單獨的緩存里。你可能認為GPU應該總是比CPU牛逼一點兒,但是在這里我們還是需要慎重的考慮一下。因為對GPU來說,當當前屏幕到離屏上下文環境的來回切換,代價是非常大的。因為對一些簡單的繪制過程來說,這個過程有可能用CoreGraphics,全部用CPU來完成反而會比GPU做的更好,所以如果你正在嘗試處理一些復雜的層級,並且在猶豫到底用-[CALayer setShouldRasterize:]還是通過CoreGraphics來繪制層級上的所有內容,唯一的方法就是測試並且進行權衡。

Instuments監測離屏渲染

Instruments的Core Animation工具中又幾個和離屏渲染相關的檢查選項:

  • Color offscreen-Rendered Yellow

    開啟后會把那些需要離屏渲染的圖層高亮成黃色,這就意味着黃色圖層可能存在性能問題。

  • Color Hits Green and Misses Red

    如果shouldRasterize 被設置成YES,對應的渲染結果會被緩存,如果圖層是綠色,就表示這些緩存被復用;如果是紅色就表示緩存會被重復創建,這就表示該出存在性能問題了。

    iOS 版本上的優化

    iOS 9.0之前 UIImageView跟UIButton設置圓角都會觸發離屏渲染

    iOS 9.0 之后 UIButton設置圓角會觸發離屏渲染,而UIImageView里png圖片設置圓角不會再觸發離屏渲染了,如果設置其他陰影效果之類的還是會觸發離屏渲染。

    離屏渲染總結

  • 1.盡量使用當前屏幕渲染,能不適用離屏渲染則盡量不用,你應當盡量避免使用layer的border、corner、shadow、mask等技術。

  • 2.必須離屏渲染時,相對簡單的視圖應該使用CPU渲染,相對復雜的視圖則使用一般的離屏渲染。

二、tableview滑動卡頓

  • 1."讓出"主線程,讓主線程減負。所謂"讓出"主線程,指的是不要什么操作都放在主線程里。放在主線程中的一般都是視圖相關的操作,比如添加子視圖、更新子視圖、刪除子視圖等。像其他的圖片下載、數據請求這樣的操作盡量不要放在主線程中進行。

1
2
3
4
5
6
7
8
9
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
   //處理一些跟當前視圖沒關系的事情  
   //...  
 
   //只用來操作與當前視圖有關系的事情,比如:刷新tableView  
   dispatch_async(dispatch_get_main_queue(), ^{  
       [tableView reload];  
   });  
});
  • 2.正確重用cell。正確重用cell不僅僅要重用cell視圖,還需要好好重用cell的子視圖。

1
2
3
4
5
6
static NSString *Identifier = @ "WeatherCell" ;  
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:Identifier];  
if  (!cell) {  
   cell = [[UITableViewCell alloc] initWithStyle:   
                                 reuseIdentifier:]   
}

上面的代碼在有cell可重用的時候,不會再創建新的cell,但是下面的一句話基本上會讓重用粉身碎骨。

1
2
3
4
//清空子視圖  
[cell.contentView.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOLBOOL *stop) {  
    [obj removeFromSuperview];  
}];

上面這段代碼之所以會出現,原因眾所周知:cell重用時會出現重疊。"解決"了重疊問題,那么新問題來了,重新創建視圖既消耗內存還占用時間,嚴重會出現滑動出現卡頓現象,而且都刪除了重建還能叫重用么?

三、其他解決卡頓辦法

  • imageView 盡量設置為不透明

    opaque盡量設為YES,當imageView的opaque設置為YES的時候其alpha的屬性就會無效,imageView的半透明取決於其圖片半透明或者imageView本身的背景色的合成的圖層view是半透明的。如果圖片全部不是半透明就不會觸發圖層的blend操作,整個圖層就回不透明,如果疊加的圖片有出現半透明的,就會立馬觸發圖層的blend操作,整個圖層不透明。

    opaque設為NO的話,當opaque為NO的時候,圖層的半透明取決於圖片和其本身合成的圖層為結果。

  • 背景色盡可能設為alpha值為1

    當某一塊圖層的alpha和其superView的背景色alpha不一樣的時候會觸發alpha合成操作,這是一項看似簡單但是卻非常消耗CPU性能的操作。

  • UIView的背景色設置

    UIview的背景色盡量不要設置為clearColor,這樣也會觸發alpha疊加,在tableview滑動時候是非常消耗性能的。子視圖的背景色盡可能設置成其superView的背景色,這樣圖層合成的時候就不會觸發blend操作了。

  • 最好不適用帶有alpha通道的圖片,如果有alpha盡量讓美工取消alpha通道。

    alpha通道 是透明的意思。

  • cell上layer盡量避免使用圓角

    在工作中關於滑動界面我們會時常遇到cell行設置頭像為圓角等需求,這時候我們盡量避免使用layer.masksToBounds,因為這會觸發離屏渲染(上面有講離屏渲染)。

    優化UIImageView圓角方式 用貝塞爾曲線,不會觸發離屏渲染:

1
2
3
4
5
6
7
8
9
10
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@ "image" ];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
                          cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
  • 優化圖片的加載方式

1
2
UIImage Image = [UIImage imageNamed:@ "helloworld"
UIImage Image = [UIImage imageWithContentOfFile:@ "helloworld" ];

圖片的兩種加載方式:

第一種:當我們經常需要這張圖片並且僅僅是小圖的時候,我們可以使用此方式加載圖片。這種方式是把圖片緩存在圖片緩沖區,當我們使用的時候會通過圖片的名字也就是通過key的方式去查找圖片在緩存區的內存地址。

當我們使用很多圖片的時候系統就會開辟很多內存來存儲圖片。

第二種:當我們使用工程里面的一張大圖並且使用次數很少甚至為1次的時候,我們優先會采用這種方式來加載圖片,這種方式當使用完圖片的時候會立即丟棄釋放資源,所以對性能不會帶來負擔。

  • 盡量延遲圖片的加載

    當我們在滑動頁面的時候,尤其是對於那種布局特別復雜的cell,滑動的時候不要加載圖片,當滑動停止的時候再進行圖片的加載。

    我們都知道不管是tableview還是scrollview在滾動的時候需要顯示東西都是通過runloop去拿。當滾動的時候runloop會處於NSRunLoopTrackingMode的模式,我們可以通過一個主線程隊列dispatch_after或者selfPerformSeletor設置runloop的模式為NSDefaultRunLoopMode模式,就可以做到停止滾動再加載圖片。注:其實嚴格意義上selfPerformSelector的事件就是在主線程隊列中等待。

  • 使用懶加載,即 需要時再加載。

  • 最終要的是避免阻塞主線程。

    讓圖片的繪制、圖片的下載、對象的創建、文本的渲染等這些耗時的操作盡可能采用子線程的方式去處理,對於layer以及UI的操作不得不在主線程里面,只能想辦法優化(Facebook -> AsyncDisplayKit)

  • xib、storyBoard、純代碼

    storyBoard可以為開發者節省大量的時間,提高開發效率,但是對於那種復雜的滑動界面,用storyBoard時非常耗資源的,對於那種重用性不強固定不怎么變化的界面還是storyBoard省事兒。

  • 不要重復創建不必要的tableviewCell

    UItableView只需要一屏幕的cell對象就可以了,因為tableview提供了cell的緩存機制,在不可見的時候,可以將其緩存起來,而在需要時繼續使用即可。值得一提的是,cell被重用時,它內部繪制的內容並不會被自動清除,因此你可能需要調用setNeedsDisplayInRect:或setNeedsDisplay方法。

  • 減少視圖的數目

    UITableViewCell包含了textLabel、detailTextLabel和imageView等view,而你還可以自定義一些視圖放在它的contentView里,然而view是很大的對象,創建它會消耗較多的資源,並且也影響渲染性能。如果你的cell包含圖片且數量較多,使用默認的cell會非常影響性能。最佳的解決辦法是繼承UITableViewCell,並在其drawRect:中自行繪制:

1
(void)drawRect:(CGRect)rect {  if  (image) { [image drawAtPoint:imagePoint]; self.image = nil; }  else  { [placeHolder drawAtPoint:imagePoint]; } [text drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation]; }

不過這樣一來,你會發現選中一行后,這個cell就變藍了,其中的內容就被擋住了。最簡單的方法就是將cell的selectionStyle屬性設為UITableViewCellSelectionStyleNone,這樣就不會被高亮了。

此 外還可以創建CALayer,將內容繪制到layer上,然后對cell的contentView.layer調用addSublayer:方法。這個例 子中,layer並不會顯著影響性能,但如果layer透明,或者有圓角、變形等效果,就會影響到繪制速度了。解決辦法可參見后面的預渲染圖像。

不要做多余的繪制工作。

在實現drawRect:的時候,它的rect參數就是需要繪制的區域,這個區域之外的不需要進行繪制。

例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判斷是否需要繪制image和text,然后再調用繪制方法。

預渲染圖像。

你會發現即使做到了上述幾點,當新的圖像出現時,仍然會有短暫的停頓現象。解決的辦法就是在bitmap context里先將其畫一遍,導出成UIImage對象,然后再繪制到屏幕

  • 少用addView給cell動態添加view,可以初始化的時候就添加,然后通過hide控制是否顯示。

  • 提前計算並緩存好高度,因為heightForRow最頻繁的調用。

  • 善用hidden隱藏(顯示)Subview

iOS 高效添加圓角效果實戰講解

內存惡鬼drawRect

UIKit性能調優實戰講解

圖片壓縮

  • 一 首先要知道 什么是壓縮:

    “壓” 指文件體積變小,但是像素不變,長寬尺寸不變,那么質量可能下降

    “縮” 指文件的尺寸變小,也就是像素數減少,而長寬尺寸變小,文件體積同樣會減小。

  • 二 圖片的壓處理

    圖片的壓處理,我們可以使用UIImageJPEGRepresentation或UIImagePNGRepresentation方法實現

    如代碼:

1
2
3
4
5
6
- (void)_imageCompression{
     UIImage *image = [UIImage imageNamed:@ "HD" ];
    //第一個參數是圖片對象,第二個參數是壓的系數,其值范圍為0~1。
     NSData * imageData = UIImageJPEGRepresentation(image, 0.7);
     UIImage * newImage = [UIImage imageWithData:imageData];
}

關於PNG和JPEG格式壓縮

UIImageJPEGRepresentation函數需要兩個參數:圖片的引用和壓縮系數UIImagePNGRepresentation只需要圖片引用作為參數。UIImagePNGRepresentation(UIImage image)要比UIImageJPEGRepresentation(UIImage image,1.0)返回的圖片數據量大很多。同樣的一張照片,使用UIImagePNGRepresentation(image)返回的數據量大小為200k,而UIImageJPEGRepresentation(image,1.0)返回的數據量大小只為150k,如果對圖片的清晰度要求不是極高,建議使用UIImageJPEGRepresentation,可以大幅度降低圖片數據量。注意:壓縮系數不宜太低,通常是0.3~0.7,過小則可能會出現黑邊等。

  • 三 圖片“縮”處理

    通過[image drawInRect:CGRectMake(0,0,targetWidth,targetHeight)可以進行圖片“縮”處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
   *  圖片壓縮到指定大小
   *  @param targetSize  目標圖片的大小
   *  @param sourceImage 源圖片
   *  @return 目標圖片
   */
  - (UIImage*)imageByScalingAndCroppingForSize:(CGSize)targetSize withSourceImage:(UIImage *)sourceImage
{
UIImage *newImage = nil;
CGSize imageSize = sourceImage.size;
CGFloat width = imageSize.width;
CGFloat height = imageSize.height;
CGFloat targetWidth = targetSize.width;
CGFloat targetHeight = targetSize.height;
CGFloat scaleFactor = 0.0;
CGFloat scaledWidth = targetWidth;
CGFloat scaledHeight = targetHeight;
CGPoint thumbnailPoint = CGPointMake(0.0,0.0);
if  (CGSizeEqualToSize(imageSize, targetSize) == NO)
{
     CGFloat widthFactor = targetWidth / width;
     CGFloat heightFactor = targetHeight / height;
     if  (widthFactor > heightFactor)
         scaleFactor = widthFactor;  // scale to fit height
     else
         scaleFactor = heightFactor;  // scale to fit width
     scaledWidth= width * scaleFactor;
     scaledHeight = height * scaleFactor;
     // center the image
     if  (widthFactor > heightFactor)
     {
         thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5;
     }
     else  if  (widthFactor < heightFactor)
     {
         thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5;
     }
}
UIGraphicsBeginImageContext(targetSize);  // this will crop
CGRect thumbnailRect = CGRectZero;
thumbnailRect.origin = thumbnailPoint;
thumbnailRect.size.width= scaledWidth;
thumbnailRect.size.height = scaledHeight;
 
[sourceImage drawInRect:thumbnailRect];
newImage = UIGraphicsGetImageFromCurrentImageContext();
if (newImage == nil)
     NSLog(@ "could not scale image" );
 
//pop the context to get back to the default
UIGraphicsEndImageContext();
 
   return  newImage;
}

數據持久化

數據持久化是一種非易失性存儲技術,在重啟計算機或設備后也不會丟失數據,試講內存中的數據模型轉換為存儲模型,以及將存儲模型轉換為內存中的數據模型的統稱。數據模型可以是任何數據結構或對象模型,存儲模型可以是關系模型、XML、二進制流等。持久化技術主要用於MVC模型中的model層。目前iOS平台上主要使用如下四種技術:

NSUserDefaults()

屬性列表概念:屬性列表是一種基於xml序列化的數據歐諾個就存儲文件,又稱plist文件,原理是將一些基本數據類型讀寫進plist文件(plist文件是XML格式文件,因為常用於存儲配置信息,使用又稱作plist格式文件)並以明文方式存儲在設備中。許多OC 的基本數據類型(如NSArray、NSString 等)本身提供了向plist文件讀寫的方法,但是實際項目中我們用到的更多是NSUserDefaults,NSUserDefaults是蘋果基於屬性列表所封裝的一個單例類,該類提供了基本數據類型的plist文件存儲方法,因為其使用方便,代碼易懂, NSUserDefaults成為了最常用的數據持久化方式之一。

NSUserDefaults常用方法

1
2
3
4
5
6
7
8
9
10
11
12
//從 NSUserDefaults 中取出 key 值所對應的 Value
id = [[NSUserDefaults standardUserDefaults] objectForKey:(NSString *)];
 
//將數據對象存儲到 NSUserDefaults 中
[[NSUserDefaults standardUserDefaults] setObject:(id)
                                           forKey:(NSString *)];
 
//將數據對象從 NSUserDefaults 中移除
[[NSUserDefaults standardUserDefaults] removeObjectForKey(NSString *)];
 
//同步更新到Plist文件,當修改了 NSUserDefaults 的數據后,必須進行此步操作
[[NSUserDefaults standardUserDefaults] synchronize];

NSUserDefaults特點

  • NSUserDefaults常用於存儲OC基本數據類型,不適合存儲自定義對象,NSUserDefaults支持的數據類型有:NSNumer(NSInteger,float,double)NSSstring,NSDate,NSArray,NSDictionary,BOOL.

  • 自定義對象可以轉化成基本類型NSData后再使用NSUserDefaults機型存儲,但並不常用。

  • 當plist文件存儲的數據發生改變(寫操作)時,需要調用syschronize方法同步,否則數據無法同步保存。

  • Key值應具有唯一性,重名時將覆蓋先前key值。

  • 實際開發中,NSUserDefaults常用語於存儲配置信息,優點是簡便,缺點是所有數據都以明文存儲在plist文件中,容易被解讀導致安全性不高。

NSUserDefautls將數據存儲在什么地方了?

它存儲在應用的一個plist文件中,在程序沙盒位置的/Library/Prefereces里面有個plist文件,存儲的就是你的userDefaults,想要刪掉的話,用removeObjectForKey或者刪掉沙盒,也就是你的應用程序然后重新安裝。

  • 此外還可以自定義plist文件的位置進行存儲數據

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
1.獲得文件路徑
 
     NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
     NSString *fileName = [path stringByAppendingPathComponent:@ "123.plist" ];
2.存儲
 
NSArray *array = @[@ "123" , @ "456" , @ "789" ];
[array writeToFile:fileName atomically:YES];
3.讀取
 
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@ "%@" , result);
4.注意
 
只有以上列出的類型才能使用plist文件存儲。
存儲時使用writeToFile: atomically:方法。 其中atomically表示是否需要先寫入一個輔助文件,再把輔助文件拷貝到目標文件地址。這是更安全的寫入文件方法,一般都寫YES。
讀取時使用arrayWithContentsOfFile:方法。

對象歸檔(序列化)

和屬性列表一樣,對象歸檔也是將對象寫入文件並保存在硬盤內,所以本質上是另一種形式的序列化(存儲模型不同)。雖說任何對象都可以被序列化,但只有某些特定的對象才能放置到某個集合類(例如:NSArray,NSMutableArray,NSDictionary,NSData等)中,並使用該集合類的方法在屬性列表中讀寫,一旦將包含了自定義對象的數組寫入屬性列表,程序就會報錯。歸檔與屬性列表方式不同,屬性列表只有指定的一些對象才能進行持久化且明文存儲,而歸檔時任何實現了NSCoding協議的對象都可以被持久化,且歸檔后的文件是加密的。對象歸檔涉及兩個類: NSKeyedArchiver和NSKeyedUnarchiver,這兩個類是NSCoder的子類,分別用於歸檔和解檔。

對象歸檔

現在,我們有一個自定義的Person類,該類有name,age,height三個屬性,其.h文件如下

1
2
3
4
5
6
//Person.h
#import
@interface Person:NSObject
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)int age;
@property(nonatomic,assign)double height;

在.m文件中,我們要實現NSCoding中的兩個協議方法,這兩個方法分別在歸檔和解檔時會被調用,Person類的.m文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Person.m
#import"Person.h"
@implementation Person
/*
* 歸檔時調用該方法,該方法說明哪些數據需要儲存,怎樣儲存
*/
- (void)encodeWithCoder:(NSCoder *)encoder
{
     [encoder encodeObject:_name forKey:@ "name" ];
     [encoder encodeInt:_age forKey:@ "age" ];
     [encoder encodeDouble:_name forKey:@ "height" ];
}
 
/*
* 歸檔時調用該方法,該方法說明哪些數據需要解析,怎樣解析
*/
-(id)initWithCoder:(NSCoder *)decode
{
     if  (self = [ super  init]) {
         _name = [decode decodeObjectForKey:@ "name" ];
         _age = [decode decodeIntForKey:@ "age" ];
         _height = [decode decodeDoubleForKey:@ "height" ];
     }
     return  self;
}
@end

這個Person類就具有了歸檔與解檔能力,當你需要對一個Person類的實力對象進行儲存或者解析時,在你自己的方法中只要鍵入如下代碼即可,下面兩個方法對應兩個按鈕的回調,點擊按鈕時分別執行person對象的歸檔和解檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//寫操作
- (IBAction)Write {
     Person *p = [[Person alloc]init];
     p.name = @ "jin" ;
     p.age = 10;
     p.height = 176.0;
 
     //設置歸檔后文件路徑
     NSString *path = @ "/Users/macbookair/Desktop/person.data" ;
     //歸檔
     [NSKeyedArchiver archiveRootObject:p toFile:path];
}
 
//讀操作
- (IBAction)read {
 
     //設置路徑
     NSString *path = @ "/Users/macbookair/Desktop/person.data" ;
 
     //解檔
     Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
     NSLog(@ "%@--%d---%f" ,p.name ,p.age ,p.height);
 
}

對象歸檔特點

可以將自定義對象寫入文件或從文件中讀出。

由於歸檔時進行了加密處理,因此安全性高於屬性列表。

CoreData(集成化)

當你的應用程序需要在本地存儲大量的關系型數據模型時,顯然上述方法並不適合,因為不論對象歸檔還是數據列表,一旦數據模型之間存在依賴關系,問題就將變得復雜。而此時iPhone自帶的輕量級數據庫Sqlite便成為我們的首選,如果你熟悉數據庫,那么恭喜,CoreData也將不再神秘,你可以理解為它是蘋果對Sqlite封裝的一個框架,你可以在Xcode中進行Sqlite數據庫的可視化操作

為什么要使用CoreData?

CoreData脫離了Sql語句,集成化更高。實際上,一個成熟的工程中一定是對數據持久化進行了封裝的,應該避免在業務邏輯中直接編寫Sql語句。

CoreData對版本遷移支持的較好,App升級之后數據庫字段或者表有更改會導致crash,CoreData的版本管理和數據遷移變得非常有用,手動寫sql語句操作相對麻煩一些。

CoreData不光能操縱SQLite,CoreData和iCloud的結合也很好,如果有這方面需求的話優先考慮CoreData。

CoreData是支持多線程的,但需要thread confinement的方式實現,使用了多線程之后可以最大化的防止阻塞主線程。

Sqlite(靈活)

Sqlite是iPhone自帶的的數據庫管理系統。如果你對數據庫和Sql語句不陌生,那么在介紹完CoreData后,你一定不滿足CoreData,作為一個程序員,也許你更希望能夠直接操作數據庫。既然蘋果選擇Sqlite作為數據庫,自然也提供了一系列可以直接操作它的函數(C語言函數),利用這些函數你完全能夠自己封裝一個sqlite數據庫框架,但同時你必須熟悉sql語句以及C語言語法。SQLite數據庫的幾個特點:

基於C語言開發的輕型數據庫

在iOS中需要使用C語言語法進行數據庫操作、訪問(無法使用ObjC直接訪問,因為libqlite3框架基於C語言編寫)

SQLite中采用的是動態數據類型,即使創建時定義了一種類型,在實際操作時也可以存儲其他類型,但是推薦建庫時使用合適的類型(特別是應用需要考慮跨平台的情況時)

建立連接后通常不需要關閉連接(盡管可以手動關閉)

#FMDB

FMDB框架中重要的框架類

FMDatabase

FMDatabase對象就代表一個單獨的SQLite數據庫,用來執行SQL語句

FMResultSet

使用FMDatabase執行查詢后的結果集

FMDatabaseQueue

用於在多線程中執行多個查詢或更新,它是線程安全的

數據庫第三方框架FMDB詳細講解

網絡傳輸協議

深入淺出-iOS的TCP/IP協議族剖析&&Socket

iOS-網絡編程(一)HTTP協議

RunTime

 

環信:發送頭像和昵稱

方法一 從APP服務器獲取昵稱和頭像

  • 昵稱和頭像的獲取:當收到一條消息(群消息)時,得到發送者的用戶ID,然后查找手機本地數據庫是否有此用戶ID的昵稱和頭像,如沒有則調用APP服務器接口通過用戶ID查詢出昵稱和頭像,然后保存到本地數據庫和緩存,下次此用戶發來信息即可直接查詢緩存或者本地數據庫,不需要再次向APP服務器發起請求

  • 昵稱和頭像的更新:當點擊發送者頭像時加載用戶詳情時從APP服務器查詢此用戶的具體信息然后更新本地數據庫和緩存。當用戶自己更新昵稱或頭像時,也可以發送一條透傳消息到其他用戶和用戶所在的群,來更新該用戶的昵稱和頭像。

方法二 從消息擴展中獲取昵稱和頭像

  • 昵稱和頭像的獲取:把用戶基本的昵稱和頭像的URL放到消息的擴展中,通過消息傳遞給接收方,當收到一條消息時,則能通過消息的擴展得到發送者的昵稱和頭像URL,然后保存到本地數據庫和緩存。當顯示昵稱和頭像時,請從本地或者緩存中讀取,不要直接從消息中把賦值拿給界面(否則當用戶昵稱改變后,同一個人會顯示不同的昵稱)。

  • 昵稱和頭像的更新:當擴展消息中的昵稱和頭像URI與當前本地數據庫和緩存中的相應數據不同的時候,需要把新的昵稱保存到本地數據庫和緩存,並下載新的頭像並保存到本地數據庫和緩存。

可以參考http://www.jianshu.com/p/26b1294c71f6


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM