iOS面試了20幾家總結(jié)出來的面試題(上)

iOS面試題

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

更新日志

  • 2020年08月17日 更新了第23條的新的引申,關(guān)于NSTimer循環(huán)引用的根本原因, 以及優(yōu)化方案

一、硬技術(shù)篇

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

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

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

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

如果你正在面試,或者正準(zhǔn)備跳槽,不妨看看我精心總結(jié)的iOS大廠面試資料https://gitee.com/Mcci7/i-oser 來獲取一份詳細(xì)的大廠面試資料 為你的跳槽加薪多一份保障

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

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

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

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

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

  • 所有的類自身也是一個(gè)對象,我們可以向這個(gè)對象發(fā)送消息(即調(diào)用類方法)。

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

引申5. 什么是野指針?

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

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

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

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

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

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

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

  • 使用NSSetUncaughtExceptionHandler可以統(tǒng)計(jì)閃退的信息。
  • 將統(tǒng)計(jì)到的信息以data的形式 利用網(wǎng)絡(luò)請求發(fā)給后臺(tái)
  • 在后臺(tái)收集信息,進(jìn)行排查
- (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(@"***********************************************");
    }
復(fù)制代碼

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

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

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

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

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

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

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

3.深拷貝和淺拷貝

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

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

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

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

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

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

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

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


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

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

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

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

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

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

引伸:關(guān)聯(lián)對象的原理?
  • 關(guān)聯(lián)對象并不是存儲(chǔ)在關(guān)聯(lián)對象本身內(nèi)存中,而是存儲(chǔ)在全局統(tǒng)一的一個(gè)容器中;
  • 由 AssociationsManager 管理并在它維護(hù)的一個(gè)單例 Hash 表 AssociationsHashMap 中存儲(chǔ);
  • 使用 AssociationsManagerLock 自旋鎖保證了線程安全
引伸:分類可以添加那些內(nèi)容?
  • 實(shí)例方法,類方法,協(xié)議,屬性
引伸:Category 的實(shí)現(xiàn)原理?
  • Category 在剛剛編譯完成的時(shí)候, 和原來的類是分開的,只有在程序運(yùn)行起來的時(shí)候, 通過runtime合并在一起。
引申 使用runtime Associate方法關(guān)聯(lián)的對象,需要在主對象dealloc的時(shí)候釋放么?
  • 不需要,被關(guān)聯(lián)的對象的生命周期內(nèi)要比對象本身釋放晚很多, 它們會(huì)在被 NSObject -dealloc 調(diào)用的 object_dispose() 方法中釋放。
引申 能否向編譯后得到的類中增加實(shí)例變量, 能否向運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)力變量?
  • 不能再編譯后得到的類中增加實(shí)例變量。因?yàn)榫幾g后的類已經(jīng)注冊在runtime中, 類結(jié)構(gòu)體中objc_ivar_list 實(shí)例變量的鏈表和objc_ivar_list 實(shí)例變量的內(nèi)存大小已經(jīng)確定,所以不能向存在的類中添加實(shí)例變量
  • 能在運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)力變量。調(diào)用class_addIvar 函數(shù)
引申 主類執(zhí)行了foo方法,分類也執(zhí)行了foo方法,在執(zhí)行的地方執(zhí)行了foo方法,主類的foo會(huì)被覆蓋么? 如果想只想執(zhí)行主類的foo方法,如何去做?
  • 主類的方法被分類的foo覆蓋了,其實(shí)分類并沒有覆蓋主類的foo方法,只是分類的方法排在方法列表前面,主類的方法列表被擠到了后面, 調(diào)用的時(shí)候會(huì)首先找到第一次出現(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);
}
復(fù)制代碼

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

① 調(diào)用時(shí)刻:+load方法會(huì)在Runtime加載類、分類時(shí)調(diào)用(不管有沒有用到這些類,在程序運(yùn)行起來的時(shí)候都會(huì)加載進(jìn)內(nèi)存,并調(diào)用+load方法); 每個(gè)類、分類的+load,在程序運(yùn)行過程中只調(diào)用一次(除非開發(fā)者手動(dòng)調(diào)用)。

② 調(diào)用方式: 系統(tǒng)自動(dòng)調(diào)用+load方式為直接通過函數(shù)地址調(diào)用,開發(fā)者手動(dòng)調(diào)用+load方式為消息機(jī)制objc_msgSend函數(shù)調(diào)用。

③ 調(diào)用順序: 先調(diào)用類的+load,按照編譯先后順序調(diào)用(先編譯,先調(diào)用),調(diào)用子類的+load之前會(huì)先調(diào)用父類的+load; 再調(diào)用分類的+load,按照編譯先后順序調(diào)用(先編譯,先調(diào)用)(注意:分類的其它方法是:后編譯,優(yōu)先調(diào)用)。


① 調(diào)用時(shí)刻:+initialize方法會(huì)在類第一次接收到消息時(shí)調(diào)用。 如果子類沒有實(shí)現(xiàn)+initialize方法,會(huì)調(diào)用父類的+initialize,所以父類的+initialize方法可能會(huì)被調(diào)用多次,但不代表父類初始化多次,每個(gè)類只會(huì)初始化一次。

② 調(diào)用方式: 消息機(jī)制objc_msgSend函數(shù)調(diào)用。

③ 調(diào)用順序: 先調(diào)用父類的+initialize,再調(diào)用子類的+initialize (先初識(shí)化父類,再初始化子類)

  • +initialize方法的調(diào)用方式為消息機(jī)制,而非像+load那樣直接通過函數(shù)地址調(diào)用。

9. 什么是線程安全?

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

10. 你接觸到的項(xiàng)目,哪些場景運(yùn)用到了線程安全?

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

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

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

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

Objective-C中的原子和非原子屬性
OC在定義屬性時(shí)有nonatomic和atomic兩種選擇
atomic:原子屬性,為setter/getter方法都加鎖(默認(rèn)就是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)存小的移動(dòng)設(shè)備

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

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

復(fù)制代碼

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

1. 運(yùn)用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這個(gè)函數(shù),它可以保證整個(gè)應(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,里面實(shí)現(xiàn)跟方法一,方法二一致就行.
+(id)allocWithZone:(struct _NSZone *)zone{
   return [self shareInstance];
} 

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

//   
復(fù)制代碼

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

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

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

復(fù)制代碼

引申2. 不使用dispatch_once 如何 實(shí)現(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 來保證線程安全,其它與上面這個(gè)方法一樣;
+ (instancetype)sharedSingleton {
    static id instance = nil;
    @synchronized (self) {
        if (!instance) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}
復(fù)制代碼

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

答 :整個(gè)程序公用一份資源的時(shí)候 例如 :

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

13.APNS的基本原理

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

首先是注冊

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

推送過程

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

// 推送過程如下圖 [圖片上傳失敗...(image-2bbbef-1647873407855)]()

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

  • RunLoop模式有哪些?

答 : iOS中有五種RunLoop模式

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

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

復(fù)制代碼
  • RunLoop的基本執(zhí)行原理

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

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

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

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

  • 程序開啟之后,要一直運(yùn)行,不會(huì)退出。 說白了就是為了讓程序不死


如何保證一個(gè)線程永遠(yuǎn)不死(常駐線程)

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

    // 保證一個(gè)線程永遠(yuǎn)不死
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] -forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

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

復(fù)制代碼

15. weak屬性?

1\. 說說你理解weak屬性?
復(fù)制代碼
1.實(shí)現(xiàn)weak后,為什么對象釋放后會(huì)自動(dòng)為nil?

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

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

延伸

  • objc中向一個(gè)nil對象發(fā)送消息將會(huì)發(fā)生什么?

首先 在尋找對象化的isa指針時(shí)就是0地址返回了, 所以不會(huì)有任何錯(cuò)誤, 也不會(huì)錯(cuò)誤

  • objc在向一個(gè)對象發(fā)送消息時(shí),發(fā)生了什么?
  - 首先是通過obj 的isa指針找到對應(yīng)的class
  - 先去操作對象中的緩存方法列表中objc_cache中去尋找 當(dāng)前方法,如果找到就直接實(shí)現(xiàn)對應(yīng)IMP
  - 如果在緩存中找不到,則在class中找到對用的Method list中對用foo
  - 如果class中沒有找到對應(yīng)的foo, 就會(huì)去superClass中去找
  - 如果找到了對應(yīng)的foo, 就會(huì)實(shí)現(xiàn)foo對應(yīng)的IMP

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

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

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

- Method resolution
- Fast forwarding
- Normal forwarding

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

復(fù)制代碼

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

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

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

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

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

17. static有什么作用?

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

**隱藏**

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

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

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

**持久**

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

**默認(rèn)初始化**

static修飾的變量,默認(rèn)初始化為0;
復(fù)制代碼

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

- objc_msgSend(recicver, selecter..)
復(fù)制代碼

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

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

20. 如何手動(dòng)觸發(fā)一個(gè)value的KVO

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

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

  • 舉例:取消Person類age屬性的默認(rèn)KVO,設(shè)置age大于18時(shí),手動(dòng)觸發(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;
    }
}
復(fù)制代碼

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

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

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

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

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

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

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

  • 取值方法valueForKey:的原理

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

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

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

21. ViewController生命周期

按照執(zhí)行順序排列:
1. initWithCoder:通過nib文件初始化時(shí)觸發(fā)。
2. awakeFromNib:nib文件被加載的時(shí)候,會(huì)發(fā)生一個(gè)awakeFromNib的消息到nib文件中的每個(gè)對象。      
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上消失。
復(fù)制代碼

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

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

三次握手

1.客戶端向服務(wù)端發(fā)起請求鏈接,首先發(fā)送SYN報(bào)文,SYN=1,seq=x,并且客戶端進(jìn)入SYN_SENT狀態(tài)

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

- 哎!
- 嗯
- 給你 
復(fù)制代碼

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

四次揮手

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

- 哎!
- 嗯
- 關(guān)了
- 好的
復(fù)制代碼

為什么需要四次揮手: 因?yàn)門CP是全雙工通信的,在接收到客戶端的關(guān)閉請求時(shí),還可能在向客戶端發(fā)送著數(shù)據(jù),因此不能再回應(yīng)關(guān)閉鏈接的請求時(shí),同時(shí)發(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ù)進(jìn)行傳輸之前,對數(shù)據(jù)進(jìn)行加密,然后再發(fā)送到服務(wù)器。這樣,就算數(shù)據(jù)被第三者所截獲,但是由于數(shù)據(jù)是加密的,所以你的個(gè)人信息讓然是安全的。這就是HTTP和HTTPS的最大區(qū)別。
  2. HTTPS的加密方式?

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

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

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

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

HTTP

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

HTTPS

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

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

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

  • POST不是通過URL進(jìn)行傳值,理論上不受限制。

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

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

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

  • post的請求過程

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

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

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

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


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

23. 有沒有使用過performSelector?

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

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

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

@end

@implementation Person

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

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

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

        // 第一個(gè)參數(shù):給哪個(gè)類添加方法
        // 第二個(gè)參數(shù):添加方法的方法編號(hào)
        // 第三個(gè)參數(shù):添加方法的函數(shù)實(shí)現(xiàn)(函數(shù)地址)
        // 第四個(gè)參數(shù):函數(shù)的類型,(返回值+參數(shù)類型) v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}
@end
復(fù)制代碼
  • 當(dāng)然面試的時(shí)候也可能問你這個(gè)
// 延時(shí)操作 和GCD的after 一個(gè)效果
[p performSelector:@selector(eat) withObject:nil afterDelay:4];
復(fù)制代碼
  • 你以為完了? 錯(cuò)了,大概率面試官會(huì)問你,*** 上面這段代碼放在子線程中 是什么樣子的?為什么?**

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

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

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

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

在子線程中調(diào)用打印線程 非1
復(fù)制代碼
  • 然后面試官開始飄了, 開始問你關(guān)于NSTimer相關(guān)問題?怎么辦? 答: 搞他!

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

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

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

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

// 利用CADisplayLink (iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會(huì)在每次刷新結(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);
復(fù)制代碼

引申: NStimer的循環(huán)引用?

  • 有的人會(huì)說, NSTimer本身的target會(huì)引用這self, 而self又引用這Timer就造成了循環(huán)引用, 那如果timer用weak聲明呢? 還會(huì)循環(huán)引用么? 答案:會(huì)的
  • 原因是NTtimer和Runloop是一個(gè)相互存在的東西, 別的道理我就不多BB, 就是Runloop和tmier相互引用,而Runloop永遠(yuǎn)不會(huì)銷毀,造成貸方面的“牽引” 所以蘋果出來了一個(gè)invalid的方法。
  • 優(yōu)化的方案還有別的, 例如利用NSProxy這個(gè)專門做消息轉(zhuǎn)發(fā)的虛類去優(yōu)化循環(huán)引用(這里也經(jīng)常會(huì)被問到。具體方案我不說, 自行百度,切記,如果兄弟你不知道這個(gè)玩意, 建議你看看,面試的時(shí)候被問到的概率還是挺大的。)

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

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

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

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

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

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

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

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

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

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

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

25. autoreleasePool 在何時(shí)被釋放?

  • ARC中所有的新生對象都是 自動(dòng)加autorelese的, @atuorelesepool 大部分時(shí)候解決了瞬時(shí)內(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)體里面的對象都是臨時(shí)創(chuàng)建使用的,就可以用@autoreleasepool 包起來,讓每次循環(huán)結(jié)束時(shí),可以及時(shí)釋放臨時(shí)對象的內(nèi)存

// for 和 for in 里面是沒有自動(dòng)包裝@autoreleasepool著的,而下面的方法是由@autoreleasepool自動(dòng)包圍的
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 這里被一個(gè)局部@autoreleasepool包圍著
}];
復(fù)制代碼
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"666"] autorelease];
[pool drain];

// 其作用于為drain 和 init 之間
復(fù)制代碼
  • 回歸正題@autoReleasePool什么時(shí)間釋放?
    • 一個(gè)被autoreleasepool包裹生成得對象,都會(huì)在其創(chuàng)建生成之后自動(dòng)添加autorelease, 然后被autorelease對象得釋放時(shí)機(jī) 就是在當(dāng)前runloop循環(huán)結(jié)束的時(shí)候自動(dòng)釋放的
    • 參考鏈接:blog.sunnyxx.com/2014/10/15/…

子線程中的autorelease變量什么時(shí)候釋放?

  • 子線程中會(huì)默認(rèn)包裹一個(gè)autoreleasepool的, 釋放時(shí)機(jī)是當(dāng)前線程退出的時(shí)候。

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

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

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

  • 先簡單解釋一下渲染機(jī)制

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

  • 一共三個(gè)階段

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

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

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

  • 稍微詳細(xì)說明

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

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

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

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

程序卡頓的原因?

  • 正常渲染流程
  • CPU計(jì)算完成之后交給GPU,來個(gè)同步信號(hào)Vsync 將內(nèi)容渲染到屏幕上
  • 非正常(卡頓/掉幀)的流程
  • CPU計(jì)算時(shí)間正?;蛘呗?,GPU渲染時(shí)間長了, 這時(shí)候Vsync信號(hào), 由于沒有繪制完全,CUP開始計(jì)算下一幀,當(dāng)下一幀正常繪制成功之后,把當(dāng)前沒有繪制完成的幀丟棄, 顯示了下一幀,于是這樣就造成了卡頓。

需要注意的是:Vsync時(shí)間間隔是固定的, 比如60幀率大的Vsync 是每16ms就執(zhí)行一個(gè)一次,類似定時(shí)器一樣

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

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

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

2.CPU渲染職能

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

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

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

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


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

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

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

基礎(chǔ)的

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

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

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

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

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

  • 光柵化 layer.shouldRasterize = YES

  • 遮罩layer.mask

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

  • 陰影l(fā)ayer.shadowXXX

進(jìn)階的

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

高階的

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

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

卡頓檢測的方法

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

繼續(xù)

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

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


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

基本概念:

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

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

  • [圖片上傳失敗...(image-74d78b-1647873407847)]

  • [圖片上傳失敗...(image-c53446-1647873407847)]

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

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

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

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

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

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

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

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

  • *** 怎么尋找當(dāng)前觸摸的是哪一個(gè)View?***

下面中兩個(gè)方法

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

// 判斷一個(gè)點(diǎn)是否落在范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
復(fù)制代碼

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

// 因?yàn)樗械囊晥D類都是繼承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\. 判斷點(diǎn)在不在當(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;

}
復(fù)制代碼
  • 判斷觸摸點(diǎn)是否在視圖內(nèi)?
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
復(fù)制代碼
  • tableView 加一個(gè)tap的手勢, 點(diǎn)擊當(dāng)前cell的位置 哪個(gè)事件被響應(yīng) 為什么?
  • tap事件被響應(yīng), 因?yàn)閠ap事件添加之后,默認(rèn)是取消當(dāng)前tap以外的所有事件的, 也就是說, tap事件處于當(dāng)前響應(yīng)者鏈的最頂端, 解決的辦法執(zhí)行tap的delagete, 實(shí)現(xiàn)
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
{  
    if([touch.view isKindOfClass:[XXXXcell class]])  
    {  
        return NO;  
    }  
    return YES;  
}

作者:執(zhí)筆續(xù)春秋
鏈接:https://juejin.cn/post/6854573212165111822

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,662評論 1 366
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內(nèi)容