備戰(zhàn)金三銀四,2021最全100道大廠高頻iOS面試題分享上(含答案)

原作者:執(zhí)筆續(xù)春秋

iOS面試題

本面試題為個人使用版本,如后續(xù)流傳出去,請轉(zhuǎn)發(fā)的朋友務(wù)必注釋一下,答案正確性有待商榷,本人的答案不代表權(quán)威,僅僅是個人理解。 文章內(nèi)部有寫混亂,將就著看吧。另外大部分圖片加載不出來,,MARKDown格式也不太統(tǒng)一(各平臺不一樣),由于博主太懶不想改,不過不影響最終效果,

一、硬技術(shù)篇

1.對象方法和類方法的區(qū)別?

  • 對象方法能個訪問成員變量。
  • 類方法中不能直接調(diào)用對象方法,想要調(diào)用對象方法,必須創(chuàng)建或者傳入對象。
  • 類方法可以和對象方法重名。

引伸1. 如果在類方法中調(diào)用self 會有什么問題?

  • 在 實例方法中self不可以調(diào)用類方法,此時的self不是Class。
  • 在類方法中self可以調(diào)用其他類方法。
  • 在類方法中self不可以調(diào)用實例方法。
  • 總結(jié):類方法中的self,是class/ 實例方法中self是對象的首地址。

引申2. 講一下對象,類對象,元類,跟元類結(jié)構(gòu)體的組成以及他們是如何相關(guān)聯(lián)的?

  • 對象的結(jié)構(gòu)體當(dāng)中存放著isa指針和成員變量,isa指針指向類對象
  • 類對象的isa指針指向元類,元類的isa指針指向NSObject的元類
  • 類對象和元類的結(jié)構(gòu)體有isa,superClass,cache等等

作為一個開發(fā)者,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:834688868,不管你是大牛還是小白都歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗,討論技術(shù), 大家一起交流學(xué)習(xí)成長!

以下資料在群文件可自行下載

引申3. 為什么對象方法中沒有保存對象結(jié)構(gòu)體里面,而是保存在類對象的結(jié)構(gòu)體里面?

  • 方法是每個對象相互可以共用的,如果每個對象都存儲一份方法列表太浪費內(nèi)存,由于對象的isa是指向類對象的,當(dāng)調(diào)用的時候, 直接去類對象中去查找就可以了,節(jié)約了很多內(nèi)存空間。

引申4. 類方法存在哪里? 為什么要有元類的存在?

  • 所有的類自身也是一個對象,我們可以向這個對象發(fā)送消息(即調(diào)用類方法)。 為了調(diào)用類方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結(jié)構(gòu)體。這就引出了meta-class的概念,元類中保存了創(chuàng)建類對象以及類方法所需的所有信息。

引申5. 什么是野指針?

  • 野指針就是指向一個被釋放或者被收回的對象,但是對指向該對象的指針沒有做任何修改,以至于該指針讓指向已經(jīng)回收后的內(nèi)存地址。
  • 其中訪問野指針是沒有問題的,使用野指針的時候會出現(xiàn)崩潰Crash!樣例如下
  __unsafe_unretained UIView *testObj = [[UIView alloc] init];
   NSLog(@"testObj 指針指向的地址:%p 指針本身的地址:%p", testObj, &testObj);
   [testObj setNeedsLayout];
   // 可以看到NSlog打印不會閃退,調(diào)用[testObj setNeedsLayout];會閃退

引申6. 如何檢測野指針?

這是網(wǎng)友總結(jié)的,有興趣的可以看下:m.itdecent.cn/p/9fd4dc046… 本人,也就是看看樂呵,其原理啥的,見仁見智吧。開發(fā)行業(yè)太j8難了!

引申7. 導(dǎo)致Crash的原因有哪些?

1、找不到方法的實現(xiàn)unrecognized selector sent to instance 2、KVC造成的crash 3、EXC_BAD_ACCESS 4、KVO引起的崩潰 5、集合類相關(guān)崩潰 6、多線程中的崩潰 7、Socket長連接,進入后臺沒有關(guān)閉 8、Watch Dog超時造成的crash 9、后臺返回NSNull導(dǎo)致的崩潰,多見于Java做后臺服務(wù)器開發(fā)語言

引申8. 不使用第三方,如何知道已經(jīng)上線的App崩潰問題, 具體到哪一個類的哪一個方法的?

大致實現(xiàn)方式如下。

  • 使用NSSetUncaughtExceptionHandler可以統(tǒng)計閃退的信息。
  • 將統(tǒng)計到的信息以data的形式 利用網(wǎng)絡(luò)請求發(fā)給后臺
  • 在后臺收集信息,進行排查
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // Override point for customization after application launch.

        NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
        return YES;
    }

    static void my_uncaught_exception_handler (NSException *exception) {
        //這里可以取到 NSException 信息
        NSLog(@"***********************************************");
        NSLog(@"%@",exception);
        NSLog(@"%@",exception.callStackReturnAddresses);
        NSLog(@"%@",exception.callStackSymbols);
        NSLog(@"***********************************************");
    }

實現(xiàn)方式如: blog.csdn.net/u013896628/…

iOS中內(nèi)省的幾個方法?

  • isMemberOfClass //對象是否是某個類型的對象
  • isKindOfClass //對象是否是某個類型或某個類型子類的對象
  • isSubclassOfClass //某個類對象是否是另一個類型的子類
  • isAncestorOfObject //某個類對象是否是另一個類型的父類
  • respondsToSelector //是否能響應(yīng)某個方法
  • conformsToProtocol //是否遵循某個協(xié)議

引申 2. ==、 isEqualToString、isEqual區(qū)別?

  • == ,比較的是兩個指針的值 (內(nèi)存地址是否相同)。
  • isEqualToString, 比較的是兩個字符串是否相等。
  • isEqual 判斷兩個對象在類型和值上是否都一樣。

引申 3. class方法和object_getClass方法有什么區(qū)別?

  • 實例class方法直接返回object_getClass(self)
  • 類class直接返回self
  • 而object_getClass(類對象),則返回的是元類

3.深拷貝和淺拷貝

  • 所謂深淺指的是是否創(chuàng)建了一個新的對象(開辟了新的內(nèi)存地址)還是僅僅做了指針的復(fù)制。
  • copy和mutableCopy針對的是可變和不可變,凡涉及copy結(jié)果均變成不可變,mutableCopy均變成可變。
  • mutableCopy均是深復(fù)制。
  • copy操作不可變的是淺復(fù)制,操作可變的是深賦值。

4.NSString類型為什么要用copy修飾 ?

  • 主要是防止NSString被修改,如果沒有修改的說法用Strong也行。
  • 當(dāng)NSString的賦值來源是NSString時,strong和copy作用相同。
  • 當(dāng)NSString的賦值來源是NSMutableString,copy會做深拷貝,重新生成一個新的對象,修改賦值來源不會影響NSString的值。

5.iOS中block 捕獲外部局部變量實際上發(fā)生了什么?__block 中又做了什么?

  • block 捕獲的是當(dāng)前在block內(nèi)部執(zhí)行的外部局部變量的瞬時值, 為什么說瞬時值呢? 看一下C++源碼中得知, 其內(nèi)部代碼在捕獲的同時

  • 其實block底層生成了一個和外部變量相同名稱的屬性值如果內(nèi)部修改值,其實修改的是捕獲之前的值,其捕獲的內(nèi)部的值因代碼只做了一次捕獲,并沒有做再一次的捕獲,所以block里面不可以修改值。

  • 如果當(dāng)前捕獲的為對象類型,其block內(nèi)部可以認為重新創(chuàng)建了一個指向當(dāng)前對象內(nèi)存地址的指針(堆),操控內(nèi)部操作的東西均為同一塊內(nèi)存地址,所以可以修改當(dāng)前內(nèi)部的對象里面的屬性,但是不能直接修改當(dāng)前的指針(無法直接修改棧中的內(nèi)容)(即重新生成一個新的內(nèi)存地址)。其原理和捕獲基本數(shù)據(jù)類型一致。

  • 說白了, block內(nèi)部可以修改的是堆中的內(nèi)容, 但不能直接修改棧中的任何東西。


  • 如果加上__block 在運行時創(chuàng)建了一個外部變量的“副本”屬性,把棧中的內(nèi)存地址放到了堆中進而在block內(nèi)部也能修改外部變量的值。

6.iOS Block為什么用copy修飾?

  • block 是一個對象
  • MRC的時候 block 在創(chuàng)建的時候,它的內(nèi)存比較奇葩,非得分配到棧上,而不是在傳統(tǒng)的堆上,它本身的作用于就屬于創(chuàng)建的時候(見光死,夭折),一旦在創(chuàng)建時候的作用于外面調(diào)用它會導(dǎo)致崩潰。
  • 所以,利用copy把原本在棧上的復(fù)制到堆里面,就保住了它。
  • **ARC的時候 由于ARC中已經(jīng)看不到棧中的block了。用strong和copy 一樣 隨意, 用copy是遵循其傳統(tǒng), **

7. 為什么分類中不能創(chuàng)建屬性Property(runtime除外)?

  • 分類的實現(xiàn)原理是將category中的方法,屬性,協(xié)議數(shù)據(jù)放在category_t結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對象的方法列表中。 Category可以添加屬性,但是并不會自動生成成員變量及set/get方法。因為category_t結(jié)構(gòu)體中并不存在成員變量。通過之前對對象的分析我們知道成員變量是存放在實例對象中的,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運行時才去加載的。那么我們就無法再程序運行時將分類的成員變量中添加到實例對象的結(jié)構(gòu)體中。因此分類中不可以添加成員變量。

  • 在往深一點的回答就是 類在內(nèi)存中的位置是編譯時期決定的, 之后再修改代碼也不會改變內(nèi)存中的位置,class_ro_t 的屬性在運行期間就不能再改變了, 再添加方法是會修改class_rw_t 的methods 而不是class_ro_t 中的 baseMethods

引伸:分類可以添加那些內(nèi)容?
  • 實例方法,類方法,協(xié)議,屬性
引伸:Category 的實現(xiàn)原理?
  • Category 在剛剛編譯完成的時候, 和原來的類是分開的,只有在程序運行起來的時候, 通過runtime合并在一起。
引申 使用runtime Associate方法關(guān)聯(lián)的對象,需要在主對象dealloc的時候釋放么?
  • 不需要,被關(guān)聯(lián)的對象的生命周期內(nèi)要比對象本身釋放晚很多, 它們會在被 NSObject -dealloc 調(diào)用的 object_dispose() 方法中釋放。
引申 能否向編譯后得到的類中增加實例變量, 能否向運行時創(chuàng)建的類中添加實力變量?
  • 不能再編譯后得到的類中增加實例變量。因為編譯后的類已經(jīng)注冊在runtime中, 類結(jié)構(gòu)體中objc_ivar_list 實例變量的鏈表和objc_ivar_list 實例變量的內(nèi)存大小已經(jīng)確定,所以不能向存在的類中添加實例變量
  • 能在運行時創(chuàng)建的類中添加實力變量。調(diào)用class_addIvar 函數(shù)
引申 主類執(zhí)行了foo方法,分類也執(zhí)行了foo方法,在執(zhí)行的地方執(zhí)行了foo方法,主類的foo會被覆蓋么? 如果想只想執(zhí)行主類的foo方法,如何去做?
  • 主類的方法被分類的foo覆蓋了,其實分類并沒有覆蓋主類的foo方法,只是分類的方法排在方法列表前面,主類的方法列表被擠到了后面, 調(diào)用的時候會首先找到第一次出現(xiàn)的方法。
  • 如果想要只是執(zhí)行主類的方法,可逆序遍歷方法列表,第一次遍歷到的foo方法就是主類的方法
- (void)foo{   
  [類 invokeOriginalMethod:self selector:_cmd];
}

+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector {
    uint count;
    Method *list = class_copyMethodList([target class], &count);
    for ( int i = count - 1 ; i >= 0; i--) {
        Method method = list[i];
        SEL name = method_getName(method);
        IMP imp = method_getImplementation(method);
        if (name == selector) {
            ((void (*)(id, SEL))imp)(target, name);
            break;
        }
    }
    free(list);
}


8. load 和 initilze 的調(diào)用情況,以及子類的調(diào)用順序問題?

  • initialize 這個方法是第一次給某給類發(fā)送消息的時候調(diào)用,并且只會調(diào)用一次。 如果某一個類一直沒有被用到,此方法也不會執(zhí)行。
  • initialize先初始化父類, 在初始化子類,子類的initialize 會覆蓋父類的方法。
  • 分類中實現(xiàn)了initialize會覆蓋本來的initialize方法,如果多個分類都執(zhí)行了initialize ,那么只是執(zhí)行最后編譯的那個。

  • load當(dāng)程序被加載的時候就會調(diào)用, 其加載順序為, 如果子類實現(xiàn)類load 先執(zhí)行父類 -> 在執(zhí)行子類,而分類的在最后執(zhí)行。
  • 如果子類不實現(xiàn)load,父類的load就不會被執(zhí)行。
  • load是線程安全的,其內(nèi)部使用了鎖,所以我們應(yīng)該避免在load方法中線程阻塞。
  • load在分類中,重寫了load方法, 不會影響其主類的方法。即不會覆蓋本類的load方法
  • 當(dāng)有多個類的時候,每個類的load的執(zhí)行順序和編譯順序一致。

9. 什么是線程安全?

  • 多條線程同時訪問一段代碼,不會造成數(shù)據(jù)混亂的情況

10. 你接觸到的項目,哪些場景運用到了線程安全?

答: 舉例說明,12306 同一列火車的車票, 同一時間段多人搶票! 如何解決 互斥鎖使用格式

synchronized(鎖對象) { // 需要鎖定的代碼  }
注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的

Tips: 互斥鎖的優(yōu)缺點
優(yōu)點:能有效防止因多線程搶奪資源造成的數(shù)據(jù)安全問題
缺點:需要消耗大量的CPU資源

互斥鎖的使用前提:多條線程搶奪同一塊資源 
相關(guān)專業(yè)術(shù)語:線程同步,多條線程按順序地執(zhí)行任務(wù)
互斥鎖,就是使用了線程同步技術(shù)

Objective-C中的原子和非原子屬性
OC在定義屬性時有nonatomic和atomic兩種選擇
atomic:原子屬性,為setter/getter方法都加鎖(默認就是atomic)
nonatomic:非原子屬性,不加鎖

atomic加鎖原理:
property (assign, atomic) int age;
 - (void)setAge:(int)age
{ 
    @synchronized(self) {  
       _age = age;
    }
}

- (int)age {
    int age1 = 0;
    @synchronized(self) {
        age1 = _age;
    }
}

原子和非原子屬性的選擇
nonatomic和atomic對比
atomic:線程安全,需要消耗大量的資源
nonatomic:非線程安全,適合內(nèi)存小的移動設(shè)備

iOS開發(fā)的建議
所有屬性都聲明為nonatomic
盡量避免多線程搶奪同一塊資源
盡量將加鎖、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動客戶端的壓力

atomic就一定能保證線程安全么?
不能,還需要更深層的鎖定機制才可以,因為一個線程在連續(xù)多次讀取某條屬性值的時候,與此同時別的線程也在改寫值,這樣還是會讀取到不同的屬性值!  或者 一個線程在獲取當(dāng)前屬性的值, 另外一個線程把這個屬性釋放調(diào)了, 有可能造成崩潰

  • 另外整理了一份 2021年《大廠最新常問iOS面試題+答案》,有需要,直接加iOS技術(shù)交流群:834688868,免費獲取;群內(nèi)更有內(nèi)推機會!
![image](//upload-images.jianshu.io/upload_images/18569867-6d3f854fb97ebc5d.png?imageMogr2/auto-orient/strip|imageView2/2/w/720/format/webp)

11. 你實現(xiàn)過單例模式么? 你能用幾種實現(xiàn)方案?

1. 運用GCD:
import "Manager.h"
implementation Manager
+ (Manager *)sharedManager {
  static dispatch_once_t onceToken;
  static Manager * sharedManager;
  dispatch_once(&onceToken, ^{
    sharedManager=[[Manager alloc] init];
  });
  return sharedManager;
}
end
注明:dispatch_once這個函數(shù),它可以保證整個應(yīng)用程序生命周期中某段代碼只被執(zhí)行一次!

2. 不使用GCD的方式:
static Manager *manager;
implementation Manager
+ (Manager *)defaultManager {
    if(!manager)
        manager=[[self allocWithZone:NULL] init];
    return  manager;
}
end

3. 正常的完整版本
+(id)shareInstance{
     static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      if(_instance == nil)
            _instance = [MyClass alloc] init]; 
    });
     return _instance;
}

//重寫allocWithZone,里面實現(xiàn)跟方法一,方法二一致就行.
+(id)allocWithZone:(struct _NSZone *)zone{
   return [self shareInstance];
} 

//保證copy時相同
-(id)copyWithZone:(NSZone *)zone{  
    return _instance;  
} 
//  方法3創(chuàng)建的目的的是  為了方式開發(fā)者在調(diào)用單例的時候并沒有用shareInstance方法來創(chuàng)建 而是用的alloc  或者copy的形式創(chuàng)建造成單例不一致的情況

//   

引申1. 單例是怎么銷毀的?

//必須把static dispatch_once_t onceToken; 這個拿到函數(shù)體外,成為全局的.
+ (void)attempDealloc {
    onceToken = 0; // 只有置成0,GCD才會認為它從未執(zhí)行過.它默認為0,這樣才能保證下次再次調(diào)用shareInstance的時候,再次創(chuàng)建對象.
    _sharedInstance = nil;
}

dispatch_once_t 的工作原理是,static修飾會默認將其初始化為0, 當(dāng)且僅當(dāng)其為0的時候dispatch_once(&onceToken, ^{})這個函數(shù)才能被調(diào)用, 如果執(zhí)行了這個函數(shù)  這個dispatch_once_t 靜態(tài)變成- 1了  就永遠不會被調(diào)用

引申2. 不使用dispatch_once 如何 實現(xiàn)單例


1.第一種方式,重寫+allocWithZone:方法;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    static id instance = nil;
    @synchronized (self) { // 互斥鎖
        if (instance == nil) {
            instance = [super allocWithZone:zone];
        }
    }
    return instance;
}

2.第二種方式,不用重寫+allocWithZone:方法,而是直接用@synchronized 來保證線程安全,其它與上面這個方法一樣;
+ (instancetype)sharedSingleton {
    static id instance = nil;
    @synchronized (self) {
        if (!instance) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}


12. 項目開發(fā)中,你用單例都做了什么?

答 :整個程序公用一份資源的時候 例如 :

  • 設(shè)置單例類訪問應(yīng)用的配置信息
  • 用戶的個人信息登錄后用的NSUserDefaults存儲,對登錄類進一步采用單例封裝方便全局訪問
  • 防止一個單例對 應(yīng)用 多處 對同意本地數(shù)據(jù)庫存進行操作

13.APNS的基本原理

  • 基本
  • 第一階段:應(yīng)用程序的服務(wù)器端把要發(fā)送的消息、目的iPhone的標(biāo)識打包,發(fā)給APNS。
  • 第二階段:APNS在自身的已注冊Push服務(wù)的iPhone列表中,查找有相應(yīng)標(biāo)識的iPhone,并把消息發(fā)送到iPhone。
  • 第三階段:iPhone把發(fā)來的消息傳遞給相應(yīng)的應(yīng)用程序,并且按照設(shè)定彈出Push通知。
  • 詳細說明

首先是注冊

  • Device(設(shè)備)連接APNs服務(wù)器并攜帶設(shè)備序列號(UUID)
  • 連接成功,APNs經(jīng)過打包和處理產(chǎn)生devicetoken并返回給注冊的Device(設(shè)備)
  • Device(設(shè)備)攜帶獲取的devicetoken發(fā)送到我們自己的應(yīng)用服務(wù)器
  • 完成需要被推送的Device(設(shè)備)在APNs服務(wù)器和我們自己的應(yīng)用服務(wù)器的注冊

推送過程

  • 1、首先手機裝有當(dāng)前的app,并且保持有網(wǎng)絡(luò)的情況下,APNs服務(wù)器會驗證devicetoken,成功那個之后會處于一個長連接。 (這里會有面試問? 如果app也注冊成功了, 也下載了,也同意了打開推送功能, 這個時候在把App刪除了, 還能接受推送了么? )
  • 2、當(dāng)我們推送消息的時候,我們的服務(wù)器按照指定格式進行打包,結(jié)合devicetoken 一起發(fā)送給APNs服務(wù)器,
  • 3、APNs 服務(wù)器將新消息推送到iOS 設(shè)備上,然后在設(shè)備屏幕上顯示出推送的消息。
  • 4、iOS設(shè)備收到推送消息后, 會通知給我們的應(yīng)用程序并給予提示

// 推送過程如下圖

image

14. RunLoop的基礎(chǔ)知識

  • RunLoop模式有哪些?

答 : iOS中有五種RunLoop模式

NSDefaultRunLoopMode (默認模式,有事件響應(yīng)的時候,會阻塞舊事件)
NSRunLoopCommonModes (普通模式,不會影響任何事件)
UITrackingRunLoopMode (只能是有事件的時候才會響應(yīng)的模式)

還有兩種系統(tǒng)級別的模式
一個是app剛啟動的時候會執(zhí)行一次
另外一個是系統(tǒng)檢測app各種事件的模式

  • RunLoop的基本執(zhí)行原理

答 : 原本系統(tǒng)就有一個runloop在檢測App內(nèi)部的行為或事件,當(dāng)輸入源(用戶的直接或者間接的操作)有“執(zhí)行操作”的時候, 系統(tǒng)的runloop會監(jiān)聽輸入源的狀態(tài), 進而在系統(tǒng)內(nèi)部做一些對應(yīng)的相應(yīng)操作。 處理完成后,會自動回到睡眠狀態(tài), 等待下一次被喚醒,

  • RunLoop和線程的關(guān)系

  • RunLoop的作用就是用來管理線程的, 當(dāng)線程的RunLoop開啟之后,線程就會在執(zhí)行完成任務(wù)后,進入休眠狀態(tài),隨時等待接收新的任務(wù),而不是退出。

  • 為什么只有主線程的runloop是開啟的

  • 程序開啟之后,要一直運行,不會退出。 說白了就是為了讓程序不死


如何保證一個線程永遠不死(常駐線程)

    // 先創(chuàng)建一個線程用于測試
     NSThread *thread = [[NSThread alloc]  initWithTarget:self selector:@selector(play) object:nil];
    [thread start];

    // 保證一個線程永遠不死
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] -forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

    // 在合適的地方處理線程的事件處理
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];

15. weak屬性?

1\. 說說你理解weak屬性?

1.實現(xiàn)weak后,為什么對象釋放后會自動為nil?

runtime 對注冊的類, 會進行布局,
對于 weak 對象會放入一個 hash 表中。 
用 weak 指向的對象內(nèi)存地址作為 key,
Value是weak指針的地址數(shù)組。
當(dāng)釋放的時候,其內(nèi)部會通過當(dāng)前的key找到所有的weak指針指向的數(shù)組
然后遍歷這個數(shù)組把其中的數(shù)據(jù)設(shè)置為nil。

稍微詳細的說:在內(nèi)部底層源碼也同時和當(dāng)前對象相關(guān)聯(lián)得SideTable, 其內(nèi)部有三個屬性, 一個是一把自旋鎖,一個是引用計數(shù)器相關(guān),一個是維護weak生命得屬性得表
**SideTable**這個結(jié)構(gòu)體一樣的東西,可以花半個小時看一眼。

延伸

  • objc中向一個nil對象發(fā)送消息將會發(fā)生什么? 首先 在尋找對象化的isa指針時就是0地址返回了, 所以不會有任何錯誤, 也不會錯誤

  • objc在向一個對象發(fā)送消息時,發(fā)生了什么?

  - 首先是通過obj 的isa指針找到對應(yīng)的class
  - 先去操作對象中的緩存方法列表中objc_cache中去尋找 當(dāng)前方法,如果找到就直接實現(xiàn)對應(yīng)IMP
  - 如果在緩存中找不到,則在class中找到對用的Method list中對用foo
  - 如果class中沒有找到對應(yīng)的foo, 就會去superClass中去找
  - 如果找到了對應(yīng)的foo, 就會實現(xiàn)foo對應(yīng)的IMP

  緩存方法列表, 就是每次執(zhí)行這個方法的時候都會做如此繁瑣的操作這樣太過于消耗性能,所以出現(xiàn)了一個objc_cache,這個會把當(dāng)前調(diào)用過的類中的方法做一個緩存, 當(dāng)前method_name作為key, method_IMP作為Value,當(dāng)再一次接收到消息的時候,直接通過objc_cache去找到對應(yīng)的foo的IMP即可, 避免每一次都去遍歷objc_method_list

如果一直沒有找到方法, 就會專用消息轉(zhuǎn)發(fā)機制,機制如下

// 動態(tài)方法解析和轉(zhuǎn)發(fā)
上面的例子如果foo函數(shù)一直沒有被找到,通常情況下,會出現(xiàn)報錯,但是在報錯之前,OC的運行時給了我們?nèi)窝a救的機會

- Method resolution
- Fast forwarding
- Normal forwarding

1. Runtime 會發(fā)送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve(重啟) 這個消息;
2. 如果 resolve 方法返回 NO,Runtime 就發(fā)送 -forwardingTargetForSelector: 允許你把這個消息轉(zhuǎn)發(fā)給另一個對象;
3. 如果沒有新的目標(biāo)對象返回, Runtime 就會發(fā)送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以發(fā)送 -invokeWithTarget: 消息來手動轉(zhuǎn)發(fā)消息或者發(fā)送 -doesNotRecognizeSelector: 拋出異常。

16.UIView和CALayer是什么關(guān)系?

    - 兩者最明顯的區(qū)別是 View可以接受并處理事件,而 Layer 不可以;
    - 每個 UIView 內(nèi)部都有一個 CALayer 在背后提供內(nèi)容的繪制和顯示,并且 UIView 的尺寸樣式都由內(nèi)部的 Layer 所提供。兩者都有樹狀層級結(jié)構(gòu),layer 內(nèi)部有 SubLayers,View 內(nèi)部有 SubViews.但是 Layer 比 View 多了個AnchorPoint
    - 在 View顯示的時候,UIView 做為 Layer 的 CALayerDelegate,View 的顯示內(nèi)容由內(nèi)部的 CALayer 的 display 
CALayer 是默認修改屬性支持隱式動畫的,在給 UIView 的 Layer 做動畫的時候,View 作為 Layer 的代理,Layer 通過 actionForLayer:forKey:向 View請求相應(yīng)的 action(動畫行為)
    - layer 內(nèi)部維護著三分 layer tree,分別是 presentLayer Tree(動畫樹),modeLayer Tree(模型樹), Render Tree (渲染樹),在做 iOS動畫的時候,我們修改動畫的屬性,在動畫的其實是 Layer 的 presentLayer的屬性值,而最終展示在界面上的其實是提供 View的modelLayer


16. @synthesize 和 @dynamic 分別有什么作用

- @property有兩個對應(yīng)的詞,一個是 @synthesize,一個是 @dynamic。如果 @synthesize和 @dynamic都沒寫,那么默認的就是@syntheszie var = _var;
- @synthesize 的語義是如果你沒有手動實現(xiàn) setter 方法和 getter 方法,那么編譯器會自動為你加上這兩個方法。

- @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶自己實現(xiàn),不自動生成。(當(dāng)然對于 readonly 的屬性只需提供 getter 即可)。假如一個屬性被聲明為 @dynamic var,然后你沒有提供 @setter方法和 @getter 方法,編譯的時候沒問題,但是當(dāng)程序運行到 instance.var = someVar,由于缺 setter 方法會導(dǎo)致程序崩潰;或者當(dāng)運行到 someVar = var 時,由于缺 getter 方法同樣會導(dǎo)致崩潰。編譯時沒問題,運行時才執(zhí)行相應(yīng)的方法,這就是所謂的動態(tài)綁定。


17. static有什么作用?

static關(guān)鍵字可以修飾函數(shù)和變量,作用如下:

**隱藏**

通過static修飾的函數(shù)或者變量,在該文件中,所有位于這條語句之后的函數(shù)都可以訪問,而其他文件中的方法和函數(shù)則不行

**靜態(tài)變量**

類方法不可以訪問實例變量(函數(shù)),通過static修飾的實例變量(函數(shù)),可以被類   方法訪問;

**持久**

static修飾的變量,能且只能被初始化一次;

**默認初始化**

static修飾的變量,默認初始化為0;

18. objc在向一個對象發(fā)送消息時,發(fā)生了什么?

- objc_msgSend(recicver, selecter..)

19. runloop是來做什么的?runloop和線程有什么關(guān)系?主線程默認開啟了runloop么?子線程呢?

1\. runloop與線程是一一對應(yīng)的,一個runloop對應(yīng)一個核心的線程,為什么說是核心的,是因為runloop是可以嵌套的,但是核心的只能有一個,他們的關(guān)系保存在一個全局的字典里。
2\. runloop是來管理線程的,當(dāng)線程的runloop被開啟后,線程會在執(zhí)行完任務(wù)后進入休眠狀態(tài),有了任務(wù)就會被喚醒去執(zhí)行任務(wù)。runloop在第一次獲取時被創(chuàng)建,在線程結(jié)束時被銷毀。
3\. 對于主線程來說,runloop在程序一啟動就默認創(chuàng)建好了。
4\. 對于子線程來說, runloop是懶加載的,只有當(dāng)我們使用的時候才會創(chuàng)建,所以在子線程用定時器要注意:確保子線程的runloop被開啟,不然定時器不會回調(diào)。  

20. 如何手動觸發(fā)一個value的KVO

鍵值觀察通知依賴于 NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一個被觀察屬性發(fā)生改變之前, willChangeValueForKey: 一定會被調(diào)用,這就 會記錄舊的值。而當(dāng)改變發(fā)生后, didChangeValueForKey: 會被調(diào)用,繼而 observeValueForKey:ofObject:change:context: 也會被調(diào)用。如果可以手動實現(xiàn)這些調(diào)用,就可以實現(xiàn)“手動觸發(fā)”了。

引申 0 如何給系統(tǒng)KVO設(shè)置篩選條件?

  • 舉例:取消Person類age屬性的默認KVO,設(shè)置age大于18時,手動觸發(fā)KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setAge:(NSInteger)age {
    if (age >= 18) {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }else {
        _age = age;
    }
}

引申 1.通過KVC修改屬性會觸發(fā)KVO么?直接修改成員變量呢 ?

  • 會觸發(fā)KVO。即使沒有聲明屬性,只有成員變量,只要accessInstanceVariablesDirectly返回的是YES,允許訪問其成員變量,那么不管有沒有調(diào)用setter方法,通過KVC修改成員變量的值,都能觸發(fā)KVO。這也說明通過KVC內(nèi)部實現(xiàn)了willChangeValueForKey:方法和didChangeValueForKey:方法
  • 直接修改成員變量不會觸發(fā)KVO。直接修改成員變量內(nèi)部并沒有做處理只是單純的賦值,所以不會觸發(fā)。

引申 kvc的底層實現(xiàn)?

  • 賦值方法setValue:forKey:的原理

(1)首先會按照順序依次查找setKey:方法和_setKey:方法,只要找到這兩個方法當(dāng)中的任何一個就直接傳遞參數(shù),調(diào)用方法;

(2)如果沒有找到setKey:和_setKey:方法,那么這個時候會查看accessInstanceVariablesDirectly方法的返回值,如果返回的是NO(也就是不允許直接訪問成員變量),那么會調(diào)用setValue:forUndefineKey:方法,并拋出異?!癗SUnknownKeyException”;

(3)如果accessInstanceVariablesDirectly方法返回的是YES,也就是說可以訪問其成員變量,那么就會按照順序依次查找 _key、_isKey、key、isKey這四個成員變量,如果查找到了,就直接賦值;如果依然沒有查到,那么會調(diào)用setValue:forUndefineKey:方法,并拋出異?!癗SUnknownKeyException”。

  • 取值方法valueForKey:的原理

(1)首先會按照順序依次查找getKey:、key、isKey、_key:這四個方法,只要找到這四個方法當(dāng)中的任何一個就直接調(diào)用該方法;

(2)如果沒有找到,那么這個時候會查看accessInstanceVariablesDirectly方法的返回值,如果返回的是NO(也就是不允許直接訪問成員變量),那么會調(diào)用valueforUndefineKey:方法,并拋出異?!癗SUnknownKeyException”;

(3)如果accessInstanceVariablesDirectly方法返回的是YES,也就是說可以訪問其成員變量,那么就會按照順序依次查找 _key、_isKey、key、isKey這四個成員變量,如果找到了,就直接取值;如果依然沒有找到成員變量,那么會調(diào)用valueforUndefineKey方法,并拋出異?!癗SUnknownKeyException”。

21. ViewController生命周期

按照執(zhí)行順序排列:
1. initWithCoder:通過nib文件初始化時觸發(fā)。
2. awakeFromNib:nib文件被加載的時候,會發(fā)生一個awakeFromNib的消息到nib文件中的每個對象。      
3. loadView:開始加載視圖控制器自帶的view。
4. viewDidLoad:視圖控制器的view被加載完成。  
5. viewWillAppear:視圖控制器的view將要顯示在window上。
6. updateViewConstraints:視圖控制器的view開始更新AutoLayout約束。
7. viewWillLayoutSubviews:視圖控制器的view將要更新內(nèi)容視圖的位置。
8. viewDidLayoutSubviews:視圖控制器的view已經(jīng)更新視圖的位置。
9. viewDidAppear:視圖控制器的view已經(jīng)展示到window上。 
10. viewWillDisappear:視圖控制器的view將要從window上消失。
11. viewDidDisappear:視圖控制器的view已經(jīng)從window上消失。

22.網(wǎng)絡(luò)協(xié)議

  • TCP三次握手和四次揮手?

三次握手

1.客戶端向服務(wù)端發(fā)起請求鏈接,首先發(fā)送SYN報文,SYN=1,seq=x,并且客戶端進入SYN_SENT狀態(tài) 2.服務(wù)端收到請求鏈接,服務(wù)端向客戶端進行回復(fù),并發(fā)送響應(yīng)報文,SYN=1,seq=y,ACK=1,ack=x+1,并且服務(wù)端進入到SYN_RCVD狀態(tài) 3.客戶端收到確認報文后,向服務(wù)端發(fā)送確認報文,ACK=1,ack=y+1,此時客戶端進入到ESTABLISHED,服務(wù)端收到用戶端發(fā)送過來的確認報文后,也進入到ESTABLISHED狀態(tài),此時鏈接創(chuàng)建成功

- 哎!
- 嗯
- 給你 

為什么需要三次握手: 為了防止已失效的連接請求報文段突然又傳送到了服務(wù)端,因而產(chǎn)生錯誤。假設(shè)這是一個早已失效的報文段,但server收到此失效的連接請求報文段后,就誤認為是client再次發(fā)出的一個新的連接請求。于是就向client發(fā)出確認報文段,同意建立連接。假設(shè)不采用“三次握手”,那么只要server發(fā)出確認,新的連接就建立了。由于現(xiàn)在client并沒有發(fā)出建立連接的請求,因此不會理睬server的確認,也不會向server發(fā)送數(shù)據(jù)。但server卻以為新的運輸連接已經(jīng)建立,并一直等待client發(fā)來數(shù)據(jù)。這樣,server的很多資源就白白浪費掉了。

四次揮手

1.客戶端向服務(wù)端發(fā)起關(guān)閉鏈接,并停止發(fā)送數(shù)據(jù) 2.服務(wù)端收到關(guān)閉鏈接的請求時,向客戶端發(fā)送回應(yīng),我知道了,然后停止接收數(shù)據(jù) 3.當(dāng)服務(wù)端發(fā)送數(shù)據(jù)結(jié)束之后,向客戶端發(fā)起關(guān)閉鏈接,并停止發(fā)送數(shù)據(jù) 4.客戶端收到關(guān)閉鏈接的請求時,向服務(wù)端發(fā)送回應(yīng),我知道了,然后停止接收數(shù)據(jù)

- 哎!
- 嗯
- 關(guān)了
- 好的

為什么需要四次揮手: 因為TCP是全雙工通信的,在接收到客戶端的關(guān)閉請求時,還可能在向客戶端發(fā)送著數(shù)據(jù),因此不能再回應(yīng)關(guān)閉鏈接的請求時,同時發(fā)送關(guān)閉鏈接的請求

引申

  1. HTTP和HTTPS有什么區(qū)別?

    • HTTP協(xié)議是一種使用明文數(shù)據(jù)傳輸?shù)木W(wǎng)絡(luò)協(xié)議。
    • HTTPS協(xié)議可以理解為HTTP協(xié)議的升級,就是在HTTP的基礎(chǔ)上增加了數(shù)據(jù)加密。在數(shù)據(jù)進行傳輸之前,對數(shù)據(jù)進行加密,然后再發(fā)送到服務(wù)器。這樣,就算數(shù)據(jù)被第三者所截獲,但是由于數(shù)據(jù)是加密的,所以你的個人信息讓然是安全的。這就是HTTP和HTTPS的最大區(qū)別。
  2. HTTPS的加密方式?

    • Https采用對稱加密和非對稱加密結(jié)合的方式來進行通信。

    • Https不是應(yīng)用層的新協(xié)議,而是Http通信接口用SSL和TLS來加強加密和認證機制。

      • 對稱加密: 加密和解密都是同一個鑰匙
      • 非對稱加密:密鑰承兌出現(xiàn),分為公鑰和私鑰,公鑰加密需要私鑰解密,私鑰加密需要公鑰解密

HTTP和HTTPS的建立連接的過程?

HTTP

  • 建立鏈接完畢以后客戶端會發(fā)送響應(yīng)給服務(wù)器
  • 服務(wù)端接受請求并且做出響應(yīng)發(fā)送給客戶端
  • 客戶端收到響應(yīng)并且解析響應(yīng)給客戶

HTTPS

  • 在使用HTTPS是需要保證服務(wù)端配置了正確的對應(yīng)的安全證書
  • 客戶端發(fā)送請求到服務(wù)器
  • 服務(wù)端返回公鑰和證書到客戶端
  • 客戶端接受后,會驗證證書的安全性,如果通過則會隨機生成一個隨機數(shù),用公鑰對其解密, 發(fā)送到服務(wù)端
  • 服務(wù)端接受到這個加密后的隨機數(shù)后,會用私鑰對其進行揭秘,得到真正的隨機數(shù),然后調(diào)用這個隨機數(shù)當(dāng)作私鑰對需要發(fā)送的數(shù)據(jù)進行對稱加密。
  • 客戶端接收到加密后的數(shù)據(jù)使用私鑰(之前生成的隨機值)對數(shù)據(jù)進行解密,并且解析數(shù)據(jù)呈現(xiàn)給客戶

HTTP協(xié)議中GET和POST的區(qū)別

  • GET在特定的瀏覽器和服務(wù)器對URL的長度是有限制的。 但是理論上是沒有限制的

  • POST不是通過URL進行傳值,理論上不受限制。

  • GET會把請求參數(shù)拼接到URL后面, 不安全,

  • POST把參數(shù)放到請求體里面, 會比GET相對安全一點, 但是由于可以窺探數(shù)據(jù), 所以也不安全, 想更安全用加密。

  • GET比POST的請求速度快。原因:Post請求的過程, 會現(xiàn)將請求頭發(fā)送給服務(wù)器確認,然后才真正的發(fā)送數(shù)據(jù), 而Get請求 過程會在鏈接建立后會將請求頭和數(shù)據(jù)一起發(fā)送給服務(wù)器。 中間少了一步。 所以get比post 快

  • post的請求過程

  • 三次握手之后 第三次會把post請求頭發(fā)送

  • 服務(wù)器返回100 continue響應(yīng)

  • 瀏覽器開始發(fā)送數(shù)據(jù)

  • 服務(wù)器返回200 ok響應(yīng)


  • get請求過程
  • 三次握手之后 第三次會發(fā)送get請求頭和數(shù)據(jù)
  • 服務(wù)器返回200 ok響應(yīng)

23. 有沒有使用過performSelector?

  • 這題主要是想問的是有沒有動態(tài)添加過方法
  • 話不多說上代碼
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];

    // 默認person,沒有實現(xiàn)eat方法,可以通過performSelector調(diào)用,但是會報錯。
    // 動態(tài)添加方法就不會報錯
    [p performSelector:@selector(eat)];
}

@end

@implementation Person

// **這里真是奇葩, 實在想不到什么時候才有這種使用場景, 我再外面找不到方法, 我再當(dāng)前類里面直接在寫一個方法就好咯,干嘛要在這里寫這個玩意, 還要寫一個C語言的東西, 既然面試想問, 那咱就要會!**

// void(*)()
// 默認方法都有兩個隱式參數(shù),
void eat(id self,SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 當(dāng)一個對象調(diào)用未實現(xiàn)的方法,會調(diào)用這個方法處理,并且會把對應(yīng)的方法列表傳過來.
// 剛好可以用來判斷,未實現(xiàn)的方法是不是我們想要動態(tài)添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        // 動態(tài)添加eat方法

        // 第一個參數(shù):給哪個類添加方法
        // 第二個參數(shù):添加方法的方法編號
        // 第三個參數(shù):添加方法的函數(shù)實現(xiàn)(函數(shù)地址)
        // 第四個參數(shù):函數(shù)的類型,(返回值+參數(shù)類型) v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}
@end

  • 當(dāng)然面試的時候也可能問你這個
// 延時操作 和GCD的after 一個效果
[p performSelector:@selector(eat) withObject:nil afterDelay:4];

  • 你以為完了? 錯了,大概率面試官會問你,*** 上面這段代碼放在子線程中 是什么樣子的?為什么?**

    —首先 上面這個方法其實就是內(nèi)部創(chuàng)建了一個NSTimer定時器,然后這個定時器會添加在當(dāng)前的RunLoop中所以上面代碼放到子線程中不會有任何定時器相關(guān)方法被執(zhí)行,如果想要執(zhí)行,開啟當(dāng)前線程即可 即

[[NSRunLoop currentRunLoop] run];

// 完整調(diào)用
 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        //  [[NSRunLoop currentRunLoop] run]; 放在上面執(zhí)行時不可以的,因為當(dāng)前只是開啟了runloop 里面沒有任何事件(source,timer,observer)也是開啟失敗的
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
         [[NSRunLoop currentRunLoop] run];
});

// 由此我自行又做了一個測試, 把        
[self performSelector:@selector(test)];
在子線程調(diào)用,是沒有任何問題的。

// 我又測試了一下,
 [self performSelector:@selector(test) withObject:nil afterDelay:2];
 這個方法在主線程執(zhí)行  打印線程是1

在子線程中調(diào)用打印線程 非1

  • 然后面試官開始飄了, 開始問你關(guān)于NSTimer相關(guān)問題?怎么辦? 答: 搞他!

引申 NSTimer在子線程執(zhí)行?

  • NSTimer直接在在子線程是不會被調(diào)用的, 想要執(zhí)行請開啟當(dāng)前的Runloop 。具體開啟方案上面題有說,不贅述。

引申 為什么說NSTimer不準(zhǔn)確?

  • NSTimer的觸發(fā)時間到的時候,runloop如果在阻塞狀態(tài),觸發(fā)時間就會推遲到下一個runloop周期 減少誤差的方法 代碼如下
// 在子線程中開啟NStimer,或者更改當(dāng)前Runloop的Mode 為NSRunLoopCommonModes
[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

// 利用CADisplayLink (iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結(jié)束都被調(diào)用,精確度相當(dāng)高)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

// 利用GCD
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"GCD timer test");
});
dispatch_resume(_timer);

24. 為什么AFN3.0中需要設(shè)置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0卻不需要?

  • 功能不一樣, 2.x是基于NSURLConnection的,其內(nèi)部實現(xiàn)要在異步并發(fā),所以不能設(shè)置1。 3.0 是基于NSURLSession其內(nèi)部是需要串行的鑒于一些多線程數(shù)據(jù)訪問的安全性考慮, 設(shè)置這個達到串行回調(diào)的效果。

AFNetworking 2.0 和3.0 的區(qū)別?

  • AFN3.0剔除了所有的NSURLConnection請求的API
  • AFN3.0使用NSOperationQueue代替AFN2.0的常駐線程

2.x版本常駐線程的分析

  • 在請求完成后我們需要對數(shù)據(jù)進行一些序列化處理,或者錯誤處理。如果我們在主線中處理這些事情很明顯是不合理的。不僅會導(dǎo)致UI的卡頓,甚至受到默認的RunLoopModel的影響,我們在滑動tableview的時候,會導(dǎo)致時間的處理停止。

  • 這里時候我們就需要一個子線程來處理事件和網(wǎng)絡(luò)請求的回調(diào)了。但是,子線程在處理完事件后就會自動結(jié)束生命周期,這個時候后面的一些網(wǎng)絡(luò)請求得回調(diào)我們就無法接收了。所以我們就需要開啟子線程的RunLoop來保存線程的常駐。

  • 當(dāng)然我們可以每次發(fā)起一個請求就開啟一條子線程,但是這個想一下就知道開銷有多大了。所以這個時候?;钜粭l線程來對請求得回調(diào)處理是比較好的一個方案。

3.x版本不在常駐線程的分析?

  • 在3.x的AFN版本中使用的是NSURLSession進行封裝。對比于NSURLConnection,NSURLSession不需要在當(dāng)前的線程等待網(wǎng)絡(luò)回調(diào),而是可以讓開發(fā)者自己設(shè)定需要回調(diào)的隊列。

  • 所以在3.x版本中AFN使用了NSOperationQueue對網(wǎng)絡(luò)回調(diào)的管理,并且設(shè)置maxConcurrentOperationCount為1,保證了最大的并發(fā)數(shù)為1,也就是說讓網(wǎng)絡(luò)請求串行執(zhí)行。避免了多線程環(huán)境下的資源搶奪問題。

25. autoreleasePool 在何時被釋放?

  • ARC中所有的新生對象都是 自動加autorelese的, @atuorelesepool 大部分時候解決了瞬時內(nèi)存暴增的問題 。
  • MRC中的情況 關(guān)鍵詞變了NSAutoreleasePool。
//來自Apple文檔,見參考
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) { 
  @autoreleasepool { 
        NSError *error;
        NSString *fileContents = [NSString stringWithContentsOfURL:urlencoding:NSUTF8StringEncoding error:&error]; 
}

// 如果循環(huán)次數(shù)非常多,而且循環(huán)體里面的對象都是臨時創(chuàng)建使用的,就可以用@autoreleasepool 包起來,讓每次循環(huán)結(jié)束時,可以及時釋放臨時對象的內(nèi)存

// for 和 for in 里面是沒有自動包裝@autoreleasepool著的,而下面的方法是由@autoreleasepool自動包圍的
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 這里被一個局部@autoreleasepool包圍著
}];

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"666"] autorelease];
[pool drain];

// 其作用于為drain 和 init 之間

  • 回歸正題@autoReleasePool什么時間釋放?
    • 一個被autoreleasepool包裹生成得對象,都會在其創(chuàng)建生成之后自動添加autorelease, 然后被autorelease對象得釋放時機 就是在當(dāng)前runloop循環(huán)結(jié)束的時候自動釋放的
    • 參考鏈接:blog.sunnyxx.com/2014/10/15/…

子線程中的autorelease變量什么時候釋放?

  • 子線程中會默認包裹一個autoreleasepool的, 釋放時機是當(dāng)前線程退出的時候。

autoreleasepool是如何實現(xiàn)的?

  • @autoreleasepool{} 本質(zhì)上是一個結(jié)構(gòu)體:
  • autoreleasepool會被轉(zhuǎn)換成__AtAutoreleasePool
  • __AtAutoreleasePool 里面有兩個函數(shù)objc_autoreleasePoolPush(),objc_autoreleasePoolPop().,其實一些列下來之后實際上調(diào)用得是AutoreleasePoolPage類中得push 和 pop兩個類方法
  • push就是壓棧操作,
  • pop就是出棧操作于此同時對其對象發(fā)送release消息進行釋放

26. iOS界面渲染機制? [這是很大的一個模塊,里面牽扯很多東西, 耐心看下去]

  • 先簡單解釋一下渲染機制

首先iOS渲染視圖的核心是Core Animation,其渲染層次依次為:圖層樹->呈現(xiàn)樹->渲染樹

  • 一共三個階段

  • CPU階段(進行Frame布局,準(zhǔn)備視圖和圖層之間的層級關(guān)系)

  • OpenGL ES階段(iOS8以后改成Metal), (渲染服務(wù)把上面提供的圖層上色,生成各種幀)

  • GPU階段 (把上面操作的東西進行一些列的操作,最終展示到屏幕上面)

  • 稍微詳細說明

  • 首先一個視圖由CPU進行Frame布局,準(zhǔn)備視圖和圖層的層及關(guān)系。

  • CUP會將處理視圖和圖層的層級關(guān)系打包,通過IPC(進程間的通信)通道提交給渲染服務(wù)(OpenGL和GPU)

  • 渲染服務(wù)首先將圖層交給OpenGL進行紋理生成和著色,生成前后幀緩存,再根據(jù)硬件的刷新幀率,一般以設(shè)備的VSync信號和CADisplayLink(類似一個刷新UI專用的定時器)為標(biāo)準(zhǔn),進行前后幀緩存的切換

  • 最后,將最終 要顯示在畫面上的后幀緩存交給GPU,進行采集圖片和形狀,運行變換, 應(yīng)用紋理混合,最終顯示在屏幕上。

程序卡頓的原因?

  • 正常渲染流程
  • CPU計算完成之后交給GPU,來個同步信號Vsync 將內(nèi)容渲染到屏幕上
  • 非正常(卡頓/掉幀)的流程
  • CPU計算時間正?;蛘呗?,GPU渲染時間長了, 這時候Vsync信號, 由于沒有繪制完全,CUP開始計算下一幀,當(dāng)下一幀正常繪制成功之后,把當(dāng)前沒有繪制完成的幀丟棄, 顯示了下一幀,于是這樣就造成了卡頓。 需要注意的是:Vsync時間間隔是固定的, 比如60幀率大的Vsync 是每16ms就執(zhí)行一個一次,類似定時器一樣

這里會出現(xiàn)一個面試題!?。?/strong> 題目如下:

  • 從第一次打開App到完全開始展現(xiàn)出UI,中間發(fā)生了什么? 或者App是怎么渲染某一個View的?
  • 回答就是上面的稍微詳細說明,如果要求更詳細, 可以繼續(xù)深究一下。

在科普一下 1.Core Animation Core Animation 在 RunLoop 中注冊了一個 Observer,監(jiān)聽了 BeforeWaiting 和 Exit 事件。這個 Observer 的優(yōu)先級是 2000000,低于常見的其他 Observer。當(dāng)一個觸摸事件到來時,RunLoop 被喚醒,App 中的代碼會執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級、設(shè)置 UIView 的 frame、修改 CALayer 的透明度、為視圖添加一個動畫;這些操作最終都會被 CALayer 捕獲,并通過 CATransaction 提交到一個中間狀態(tài)去(CATransaction 的文檔略有提到這些內(nèi)容,但并不完整)。當(dāng)上面所有操作結(jié)束后,RunLoop 即將進入休眠(或者退出)時,關(guān)注該事件的 Observer 都會得到通知。這時 CA 注冊的那個 Observer 就會在回調(diào)中,把所有的中間狀態(tài)合并提交到 GPU 去顯示;如果此處有動畫,CA 會通過 DisplayLink 等機制多次觸發(fā)相關(guān)流程。

2.CPU渲染職能

  • 布局計算:如果視圖層級過于復(fù)雜,當(dāng)試圖呈現(xiàn)或者修改的時候,計算圖層幀率就會消耗一部分時間,
  • 視圖懶加載: iOS只會當(dāng)視圖控制器的視圖顯示到屏幕上才會加載它,這對內(nèi)存使用和程序啟動時間很有好處,但是當(dāng)呈現(xiàn)到屏幕之前,按下按鈕導(dǎo)致的許多工作都不會被及時響應(yīng)。比如,控制器從數(shù)據(jù)局中獲取數(shù)據(jù), 或者視圖從一個xib加載,或者涉及iO圖片顯示都會比CPU正常操作慢得多。
  • 解壓圖片:PNG或者JPEG壓縮之后的圖片文件會比同質(zhì)量的位圖小得多。但是在圖片繪制到屏幕上之前,必須把它擴展成完整的未解壓的尺寸(通常等同于圖片寬 x 長 x 4個字節(jié))。為了節(jié)省內(nèi)存,iOS通常直到真正繪制的時候才去解碼圖片。根據(jù)你加載圖片的方式,第一次對 圖層內(nèi)容賦值的時候(直接或者間接使用 UIImageView )或者把它繪制到 Core Graphics中,都需要對它解壓,這樣的話,對于一個較大的圖片,都會占用一定的時間。
  • Core Graphics繪制:如果對視圖實現(xiàn)了drawRect:或drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在繪制任何東 西之前都會產(chǎn)生一個巨大的性能開銷。為了支持對圖層內(nèi)容的任意繪制,Core Animation必須創(chuàng)建一個內(nèi)存中等大小的寄宿圖片。然后一旦繪制結(jié)束之后, 必須把圖片數(shù)據(jù)通過IPC傳到渲染服務(wù)器。在此基礎(chǔ)上,Core Graphics繪制就會變得十分緩慢,所以在一個對性能十分挑剔的場景下這樣做十分不好。
  • 圖層打包:當(dāng)圖層被成功打包,發(fā)送到渲染服務(wù)器之后,CPU仍然要做如下工作:為了顯示 屏幕上的圖層,Core Animation必須對渲染樹種的每個可見圖層通過OpenGL循環(huán) 轉(zhuǎn)換成紋理三角板。由于GPU并不知曉Core Animation圖層的任何結(jié)構(gòu),所以必須 要由CPU做這些事情。這里CPU涉及的工作和圖層個數(shù)成正比,所以如果在你的層 級關(guān)系中有太多的圖層,就會導(dǎo)致CPU沒一幀的渲染,即使這些事情不是你的應(yīng)用 程序可控的。

3.GPU渲染職能 GPU會根據(jù)生成的前后幀緩存數(shù)據(jù),根據(jù)實際情況進行合成,其中造成GPU渲染負擔(dān)的一般是:離屏渲染,圖層混合,延遲加載。

這里又會出現(xiàn)一個面試題?。。?/strong> 一個UIImageView添加到視圖上以后,內(nèi)部如何渲染到手機上的?

圖片顯示分為三個步驟: 加載、解碼、渲染、 通常,我們程序員的操作只是加載,至于解碼和渲染是由UIKit內(nèi)部進行的。 例如:UIImageView顯示在屏幕上的時候需要UIImage對象進行數(shù)據(jù)源的賦值。而UIImage持有的數(shù)據(jù)是未解碼的壓縮數(shù)據(jù),當(dāng)賦值的時候,圖像數(shù)據(jù)會被解碼變成RGB顏色數(shù)據(jù),最終渲染到屏幕上。


看完上面的又來問題了! 關(guān)于UITableView優(yōu)化的問題?(真他媽子子孫孫無窮盡也~) 先說造成UITableView滾動時候卡頓的的原因有哪些?

  • 隱式繪制 CGContext
  • 文本CATextLayer 和 UILabel
  • 光柵化 shouldRasterize
  • 離屏渲染
  • 可伸縮圖片
  • shadowPath
  • 混合和過度繪制
  • 減少圖層數(shù)量
  • 裁切
  • 對象回收
  • Core Graphics繪制
  • -renderInContext: 方法

在說關(guān)于UITableView的優(yōu)化問題!

基礎(chǔ)的

  • 重用機制(緩存池)
  • 少用有透明度的View
  • 盡量避免使用xib
  • 盡量避免過多的層級結(jié)構(gòu)
  • iOS8以后出的預(yù)估高度
  • 減少離屏渲染操作(圓角、陰影啥的)

  • **** 解釋一下為什么減少離屏渲染操作?****

  • 需要創(chuàng)建新的緩沖區(qū)

  • 整個過程需要多次切換上下文環(huán)境, 顯示從當(dāng)前的屏幕切換到離屏,等待離屏渲染結(jié)束后,將離屏緩沖區(qū)的渲染結(jié)果 顯示到屏幕有上, 又要將上下文環(huán)境從離屏切換到當(dāng)前屏幕,

  • ****那些操作會觸發(fā)離屏渲染?****

  • 光柵化 layer.shouldRasterize = YES

  • 遮罩layer.mask

  • 圓角layer.maskToBounds = Yes,Layer.cornerRadis 大于0

  • 陰影l(fā)ayer.shadowXXX

進階的

  • 緩存cell的高度(提前計算好cell的高度,緩存進當(dāng)前的模型里面)
  • 異步繪制
  • 滑動的時候,按需加載

高階的

  • 你想不到 竟然不推薦用UILabel。哈哈哈~ 至于為什么 看下面的鏈接吧

至于上面的那些基礎(chǔ)的,涉及到渲染級別的自己說的時候悠著點,面試官如果想搞你的話,考一考你最上面的那些,CUP和GUP,以及openGL相關(guān), 在考一下你進程通信IPC,以及VSync信號啥的, 這些東西太雞兒高深了,沒點匠心 這東西還真搞不了,要想研究可以看看YYKit的作者寫的一篇關(guān)于頁面流暢的文章:blog.ibireme.com/2015/11/12/…

卡頓檢測的方法

  • 卡頓就是主線程阻塞的時間問題,可以添加Observer到主線程Runloop中,通過監(jiān)聽Runloop狀態(tài)切換的耗時,以達到監(jiān)聽卡頓的目的

繼續(xù)

既然都是圖形繪制了,那就再研究一下事件響應(yīng)鏈&原理

傳統(tǒng)的問法來了:UIView和CALayer的區(qū)別? 通常我們這樣回答:UIView可以響應(yīng)用戶事件,而CALayer不能處理事件


回答這個之前, 先回顧一下另外一個經(jīng)典面試題:事件響應(yīng)鏈和事件傳遞?

基本概念:

  • 響應(yīng)鏈: 是由鏈接在一起的響應(yīng)者(UIResponse子類)組成的,一般為第一響應(yīng)著到application對象以及中間所有響應(yīng)者一起組成的。

  • 事件傳遞: 獲取響應(yīng)鏈之后, 將事件由第一響應(yīng)者網(wǎng)application的傳遞過程

  • image
  • image
  • 事件的分發(fā)和傳遞

  • 當(dāng)程序中發(fā)生觸摸事件之后,系統(tǒng)會將事件添加到UIApplication管理的一個隊列當(dāng)中

  • UIApplication將處于任務(wù)隊列最前端的事件向下分發(fā) 即UIWindow

  • UIWindow將事件向下分發(fā),即UIView或者UIViewController

  • UIView首先看自己能否處理這個事件,觸摸點是否在自己身上,自己的透明度是否大于0,01,userInteractionEnabled 是否是YES, Hidden實際是NO,如果這些都滿足,那么繼續(xù)尋找其子視圖

  • 遍歷子控件,重復(fù)上面步驟

  • 如果沒有找到,那么自己就是改事件的處理者

  • 如果自己不能處理,那么就不做任何處理 即視為沒有合適的View能接收處理當(dāng)前事件,則改事件會被廢棄。

  • *** 怎么尋找當(dāng)前觸摸的是哪一個View?*** 下面中兩個方法

// 此方法返回的View是本次點擊事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判斷一個點是否落在范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

事件傳遞給控件之后, 就會調(diào)用hitTest:withEvent方法去尋找更合適的View,如果當(dāng)前View存在子控件,則在子控件繼續(xù)調(diào)用hitTest:withEvent方法判斷是否是合適的View, 如果還不是就一直遍歷尋找, 找不到的話直接廢棄掉。

// 因為所有的視圖類都是繼承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   // 1.判斷當(dāng)前控件能否接收事件
   if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
   // 2\. 判斷點在不在當(dāng)前控件
   if ([self pointInside:point withEvent:event] == NO) return nil;
   // 3.從后往前遍歷自己的子控件
   NSInteger count = self.subviews.count;
   for (NSInteger i = count - 1; i >= 0; i--) {
      UIView *childView = self.subviews[I];
       // 把當(dāng)前控件上的坐標(biāo)系轉(zhuǎn)換成子控件上的坐標(biāo)系
      CGPoint childP = [self convertPoint:point toView:childView];
      UIView *fitView = [childView hitTest:childP withEvent:event];
       if (fitView) { // 尋找到最合適的view
           return fitView;
       }
   }
   // 循環(huán)結(jié)束,表示沒有比自己更合適的view
   return self;

}

  • 判斷觸摸點是否在視圖內(nèi)?
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

  • tableView 加一個tap的手勢, 點擊當(dāng)前cell的位置 哪個事件被響應(yīng) 為什么?
  • tap事件被響應(yīng), 因為tap事件添加之后,默認是取消當(dāng)前tap以外的所有事件的, 也就是說, tap事件處于當(dāng)前響應(yīng)者鏈的最頂端, 解決的辦法執(zhí)行tap的delagete, 實現(xiàn)
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
{  
    if([touch.view isKindOfClass:[XXXXcell class]])  
    {  
        return NO;  
    }  
    return YES;  
}

結(jié)交人脈

最后推薦個我的iOS交流群:834688868,不管你是大牛還是小白都歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗,討論技術(shù), 大家一起交流學(xué)習(xí)成長!

'有一個共同的圈子很重要,結(jié)識人脈!里面都是iOS開發(fā),全棧發(fā)展,歡迎入駐,共同進步?。ㄈ簝?nèi)會免費提供一些群主收藏的免費學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔?。?/p>

以下資料在群文件可自行下載

作為一個開發(fā)者,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:834688868,不管你是大牛還是小白都歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗,討論技術(shù), 大家一起交流學(xué)習(xí)成長!

作者:iOS開發(fā)面試總結(jié)
鏈接:http://m.itdecent.cn/p/65dd685e3b05
來源:簡書

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容