來來來!關於iOS基礎總結我們倆好好嘮嘮

si1ence發表於2017-12-14

2016.05.20 10:24

塵封已久的學習基礎總結,最近公司專案不是很忙,終於抽空整理出來,現分享出來。

1.1 談一談GCD和NSOperation的區別?

  • 首先二者都是多執行緒相關的概念,當然在使用中也是根據不同情境進行不同的選擇;
  • GCD是將任務新增到佇列中(序列/併發/主佇列),並且制定任務執行的函式(同步/非同步),其效能最好,底層是C語言的API,也更輕量級。iOS4.0以後推出的,針對多核處理器的併發技術,只能設定某一個佇列的優先順序,常用的功能包括 一次性執行 dispatch_once延遲操作dispatch_after(這裡是延遲推到執行緒中,而不是線上程中等待,因此比如設定延遲1秒執行,但是一秒後只是推到了執行緒中,不會立刻執行),排程組等,其高階功能有
    • dispatch_barrier_async柵欄來控制非同步操作的順序
    • dispatch_apply充分利用多核進行快速迭代遍歷
    • dispatch_group_t佇列組,新增到佇列組中的任務完成之後會呼叫dispatch_group_notify 函式,可以實現類似A、B兩個耗時操作都完成之後,去主執行緒更新UI的操作
  • NSOperation把操作(非同步)新增到佇列中(全域性的併發佇列),是OC框架,更加物件導向,是對GCD的封裝,iOS2.0推出,蘋果推出GCD之後,對NSOperation的底層全部重寫,可以隨時取消已經設定準備要執行的任務,已經執行的除外,可以設定佇列中每一個操作的優先順序,其基本功能包括設定最大操作併發數maxConcurrentOperationCount,繼續/暫停/全部取消,可以快佇列設定操作的依賴關係,通過KVO監聽 NSOperation 物件的屬性,如 isCancelled、isFinished;物件可重用。
    • NSInvocationOperationNSBlockOperation建立方法等這些基礎面試官往往預設你是會的
    • NSOperationQueue只有兩種佇列:主佇列、其他佇列。其他佇列包含了序列和併發
    • NSOperation + NSOperationQueue將任務加入到佇列
    • 操作依賴:[operation2 addDependency:operation1];(operation2 依賴於operation1的完成,但這兩個任務要加入到同一個佇列中)

1.2 談談多執行緒的應用

通常耗時的操作都放在子執行緒處理,然後到主執行緒更新UI,如

  • 我們要從資料庫提取資料還要將資料分組後顯示,那麼就會開個子執行緒來處理,處理完成後才去重新整理UI顯示。
  • 拍照後,會在子執行緒處理圖片,完成後才回到主執行緒來顯示圖片。拍照出來的圖片太大了,因此要做處理。
  • 音訊、視訊處理會在子執行緒來操作
  • 檔案較大時,檔案操作會在子執行緒中處理
  • 做客戶端與服務端資料同步時,會在後臺閒時自動同步

2. 執行緒之間是如何通訊的?

  • 通過主執行緒和子執行緒切換的時候傳遞引數performSelecter:onThread:withObject:waitUntilDone:

3. 網路圖片處理問題怎麼解決圖片重複下載問題?(SDWebImage大概實現原理)

  • 這個就需要用到字典,以圖片的下載地址url為key,下載操作為value,所有的圖片大概分成三類:已經下載好的,正在下載的和將要下載的;

  • 當一張圖片將要進行下載操作的時候,先判斷快取中是否有相同的圖片,如果有的話就返回,沒有的話就根據url的md5加密值去沙盒中找,有的話就拿出來用,沒有的話再去以圖片的url為key去字典中找有沒有正在進行的任務,最後去判斷等待的下載操作任務裡面的字典有無相同key,如果沒有,就自己開啟任務,記錄一下,檔案儲存的名稱是url的md5值

  • 這裡建立了兩個字典 : 1.iconCache:儲存快取的圖片 2.blockOperation 用來儲存下載任務

    653183AF-3074-47AD-8460-10B5CEF1323C.png

  • 每當進入或退出程式時,會進行圖片檔案的管理:超過一星期的檔案會被清除,如果設定了最大快取,超過這個快取就會刪除最舊的檔案,直到當前快取檔案為最大快取檔案的一半大小;

  • 一般app中大部分快取都是圖片的情況下,可以直接呼叫clear方法進行清除快取,getSize()方法獲取當前快取大小。

4. 多執行緒安全的幾種解決方法?

  • 1> 只有在主執行緒重新整理訪問UI
  • 2> 如果要防止資源搶奪,需要用synchronize進行加鎖保護
  • 3> 如果是非同步操作要保證執行緒安全等問題,儘量使用GCD(有些函式預設就是安全的)
  • 4> 單例為什麼用static dispatch_once?使用dispatch_once可以簡化程式碼並且徹底保證執行緒安全,開發者無需擔心加鎖或同步。此外,dispatch_once更高效,它沒有使用重量級的同步機制,若是那樣做的話,每次執行程式碼前都要獲取鎖。

5. 原子屬性

  • 原子屬性採用的是"多讀單寫"機制的多執行緒策略;"多讀單寫"縮小了鎖範圍,比互斥鎖的效能好
  • 規定只在主執行緒更新UI,就是因為如果在多執行緒中更新,就需要給UI物件加鎖,防止資源搶佔寫入錯誤,但是這樣會降低UI互動的效能,所以ios設計讓所有UI物件都是非執行緒安全的(不加鎖)

6. 代理的作用、block

  • 代理又叫委託,是一種設計模式(可以理解為java中回撥監聽機制),代理是物件與物件之間的通訊互動,代理解除了物件之間的耦合性
  • 改變或傳遞控制鏈,允許一個類在某些特定時刻通知到其他類,而不需要獲取到那些類的指標,可以減少框架複雜度
  • 代理的屬性常是 weak 的原因:防止迴圈引用,以致物件無法得到正確的釋放(具體原因會在下文第十一條闡述)
  • block底層是根據函式指標和結構體結合實現的,block本身就是結構體,更加簡潔,不需要定義繁瑣的協議方法,但通訊事件比較多的話,建議使用Delegate
  • block就是一個資料型別,存放一段程式碼,編譯的時候不會執行,只有用到的時候才會去執行裡面的程式碼。宣告的時候使用copy是因為要從棧區拷貝到堆區,在棧區會受到作用域的限制,超出所在的函式就會被銷燬,就沒辦法進行傳值回撥等一系列操作了。應注意迴圈引用,__weak來修飾。如果一個變數是在block外部建立,需要在block內部修改,那麼需要使用__block修飾這個變數(__block可以在ARC和MRC情況下使用,可以修飾物件和基本資料型別,__weak只能在ARC下使用,只能修飾物件,不能修飾基本資料型別)
  • 最常用的是使用block作為引數傳值,不同情況下回撥不同的程式碼(如成功回撥失敗回撥)

7. 談談你對runTime執行時機制的瞭解(注意哦,這個很重要的)

  • runtime是一套比較底層的純C語言API,屬於一個C語言庫,包含了很多底層的C語言的API
  • 平時編寫的OC程式碼,在程式執行過程中,其實都是轉成了runtime的C語言程式碼,runtime是OC的幕後工作者,底層語言,例如:
    • OC--> [[WPFPerson alloc] init]
    • runtime-->objc_msgSend(objc_msgSend("WPFPerson", "alloc"), "init")
  • 利用runtime可以實現一些非常底層的操作(用OC不好實現)
    • 在程式執行過程中,動態建立一個類(比如KVO底層實現:檢測isa指標,發現是新建了一個類,當然Xcode7.0以前的版本才可以監聽到isa指標)
    • 遍歷一個類的所有成員變數、方法,訪問私有變數(先通過runtime的class_getInstanceVariable獲取成員變數,再通過class_getIvar獲取它的值)
    • 在程式執行過程中,動態為某個類新增屬性\方法,修改屬性值\方法,比如產品經理需要跟蹤記錄APP中按鈕的點選次數和頻率等資料,可以通過整合按鈕或者類別實現,但是帶來的問題比如別人不一定去例項化你寫的子類,或者其他類別也實現了點選方法導致不確定會呼叫哪一個,runtime可以這樣解決:在按鈕的分類裡面,重寫load方法,新建監控按鈕點選的方法,先用class_addMethod方法,判斷其返回的bool值,如果新增成功,就用class_replaceMethod將原來的方法移除,如果新增失敗,就用method_exchangeImplementations方法進行替換
    • 攔截並替換方法,比如由於某種原因,我們要改變這個方法的實現,但是又不能動它的原始碼(比如一些開源庫出現問題的時候,這時候runtime就可以出場了)-->先增加一個tool類,然後寫一個我們自己實現的方法-change,通過runtime的class_getInstanceMethod獲取兩個方法,在用class_replaceMethod方法進行替換。防止陣列越界的方法:陣列越界的時候報錯的方法是add_object,做一個邏輯判斷,越界的時候通過class_replaceMethod交換掉add_object(相當於重寫了這個方法)
  • 相關應用
    • NSCoding(歸檔和解檔),如果一個模型有很多個屬性,那麼需要對每個屬性都實現一遍encodeObject和decodeObjectForKey方法,十分麻煩,但是如果使用class_copyIvarList獲取所有屬性,然後迴圈遍歷,使用[ivarName substringFromIndex:1]去掉成員變數下劃線
    • 字典轉模型:像幾個出名的開源庫JSONModel、MJExtension等都是通過這種方式實現的(利用runtime的class_copyIvarList獲取屬性陣列,遍歷模型物件的所有成員屬性,根據屬性名找到字典中key值進行賦值,當然這種方法只能解決NSString、NSNumber等,如果含有NSArray或NSDictionary,還要進行第二步轉換,如果是字典陣列,需要遍歷陣列中的字典,利用objectWithDict方法將字典轉化為模型,在將模型放到陣列中,最後把這個模型陣列賦值給之前的字典陣列)
  • Method Swizzling:OC中呼叫方法事實上就是向物件傳送訊息,而查詢訊息的唯一依據就是selector的名字,因此可以使用runtime執行時機制動態交換方法。在+load方法裡面調換,因為method swizzling的影響範圍是全域性的,所以應該放在最保險的地方來處理,+load方法能夠保證能在類初始化的時候一定能被呼叫,可以保證統一性,如果是在使用的時候才去呼叫,可能達不到全域性處理的效果;使用dispatch_once保證只交換一次。[objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:) withSwizzledSelector:@selector(wpf_safeAddObject:)];使用場景:addObject方法新增的值為nil的時候會崩潰。呼叫objectAtIndex:時出現崩潰提示empty陣列問題

8. 談談你對Run Loop的理解

  • RunLoop是多執行緒的一個很重要的機制,就是一個執行緒一次只能執行一個任務,執行完任務後就會退出執行緒。主執行緒會通過do-while死迴圈讓程式持續等待下一個任務不退出。通過mach_msg()讓runloop沒事時進入trap狀態,節省CPU資源。非主執行緒通常來說就是為了執行某個任務而建立的,執行完就會歸還資源,因此預設不開啟RunLoop
  • 實質上,對於子執行緒的runloop是預設不存在的,因為蘋果採用了懶載入的方式,如果沒有手動呼叫[NSRunLoop currentRunLoop]的話,就不會去查詢當前執行緒的RunLoop,也不會建立、載入
  • 當然如果子執行緒處理完某個任務後不退出,需要繼續等待接受事件,需要啟動的時候也可以手動啟動,比如說新增定時器的時候就要手動開始RunLoop

如何處理事件

  • 介面重新整理: 當UI改變( Frame變化、 UIView/CALayer 的繼承結構變化等)時,或手動呼叫了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理。 蘋果註冊了一個用來監聽BeforeWaiting和Exit的Observer,在它的回撥函式裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 介面。

  • 手勢識別: 如果上一步的 _UIApplicationHandleEventQueue() 識別到是一個guesture手勢,會呼叫Cancel方法將當前的touchesBegin/Move/End 系列回撥打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。 蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,其回撥函式為 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,並執行GestureRecognizer的回撥。 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。

  • 網路請求:最底層是CFSocket層,然後是CFNetwork將其封裝,然後是NSURLConnection對CFNetwork進行物件導向的封裝。當網路開始傳輸時,NSURLConnection建立了兩個新執行緒:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket執行緒是處理底層socket連線的。NSURLConnectionLoader這個執行緒內部會使用RunLoop來接受底層socket的事件,並新增到上層的Delegate

應用

  • 滑動與圖片重新整理:當tableView的cell上有需要從網路獲取的圖片的時候,滾動tableView,非同步執行緒回去載入圖片,載入完成後主執行緒會設定cell的圖片,但是會造成卡頓。可以設定圖片的任務在CFRunloopDefaultMode下進行,當滾動tableView的時候,Runloop切換到UITrackingRunLoopMode,不去設定圖片,而是而是當停止的時候,再去設定圖片。(在viewDidLoad中呼叫self.imageView performSelector@selector(setImage) withObject:...afterDelay:...inModes@[NSDefayltRunLoopMode])

  • 常駐子執行緒,保持子執行緒一直處理事件 為了保證執行緒長期運轉,可以在子執行緒中加入RunLoop,並且給Runloop設定item,防止Runloop自動退出

10. 關於Socket,談談TCP/IP 和 UDP的理解

  • Socket是一個用於傳輸網路資料的工具,TCP/IP 和 UDP都是傳輸協議,用於定義網路資料傳輸的格式,屬於長連線
  • TCP/IP 側重可靠傳輸,傳輸速度慢,不會丟失資料,安全,聊天和下載檔案時用到
  • UDP:側重快速傳輸,傳輸速度快,容易丟失資料包,不安全。區域網遊戲和網路遊戲,視訊聊天的時候用到
  • TCP更安全是因為有一個三次握手:第一次握手(客戶端傳送syn包到伺服器,並進入SYN_SEND狀態,等待伺服器確認),第二次握手(伺服器收到syn包,必須確認客戶的SYN包,同時自己傳送一個SYN+ACK包,此時伺服器進入SYN_RECV狀態),第三次握手(客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK,傳送完畢後伺服器和客戶端都進入ESTABLISHED狀態,完成三次握手),三次握手之後才開始正式傳輸資料。
  • 那麼現在即時通訊更適合用TCP還是UDP?
    • 早期使用MSN是使用TCP協議的,QQ使用採用UDP的,但並不代表UDP就不安全,因為可以手動對UDP的資料收發進行驗證(比如傳送方對每個資料包進行編號然後由接收方進行驗證),正是因為這個,QQ的傳遞速度是遠遠快於MSN,可能這也是打敗MSN的其中一個原因吧
  • Http:超文字傳輸協議,用於定義網路資料傳輸的格式(短連結)http1.0之前不支援短連線,1.1之後預設就是長連線,只要在伺服器和客戶端同時設定Connection為keep-alive即可
    • 長連線是為了複用,長連線指的是TCP連線,也就是為了複用TCP連線,也就是說多個HTTP請求可以複用一個TCP連線,節省了很多TCP連線建立和斷開的消耗
    • 比如請求了一個網頁,這個網頁肯定還包含了CSS、JS等一系列資源,如果是短連線的話,每次開啟一個網頁,基本要建立幾個甚至幾十個TCP連線,浪費了大量資源
    • 長連線不是永久連線,如果一段時間內,具體的時間長短,是可以在header當中進行設定的,也就是所謂的超時時間,這個連線沒有HTTP請求發出的話,那麼這個長連線就會被斷掉
  • socket連線是長連線,客戶端與伺服器保持通道,雙方可以主動傳送資料,一般多用於即時通訊,遊戲,預設超時時間是30秒,預設大小是8k(一個資料包大小)

11. 談一談記憶體管理

  • iOS的記憶體管理分為 MRC 和 ARC,管理的是堆區動態產生的物件,基本資料型別就不是記憶體管理的範圍
  • 記憶體管理的核心概念是引用計數器:當物件被alloc、copy、new的時候,引用計數器+1,當被release的時候引用計數器—1,為0的時候就會被系統回收,呼叫dealloc方法
  • 說道記憶體管理,就必須說說@property的記憶體管理引數:
    • assign --> 針對於基本資料型別的簡單賦值操作
    • retain --> release 一次舊物件 retain 一次新物件 (適用於OC物件型別)
    • copy --> release 一次舊物件 拷貝一個新物件出來(一般修飾字串和block)
    • weak--> 表示一種非擁有關係,設定該屬性時既不釋放新值,也不保留舊值,和assign類似,但是目標物件釋放時,屬性值也會自動清空
  • 如何避免記憶體洩露 --> 使用Analyze進行程式碼的靜態分析
  • 當然使用block的時候最應該注意下迴圈引用,使用Leaks檢測記憶體洩露,顯示綠色的勾告知記憶體處理的不錯,實際上記憶體得不到釋放。一般我的方法是在控制器宣告週期的viewDidAppear和dealloc方法裡面列印日誌[[self class] description],如果沒有列印出來,就說明沒有被釋放。使用__weak __typeof(self) weakSelf = self;解決。有一次我是直接使用成員變數,而不是屬性,_age,我以為這樣沒有使用self就可以了,但是後來測試發現還是造成迴圈引用了,因為_age是控制器的成員變數,也就是強引用了控制器,也要改成弱引用__block __weak __typeof(_currentModel) weakModel = _currentModel;

11.1 為什麼用 weak 修飾的變數會自動置為 nil?

  • runtime 對註冊類,會進行佈局,將 weak 修飾的物件放到一個hash表中,key值是該物件的記憶體地址,value是該物件
  • 當該物件retainCount為0時,執行dealloc,根據該地址去weak的hash表中查詢到該物件,從而設定為nil(向nil傳送訊息是安全的)

12. 常見的資料持久化有哪些

  • 偏好設定(preference),利用NSUserDefaults
    • 用來儲存應用程式設定和屬性、使用者儲存的資料。使用者再次開啟程式或開機後這些資料仍然存在
    • NSUserDefaults可以儲存的資料型別包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要儲存其他型別,需要先轉化為前面的型別,才能用NSUserDefault儲存
    • 偏好設定是專門用來儲存應用程式的配置資訊的,一般不要在偏好設定中儲存其他資料
    • 偏好設定會將所有資料儲存到同一個檔案中。即preference目錄下的一個以此應用包名來命名的plist檔案。
//1.獲得NSUserDefaults檔案
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向檔案中寫入內容
[userDefaults setObject:@"AAA" forKey:@"a"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
//3.讀取檔案
NSString *name = [userDefaults objectForKey:@"a"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
NSLog(@"%@, %d, %ld", name, sex, age);
複製程式碼
  • 歸檔(Archiver)、解檔(unArchiver),利用NSKeyedArchiver實現歸檔、利用NSKeyedUnarchiver反接的那個
    • 歸檔及時將記憶體中的物件寫入到磁碟檔案中,歸檔也叫序列化,解檔就是講磁碟中檔案中的物件讀取出來
    • 必須遵循NSCoding協議,只要遵循了NSCoding協議的物件都可以通過它實現序列化,兩個協議方法必須實現
// 反歸檔
  - (id)initWithCoder:(NSCoder *)aDecoder {
      if ([super init]) {
          self.avatar = [aDecoder decodeObjectForKey:@"avatar"];
          self.name = [aDecoder decodeObjectForKey:@"name"];
          self.age = [aDecoder decodeIntegerForKey:@"age"];
      }
      return self;
  }
  // 歸檔
  - (void)encodeWithCoder:(NSCoder *)aCoder {
      [aCoder encodeObject:self.avatar forKey:@"avatar"];
      [aCoder encodeObject:self.name forKey:@"name"];
      [aCoder encodeInteger:self.age forKey:@"age"];
  }
複製程式碼
* 歸檔,把物件歸檔時需要呼叫NSKeyedArchiver的工廠方法archiveRootObject: toFile: 方法
複製程式碼
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
  Person *person = [[Person alloc] init];
  person.avatar = self.avatarView.image;
  person.name = self.nameField.text;
  person.age = [self.ageField.text integerValue];
  [NSKeyedArchiver archiveRootObject:person toFile:file];
複製程式碼
* 反歸檔
複製程式碼
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
  Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
  if (person) {
     self.avatarView.image = person.avatar;
     self.nameField.text = person.name;
     self.ageField.text = [NSString stringWithFormat:@"%ld", person.age];
  }
複製程式碼

這五種持久化操作不同點

  • 從儲存資料大小來看,歸檔、偏好設定、屬性列表三種方法適合儲存資料量較小的資料,資料庫、CoreData方法適合儲存資料量較大的資料

  • 從加密性來看,其中歸檔會將資料進行加密,而偏好設定是直接儲存到屬性列表中,不會對資料進行加密

  • 從儲存型別來看,屬性列表只能存放固定的七種型別(可在plist檔案中看到),歸檔對儲存型別無限制

13. KVC 和 KVO

  • KVC(key-value-coding鍵值編碼,跟多情況下會簡化程式程式碼)的常見用法:

    • 給私有變數(該變數不對外開放)賦值:[Person setValue: @"19" ForKeyPath:@"age"]
    • 字典轉模型:setValuesForKeyWithDictionary
    • 取出私有變數:[Person valueForKey:@"age"]
    • 沒有找到對應的key會崩潰:重寫setValueForUndefinedKey
  • KVC缺點:一旦使用KVC,編譯器無法檢查出錯誤,即不會對設定的鍵、鍵路徑進行錯誤檢查,且執行效率低於自定義的setter和getter方法,因為使用KVC鍵值編值,必須先解析字串,然後設定或訪問物件的例項變數

  • 通過KVO(key-value-observing,典型的觀察者模式,被觀察的物件必須使用KVC鍵值編碼來修改它的例項變數,這樣才能被觀察者觀察到)監聽person物件中name屬性發生改變

    • 給監聽的屬性設定一個觀察者:
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
複製程式碼
* 當person的name的值發生改變時,就會執行該方法
複製程式碼
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
  do something....
}
複製程式碼
  • 當一個類的屬性被觀察的時候,系統會通過runtime動態的建立一個該類的派生類,並且會在這個類中重寫基類被觀察的屬性的setter方法,而且系統將這個類的isa指標指向了派生類(NSNotifying_類名),從而實現了給監聽的屬性賦值時呼叫的是派生類的setter方法。重寫的setter方法會在呼叫原setter方法前後,通知觀察物件值得改變。

14. @synthesize和@dynamic區別是什麼

  • 這兩個關鍵字都是@property對應的詞
  • @synthesize 語義是如果沒有手動實現setter和getter方法,那麼編譯器會自動幫你加上這兩個方法
  • @dynamic告訴編譯器,屬性的setter和getter由使用者自己實現,不自動生成(readOnly只實現getter即可),但是如果沒有自己實現,編譯的時候不會報錯,執行的時候就會報錯,這就是所謂的動態繫結

15. 談談時間響應鏈的一般順序

  • 第一步:事件的產生:首先找到最合適的從UIApplication到keyWindow依次找到view,由上到下
    • 發生觸控事件後,系統會將該事件加入到一個由UIApplication管理的事件佇列中,為什麼是佇列而不是棧,因為佇列先進先出,先產生的事件優先處理才合理
    • UIApplication 會從事件佇列中取出最前面的事件,並將事件分發下去以便處理,通常,先把事件傳送給應用程式的主視窗(keywindow)
    • 主視窗會在檢視層級結構中找到一個最合適的檢視來處理觸控事件(hitTest:方法遍歷當前view的所有子類,返回最合適的view)
    • 找到合適的檢視控制元件後,就會用檢視控制元件的touches 方法來做具體的事件處理
  • 由於是從父檢視到子檢視,因此如果父view不接收事件,自檢視也無法接收事件
  • 如果確定父控制元件是最合適的view,那麼父控制元件的子控制元件的hitTest:withEvent: 方法也會被呼叫
  • 想讓誰成為最合適的view就重寫自己的父控制元件的hitTest:withEvent: 方法,返回指定的控制元件
  • 第二步:事件的響應:由下到上
    • 先看inital view能否處理這個事件,如果不能依次往上傳遞(如self或superView的userInterface/enable屬性為NO,透明度小於0.1等原因),一直傳遞到該檢視的VC/window/application ,如果application還不能處理,就將該事件拋棄
    • 在事件的響應過程中,如果某控制元件實現了 touches 方法,則這個事件由該控制元件來接收,如果呼叫了[super touches] 方法,就會將事件順著響應者鏈條往上傳遞,傳遞給上一個響應者

16.1 post和get方式的區別

  • GET請求的資料會負載URL之後,即把資料放在HTTP協議頭中,以?區分URL和傳輸資料,引數之間以&相連,英文字母/數字,原樣傳送,如果是空格,轉化為+,如果是中文,把字串用BASE64加密;POST就是把提交的資料放在HTTP包的包體中

  • GET一般用於提交少量資料(最多提交1k,瀏覽器限制),POST用於提交大量資料(理論上無限制,收伺服器限制)

  • GET 無副作用,POST 有副作用

  • GET提交的資料可以在瀏覽器歷史記錄中看到,安全性不好,別人可以拿到賬號密碼,POST不會

  • Get是向伺服器發索取資料的一種請求,而POST是向伺服器發提交資料的一種請求,只是傳送機制不同

  • GET不可以設定書籤,POST可以設定書籤

  • POST支援更多編碼型別且不對資料型別限制

  • 什麼情況下用POST:

    • 請求的結果具有持續性副作用,如資料庫新增新的資料行
    • 若使用get方法,則表單上手機的資料可能讓URL過長
    • 要傳送的資料不是採用7位的ASCII編碼
  • 什麼情況下用GET:

    • 請求是為了查詢資源,HTML表單資料僅用來幫助搜尋
    • 請求結果無持續副作用性的副作用
    • 手機的資料及HTML表單內的輸入欄位名稱的總長不超過1024個字元

16.2 POST和PUT區別

  • POST請求的url表示處理該封閉實體的資源,該資源可能是個資料接收過程、某種協議的閘道器、或者接收註解的獨立實體。

  • PUT請求中的url表示請求中封閉的實體-使用者代理知道url的目標,並且伺服器無法將請求應用到其他資源。如果伺服器希望該請求應用到另一個url,就必須傳送一個301響應;使用者代理可通過自己的判斷來決定是否轉發該請求。

  • POST是用來提交資料的。提交的資料放在HTTP請求的正文裡,目的在於提交資料並用於伺服器端的儲存,而不允許使用者過多的更改相應資料(主要是相對於在url 修改要麻煩很多)。

  • PUT操作是冪等的。所謂冪等是指不管進行多少次操作,結果都一樣。比如我用PUT修改一篇文章,然後在做同樣的操作,每次操作後的結果並沒有不同

  • POST操作既不是安全的,也不是冪等的,比如常見的POST重複載入問題:當我們多次發出同樣的POST請求後,其結果是建立出了若干的資源。

  • 安全和冪等的意義在於:當操作沒有達到預期的目標時,我們可以不停的重試,而不會對資源產生副作用。從這個意義上說,POST操作往往是有害的,但很多時候我們還是不得不使用它。

  • 還有一點需要注意的就是,建立操作可以使用POST,也可以使用PUT,區別在於POST 是作用在一個集合資源之上的,而PUT操作是作用在集合的一個具體資源之上的,再通俗點說,如果URL可以在客戶端確定,那麼就使用PUT,如果是在服務端確定,那麼就使用POST,比如說很多資源使用資料庫自增主鍵作為標識資訊,而建立的資源的標識資訊到底是什麼只能由服務端提供,這個時候就必須使用POST。

17. 深複製和淺複製

  • 非集合類對immutable物件進行copy操作,是指標複製,mutableCopy操作時內容複製
  • 非集合類對mutable物件進行copy和mutableCopy都是內容複製
  • 在集合類物件中,對immutable物件進行copy,是指標複製,mutableCopy是內容複製
  • 在集合類物件中,對mutable物件進行copy和mutableCopy都是內容複製。但是:集合物件的內容複製僅限於物件本身,物件元素仍然是指標複製
  • copy出來的物件都是不可變的,mutableCopy出來的物件都是可變的
  • NSString *str = @"string"; str = @"newString"; 列印物件地址,發現是發生變化的,需要把@"newStirng"當做一個新的物件,將這段物件的記憶體地址賦值給str

18. 關於專案中動畫的使用

  • 序列幀動畫:self.imageView.animationImages = array;
  • [UIView animateWithDuration] + CGAffinetransform
  • 核心動畫CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position.y"]; anim.fromValue toValue repeatCount [btn.layer addAnimation]
  • 關鍵幀動畫CAKeyframeAnimation,anim.values = array,新增到layer上
  • 組動畫CAAnimationGroup,將以上動畫組合起來
  • 轉場動畫:CATransition,設定duration和type,然後新增到layer上。利用UIView 的類方法實現轉場動畫 [UIView transitionWithView: duration: options: animations:^{ } completion:nil];
  • UIDynamicAnimator模擬者 、 UISnapBehavior吸附行為,設定damping來調節震動幅度 、 UIPushBehavior推動行為 、 UICollisionBehavior碰撞邊緣檢測行為 、 UIAttachmentBehavior附著行為 、 UIGravityBehavior重力行為
  • POPSpringAnimation
    • springBounciness[0,20]越大振幅越大。
    • springSpeed速度

20.0 關於Swift與OC的不同

其實是個面試官八個不懂Swift,而且一般不懂得就問個Swift與OC的不同。。題主也只是在自學Swift,等到3.0出了之後再深入研究,而且專案中可能也要開始從混編逐漸向Swift靠攏了。

  • Swift是一門更加現代化的語言,但是目前還在成長階段,更新改動比較大,雖然說其底層思想不變,變的是API和介面,但是每次更新完Xcode看到自己的Swift專案還是有些淡淡的憂傷,而且目前Swift開發都要轉成OC的runtime,包略大,因此題主認為成熟專案最好還是採用OC

  • 先記住一句話:OC底層物件導向,而Swift底層更加面向協議

  • 我們已經見識過Apple使用了大量協議,比如在tableView當中,我們可以通過協議來告訴Apple需要多少個表檢視單元格,而不是每時每刻都要繼承UITableViewController

  • 在這裡以MVVM作為測試用例:比如現在需要建立一個類似設定介面的tableView,每個cell需要一個label和一個switch,自定義SwitchWithTextTableViewCell,在其內部建立一個configure方法中對label的title,titleFont,titleColor,switch的switchOn和switchColor等進行初始化,但這種方式非常累贅,比如新增一個副標題,就需要額外新增三個屬性

  • 但是利用協議SwitchWithTextCellProtocol,讓檢視模型實現這個協議,然後在這裡設定所有的屬性

protocol SwitchWithTextCellProtocol {
    var title: String { get }
    var titleFont: UIFont { get }
    var titleColor: UIColor { get }

    var switchOn: Bool { get }
    var switchColor: UIColor { get }

    func onSwitchTogglenOn(onL Bool)
}
複製程式碼
  • 通過swift2.0重點餓協議擴充套件,就可以通過預設值來做一些處理了,如果對於大多數單元格來說,可以確定某一種顏色的話,就可以對其建立擴充套件,然後設定顏色即可,所有實現此協議的檢視就沒有必要再去設定這個顏色了

  • 現在,我的configure方法裡面只要實現此協議的值就可以了

// 這個方法只需要一個引數,相比於之前的多個引數簡便了很多
class SwitchWithTextTableViewCell: UITableViewCell {
    func configure(withDelegate delagate: SwitchWithTextCellProtocol) {
        // 在這裡配置方法
    }
}
複製程式碼
  • 現在的檢視模型
struct MinionModeViewController: SwitchWithTextCellProtocol {
    var title = "excellent!!"
    var switchOn = true

    var switchColor: UIColor {
        return .yellowColor()
    }

    func onSwitchToggleOn(on: Bool) {
        if on {
            print("The Minions are here to stay!")
        } else {
            print("The Minions went out to play!")
        }
    }
}
複製程式碼
  • 現在,cellForRowAtIndexPath()也變得非常簡明瞭
let cell = tableView.dequeueReuseableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell

cell.configure(withDelegate: MinionModeViewModel())

return cell
複製程式碼

再把模型放在檢視模型層級,一遍對其進行跟蹤,再檢視模型中傳遞這些資訊,這樣單元格就可以生成了

  • 但是在這個基礎上,還可以再做進一步的深化,就是建立兩個協議,一個作為實際編碼的資料來源,比如標題內容之類的實際資料,一個作為單元格委託,儲存顏色、字型之類的並沒有包含實際資料的資訊,也就是仿照Apple中UITableView等集合檢視之類的地方,按照這種思維去建立單元格儲存和單元格委託
protocol SwitchWithTextCellDataSource {
    var title: String { get }
    var switchOn: Bool { get }
}

protocol SwitchWithTextCellDelegate {
    func onSwitchTogglenOn(on: Bool)

    var switchColor: UIColor { get }
    var textColor: UIColor { get }
    var font: UIFont { get }
}
複製程式碼
  • 接下來,再讓configure方法同時接受這兩個協議。因為委託可以全部在協議擴充套件中使用預設值進行配置,比如說字型、顏色之類的資訊,這樣在理論上就可以不用向裡面傳遞任何東西進去,只需要建立一個模型就可以了
// SwitchWithTextTableViewCell
func configure(withDataSource dataSource: SwitchWithTextCellDataSource, delegate: SwitchWithTextCellDelegate?) {
    // 在這裡配置檢視
}
複製程式碼
  • 然後就要開始通過擴充套件來改進檢視模型了,使用一個實際資料來源的程式碼塊,然後給定要傳遞的檢視當中的原始資訊
struct MinionModeViewModel: SwiftWithTextCellDataSource {
    var title = "Minion Mode!!"
    var switchOn = true
}
複製程式碼
  • 接下來會在一個單獨的檢視模型的部分使用處理字型、顏色之類的委託,然後在其中進行相關的配置
extension MinionModeViewModel: SwitchWithTextCellDelegate {

    var switchColor: UIColor {
        return .yellowColor()
    }

    func onSwitchToggleOn(on: Bool) {
        if on {
            print("The Minions are here to stay!")
        } else {
            print("The Minions went out to play!")
        }
    }
}
複製程式碼
  • 最終,表格檢視單元格變得非常簡單
// SettingViewController
let viewModel = MinionModeViewModel()
cell.configure(withDataSource:viewModel, delegate: viewModel)

return cell
複製程式碼

僅僅需要建立檢視模型,然後將其傳遞到配置方法當中,最後返回單元格,就可以了

20.1 Swift2.0中的 Minxin 和 Trait

  • 在遊戲開發中通常會有一個很龐大的層級關係,以及一系列的繼承,比如各種怪,繼承在這裡顯得十分有意義,但是隨著層級的擴充套件,這個專案就會變得凌亂起來

  • 比如說需要設計一個可以射擊的怪物,但這時候塔防頂部的大炮也會射擊,就需要把“射擊輔助類”提取出來,但是如果一直這樣提取子類,程式碼後面會一團亂麻

  • 將這個程式碼重構,不再去提取能夠射擊或者能夠加血的子類,而是將其提取為協議,通過協議擴充套件來實現這個功能,程式碼更加簡潔,更利於理解

// 一看這個物件的型別,就知道他有哪些功能,而不是一個個去查詢她的實現
class ZapMonster: GameObject, GunTraint, HealthTraint, MovementTraint {
    ...
}
複製程式碼
  • 雖然說這種設計模式是遊戲方面的,但是我們平時的程式碼也可以參考這種設計模式:這樣就不需要讓實際的單元格實現這個協議了,只需要將其根更廣泛的TextPresentable 聯絡在一起就可以了,這樣,任何擁有標籤的檢視,而不僅僅是單元格,都可以實現這個協議來彎沉相關的功能。這樣就可以說這個標籤有什麼樣的溫恩,什麼樣的顏色,以及什麼樣的字型
protocol TextPresentable {
    var text: String { get }
    var textColor: UIColor { get }
    var font: UIFont { get }
}

protocol SwitchPresentable {
    var switchOn: Bool { get }
    var switchColor: UIColor { get }

    func onSwitchToggleOn(on: Bool)
}
複製程式碼

這種情況下,比如需要一個圖片框,只要一個iamgeProtocol就可以了,設計師要求改所有標籤的顏色的話一行程式碼就可以搞定

  • 現在單元格的模樣
class SwitchWithTextTableViewCell<T where T: TextPresentable, T: SwitchPresentable>: UITableViewCell {
    private var delegate: T?

    // T是檢視模型
    func configure(withDelegate delegate: T) {
        // 在這裡配置檢視
    }
}
複製程式碼

在這種情況下,它沒有實現這些協議,但是會期待某種實現這些協議的東西傳遞進去,因此我們使用了泛型,這個單元格期待了一個實現了TextPresentableProtocol 的委託。就我們而言,傳遞進去的將是一個實現了這些協議的東西就可以了,現在要基於這些資訊在單元格當中配置所有的東西了,現在就可以基於浙西而資訊在單元格中配置所有的東西了

extension MinionModeViewModel: TextPresentable {
    var text: String { return "Minion Mode" }
    var textColor: UIColor { return .blackColor() }
    var font: UIFont { return .systemFontOfsize(17.0) }
}
複製程式碼
  • 我們的檢視模型將會有一個TextPresentable程式碼,在其中可以配置文字、顏色、字型,並且由於所有的這些協議擴充套件中都已經有預設值了,甚至不需要檢視模型去實現這些具體的內容

  • 最後,檢視模型當中的程式碼就只需要dequeue相應的單元格。然後通過檢視模型對其進行配置,然後返回單元格即可

21. 優化tableViewCell高度

  • 一種是針對所有 Cell 具有固定高度的情況,通過:self.tableView.rowHeight = 88; 指定了一個所有 cell 都是 88 高度的 UITableView,對於定高需求的表格,強烈建議使用這種(而非下面的)方式保證不必要的高度計算和呼叫。

  • 另一種方式就是實現 UITableViewDelegate 中的:heightForRowAtIndexPath:需要注意的是,實現了這個方法後,rowHeight 的設定將無效。所以,這個方法適用於具有多種 cell 高度的 UITableView。

  • iOS7之後出了了estimatedRowHeight,面對不同高度的cell,只要給一個預估的值就可以了,先給一個預估值,然後邊滑動邊計算,但是缺點就是

    • 設定估算高度以後,tableView的contentSize.height是根據cell高度預估值和cell的個數來計算的,導致導航條處於很不穩定的狀態,因為contentSize.height會逐漸由預估高度變為實際高度,很多情況下肉眼是可以看到導航條跳躍的
    • 如果是設計不好的上拉載入或下拉重新整理,有可能使表格滑動跳躍
    • 估算高度設計初衷是好的,讓載入速度更快,但是損失了流暢性,與其損失流暢性,我寧願讓使用者載入介面的時候多等那零點幾秒
  • iOS8 WWDC 中推出了 self-sizing cell 的概念,旨在讓 cell 自己負責自己的高度計算,使用 frame layout 和 auto layout 都可以享受到:

    • self.tableView.estimatedRowHeight = 213; self.tableView.rowHeight = UITableViewAutomaticDimension; 如果不加上估算高度的設定,自動算高就失效了
    • 這個自動算高在 push 到下一個頁面或者轉屏時會出現高度特別詭異的情況,不過現在的版本修復了。
  • 相同的程式碼在 iOS7 和 iOS8 上滑動順暢程度完全不同,iOS8 莫名奇妙的卡。很大一部分原因是 iOS8 上的算高機制大不相同,從 WWDC 也倒是能找到點解釋,cell 被認為隨時都可能改變高度(如從設定中調整動態字型大小),所以每次滑動出來後都要重新計算高度。

    • dequeueReusableCellWithIdentifier:forIndexPath: 相比不帶 “forIndexPath” 的版本會多呼叫一次高度計算
    • iOS7 計算高度後有”快取“機制,不會重複計算;而 iOS8 不論何時都會重新計算 cell 高度
  • 使用 UITableView+FDTemplateLayoutCell(百度知道負責人孫源) 無疑是解決算高問題的最佳實踐之一,既有 iOS8 self-sizing 功能簡單的 API,又可以達到 iOS7 流暢的滑動效果,還保持了最低支援 iOS6

  • FDTemplateLayoutCell 的高度預快取是一個優化功能,利用RunLoop空閒時間執行預快取任務計算,當使用者正在滑動列表時顯然不應該執行計算任務影響滑動體驗。

    • 當使用者正在滑動 UIScrollView 時,RunLoop 將切換到 UITrackingRunLoopMode 接受滑動手勢和處理滑動事件(包括減速和彈簧效果),此時,其他 Mode (除 NSRunLoopCommonModes 這個組合 Mode)下的事件將全部暫停執行,來保證滑動事件的優先處理,這也是 iOS 滑動順暢的重要原因
    • 註冊 RunLoopObserver 可以觀測當前 RunLoop 的執行狀態,並在狀態機切換時收到通知:
      • RunLoop開始
      • RunLoop即將處理Timer
      • RunLoop即將處理Source
      • RunLoop即將進入休眠狀態
      • RunLoop即將從休眠狀態被事件喚醒
      • RunLoop退出
    • 分解成多個RunLoop Source任務,假設列表有 20 個 cell,載入後展示了前 5 個,那麼開啟估算後 table view 只計算了這 5 個的高度,此時剩下 15 個就是“預快取”的任務,而我們並不希望這 15 個計算任務在同一個 RunLoop 迭代中同步執行,這樣會卡頓 UI,所以應該把它們分別分解到 15 個 RunLoop 迭代中執行,這時就需要手動向 RunLoop 中新增 Source 任務(由應用發起和處理的是 Source 0 任務)

23. 為什麼AFN顯示圖片不如SDWebImage流暢?同樣是從網路上下載圖片而不是從快取取圖片?

  • 因為SDWebImage有一個decoder
  • UIImage的imageWithData函式是每次畫圖的時候才將Data解壓成ARGB的影象
  • 所以每次畫圖的時候,會有一個解壓操作,這樣效率很低,但是隻有瞬時的記憶體需求
  • 為了提高效率通過SDWebImageDecoder將包裝在Data的資源解壓,然後畫在另外一張圖片上,這樣新的圖片就不再需要重複解壓了
  • 這是典型的空間換時間的做法

25. 我是怎樣用兩個imageView實現了無線輪播!

  1. 建立一個scrollView,設定contentsize為3*kWidth,contentOffSet為kWidth
  2. 接下來使用代理方法scrollViewDidScroll來監聽scrollview的滾動,定義一個列舉變數來記錄滾動的方向
  3. 使用KVO來監聽direction屬性值的改變-->[self addObserver:self forKeyPath:@"direction" options:NSKeyValueObservingOptionNew context:nil];
  4. 通過observeValueForKeyPath判斷滾動的方向,當偏移量大於x,表示左移,則將otherImageView加在右邊,偏移量小於x,表示右移,則將otherImageView加在左邊。同時判斷設定對應的索引,圖片
  5. 通過代理方法scrollViewDidEndDecelerating來監聽滾動結束,結束後,scrollview的偏移量為0或者2x,我們通過程式碼再次將scrollview的偏移量設定為x,並將currImageView的圖片修改為otherImageView的圖片,那麼我們看到的還是currImageView,只不過展示的是下一張圖片,如圖,又變成了最初的效果
  6. ,然後設定自動輪播,新增計時器,利用setContentOffset方法裡面setContentOffset:animated:方法執行完畢後不會呼叫scrollview的scrollViewDidEndDecelerating方法,但是會呼叫scrollViewDidEndScrollingAnimation方法,因此我們要在該方法中呼叫pauseScroll(即監聽減速結束後由otherImageView切換到currImageView的方法)
  7. 新增計時器:self.timer = [NSTimer timerWithTimeInterval:self.time target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
  8. 在scrollViewWillBeginDragging中停止計時器
  9. 在scrollViewDidEndDragging中開啟計時器
  10. 判斷外界傳入的是圖片還是路徑,如果是圖片,直接加入圖片陣列中,如果是路徑,先新增一個佔點陣圖片,然後根據路徑去下載圖片
  11. 監聽圖片被點選
    • 定義一個block屬性暴露給外界void(^imageClickBlock)(NSInteger index) (不會block的可以用代理,或者看這裡)
    • 設定currImageView的userInteractionEnabled為YES
    • 給currImageView新增一個點選的手勢
    • 在手勢方法裡呼叫block,並傳入圖片索引
  12. NSTimer的兩種形式
    • scheduledTimerWithTimeInterval 是建立一個定時器,並加入到當前執行迴圈[NSRunLoop currentRunLoop]中
    • 其他兩個([NSTimer timerWithTimeInterval:3 target:self selector:@selector(doSomeThing1) userInfo:nil repeats:YES]; [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:5] interval:3 target:self selector:@selector(doSomeThing2) userInfo:nil repeats:YES];)只是建立定時器,並未新增到當前執行迴圈中,所以如果是其他兩種方式建立的定時器則需要手動新增到currentRunLoop中
  • NSTimer是普通的定時器,如果系統繁忙,重新整理可能會被延遲。但是CADisplaylink實時重新整理,跟著螢幕的重新整理頻率實時重新整理,60次/s,與螢幕重新整理頻率相同

26. tableView的優化

iOS平臺因為UIKit本身的特性,需要將所有的UI操作都放在主執行緒執行,所以有時候就習慣將一些執行緒安全性不確定的邏輯,以及它執行緒結束後的彙總工作等等放到了主執行緒,所以主執行緒包含大量計算、IO、繪製都有可能造成卡頓。

  • 可以通過監控runLoop監控監控卡頓,呼叫方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之後,也就是如果我們發現這兩個時間內耗時太長,那麼就可以判定出此時主執行緒卡頓.
  • 使用到CFRunLoopObserverRef,通過它可以實時獲得這些狀態值的變化
  • 監控後另外再開啟一個執行緒,實時計算這兩個狀態區域之間的耗時是否到達某個閥值,便能揪出這些效能殺手.
  • 監控到了卡頓現場,當然下一步便是記錄此時的函式呼叫資訊,此處可以使用一個第三方Crash收集元件PLCrashReporter,它不僅可以收集Crash資訊也可用於實時獲取各執行緒的呼叫堆疊
  • 當檢測到卡頓時,抓取堆疊資訊,然後在客戶端做一些過濾處理,便可以上報到伺服器,通過收集一定量的卡頓資料後經過分析便能準確定位需要優化的邏輯
  1. 設定正確的 reuseidentifer 以重用 cell

  2. 儘量將 View 設定為不透明,包括 cell 本身(backgroundcolor預設是透明的),圖層混合靠GPU去渲染,如果透明度設定為100%,那麼GPU就會忽略下面所有的layer,節約了很多不必要的運算。模擬器上點選“Debug”選單,然後選擇“color Blended Layers”,會把所有區域分成綠色和紅色,綠色的好,紅色的效能差(經過混合渲染的),當然也有一些圖片雖然是不透明的,但是也會顯示紅色,如果檢查程式碼沒錯的話,一般就是圖片自身的性質問題了,直接聯絡美工或後臺解決就好了。除非必須要用GPU載入的,其他最好要用CPU載入,因為CPU一般不會百分百載入,可以通過CoreGraphics畫出圓角

  3. 有時候美工失誤,圖片大小給錯了,引起不必要的圖片縮放(可以找美工去改,當然也可以非同步去裁剪圖片然後快取下來),還是使用Instrument的Color Misaligned Images,黃色表示圖片需要縮放,紫色表示沒有畫素對齊。當然一般情況下圖片格式不會給錯,有些圖片格式是GPU不支援的,就還要勞煩CPU去進行格式轉換。還有可以通過Color Offscreen-Rendered Yellow來檢測離屏渲染(就是把渲染結果臨時儲存,等到用的時候再取出,這樣相對於普通渲染更消耗記憶體,使用maskToBounds、設定shadow,重寫drawRect方法都會導致離屏渲染) 避免漸變,cornerRadius在預設情況下,這個屬性只會影響檢視的背景顏色和 border,但是不會離屏繪製,不影響效能。不用clipsToBounds(過多呼叫GPU去離屏渲染),而是讓後臺載入圖片並處理圓角,並將處理過的圖片賦值給UIImageView。UIImageView 的圓角通過直接擷取圖片實現,圓角路徑直接用貝塞爾曲線UIBezierPath繪製(人為指定路徑之後就不會觸發離屏渲染),UIGraphicsBeginImageContextWithOptions。UIView的圓角可以使用CoreGraphics畫出圓角矩形,核心是CGContextAddArcToPoint 函式。它中間的四個參數列示曲線的起點和終點座標,最後一個參數列示半徑。呼叫了四次函式後,就可以畫出圓角矩形。最後再從當前的繪圖上下文中獲取圖片並返回,最後把這個圖片插入到檢視層級的底部。 “Flash updated Regions”用於標記發生重繪的區域

  4. 如果 row 的高度不相同,那麼將其快取下來

  5. 如果 cell 顯示的內容來自網路,那麼確保這些內容是通過非同步下載

  6. 使用 shadowPath 來設定陰影,圖層最好不要使用陰影,陰影會導致離屏渲染(在進入螢幕渲染之前,還看不到的時候會再渲染一次,儘量不要產生離屏渲染)

  7. 減少 subview 的數量,不要去新增或移除view,要就顯示,不要就隱藏

  8. 在 cellForRowAtIndexPath 中儘量做更少的操作,最好是在別的地方算好,這個方法裡只做資料的顯示,如果需要做一些處理,那麼最好做一次之後將結果儲存起來.

  9. 使用適當的資料結構來儲存需要的資訊,不同的結構會帶來不同的操作代價

  10. 使用,rowHeight , sectionFooterHeight 和 sectionHeaderHeight 來設定一個恆定高度 , 而不是從代理(delegate)中獲取

  11. cell做資料繫結的時候,最好在willDisPlayCell裡面進行,其他操作在cellForRowAtIndexPath,因為前者是第一頁有多少條就執行多少次,後者是第一次載入有多少個cell就執行多少次,而且呼叫後者的時候cell還沒顯示

  12. 讀取檔案,寫入檔案,最好是放到子執行緒,或先讀取好,在讓tableView去顯示

  13. tableView滾動的時候,不要去做動畫(微信的聊天介面做的就很好,在滾動的時候,動態圖就不讓他動,滾動停止的時候才動,不然可能會有點影響流暢度)。在滾動的時候載入圖片,停止拖拽後在減速過程中不載入圖片,減速停止後載入可見範圍內圖片

27. 談談記憶體的優化和注意事項(使用Instrument工具的CoreAnimation、GPU Driver、I/O操作,檢查fps數值)

  • 重用問題:比如UITableViewCell、UICollectionViewCell、UITableViewHeaderFooterViews等設定正確的reuseIdentifier,充分重用

  • 懶載入控制元件、頁面:對於不是立刻使用的資料,都應該使用延遲載入的方式,比如網路連線失敗的提示介面,可能一直都用不到

  • 使用Autorelease Pool:在某些迴圈建立臨時變數處理資料時,自動釋放池以保證能及時釋放記憶體

  • 不要使用太多的xib/storyboard:載入時會將其內部的圖片在內的所有資源載入記憶體,即使未來很久才會需要使用,相對於純程式碼寫的延遲載入,在效能和記憶體上就差了很多

  • 資料快取:對於cell的行高要快取起來,使用reloadData效率也極高,對於網路資料,不需要每次都請求的,應該快取起來,可以寫入資料庫,也可以通過plist檔案儲存

  • 選擇正確的資料結構:針對不同的業務場景選擇最合適的資料結構是寫出高效程式碼的基礎

    • 陣列:有序的一組值,使用索引查詢起來很快,使用值查詢的很慢,插入/刪除 很慢
    • 字典:儲存鍵值對對,用鍵查詢比較快
    • 集合:無序的一組值,用值來查詢很快,插入/刪除很快
  • gzip/zip壓縮:當從伺服器下載相關附件時,可以通過 zip壓縮後再下載,使得記憶體更小,下載速度也更快

  • 重大開銷物件:一些objects的初始化很慢,比如NSDateFormatter和 NSCalendar,但是又無可避免的需要使用,通常作為屬性儲存起來,避免反覆使用

  • 避免反覆處理資料:需要應用需要從伺服器載入資料,常為JSON或者XML格式的資料,在伺服器端或者客戶端使用相同的資料結構很重要

  • 選擇圖片時,要對圖片進行壓縮處理,根據不同的情況選擇不同的圖片載入方式,-imageNamed:讀取到記憶體後會快取下來,適合圖片資源較小,使用很頻繁的圖片;-initWithContentsOfFiles:僅載入圖片而不快取,適合較大的圖片。若是collectionView中使用大量圖片的時候,可以用UIVIew.layer.contents=(__bridge id _Nullable)(model.clipedImage.CGImage);這樣就更輕量級一些

  • 當然有時候也會用到一些第三方,比如在使用UICollectionView和UITableView的時候,Facebook有一個框架叫AsyncDisplayKit,這個庫就可以很好地提升滾動時流暢性以及圖片非同步下載功能(不支援sb和autoLayout,需要手動進行約束設定),AsyncDisplayKit用相關node類,替換了UIView和它的子類,而且是執行緒安全的。它可以非同步解碼圖片,調整圖片大小以及對圖片和文字進行渲染,把這些操作都放到子執行緒,滑動的時候就流暢許多。我認為這個庫最方便的就是實現圖片非同步解碼。UIImage顯示之前必須要先解碼完成,而且解碼還是同步的。尤其是在UICollectionView/UITableView 中使用 prototype cell顯示大圖,UIImage的同步解碼在滾動的時候會有明顯的卡頓。另外一個很吸引人的點是AsyncDisplayKit可以把view層次結構轉成layer。因為複雜的view層次結構開銷很大,如果不需要view特有的功能(例如點選事件),就可以使用AsyncDisplayKit 的layer backing特性從而獲得一些額外的提升。當然這個庫還處於開發階段,還有一些地方地方有待完善,比如不支援快取,我要使用這個庫的時候一般是結合Alamofire和AlamofireImage實現圖片的快取

希望此文對您的求職或夯實基礎起到作用,感謝閱讀!

相關文章