開發小知識(二)

開發小知識(一)

開發小知識(二)

目錄

五十一、關聯對象

關聯對象的 key

實際開發中一般使用屬性名作為key。

objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");

另外一種方式是使用get方法的@selecor作為key。這里要知道 _cmd實際上等價于 @selector(getter),兩者都是 SEL類型。

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// 隱式參數 _cmd == @selector(getter)
objc_getAssociatedObject(obj, _cmd)
objc_getAssociatedObject(obj, @selector(getter))
關聯對象的懶加載
- (UIView *) testView{
    UIView * testView = objc_getAssociatedObject(self, _cmd);
    if (! testView) {
        testView = [[UIView alloc]init];
        objc_setAssociatedObject(self, _cmd, testView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return testView;
}

五十二、TCP 面向連接的本質是什么?TCP 和 UDP 的區別?

一般面試的時候問UDP和TCP這兩個協議的區別,大部分人會回答,TCP 是面向連接的,UDP 是面向無連接的。什么叫面向連接,什么叫無連接呢?在互通之前,面向連接的協議會先建立連接。例如,TCP 會三次握手,而 UDP 不會。為什么要建立連接呢?所謂的建立連接,是為了在客戶端和服務端維護連接,而建立一定的數據結構來維護雙方交互的狀態,用這樣的數據結構來保證所謂的面向連接的特性。

為了維護這個連接,雙方都要維護一個狀態機,在連接建立的過程中,雙方的狀態變化狀態如下。最初,客戶端和服務端都處于 CLOSED 狀態。首先,服務端處于 LISTEN 狀態,主要為了主動監聽某個端口。客戶端主動發起連接 SYN,變為 SYN-SENT 狀態。然后,服務端收到發起的連接,返回 SYN,并且 ACK 客戶端的 SYN,之后處于 SYN-RCVD 狀態。客戶端收到服務端發送的 SYN 和 ACK 之后,發送 ACK 的 ACK,之后處于ESTABLISHED 狀態,因為一發一收成功了。服務端收到 ACK 的 ACK 之后,也同樣變為 ESTABLISHED 狀態。


另外,TCP 是可以擁塞控制的。它意識到包丟棄了或者網絡的環境不好了,就會根據情況調整自己的行為,看看是不是發快了,要不要發慢點。UDP 就不會,應用讓發就發,從不考慮網絡狀況。

五十三、高效安全讀寫方案

讀寫操作中為了保證線程安全可以為讀和寫操作都添加鎖。但是此種情況似乎有些浪費,往往都是因為寫操作會引發線程安全問題,而讀操作一般不會引發線程安全問題。為了優化讀寫效率,一般是允許同一時間有多個讀操作,但同一時間不能有多個寫操作,且同一時間不能既有讀操作又有寫操作,即所謂的多讀單寫。針對該種情況,一般有兩種處理方法:讀寫鎖和異步柵欄函數。

讀寫鎖方案pthread_rwlock_t
@property (assign, nonatomic) pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);// 初始化鎖

- (void)read {
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)write{
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)dealloc{
    pthread_rwlock_destroy(&_lock);
}
異步柵欄函數方案

每次必須等前面所有讀操作執行完之后,才能執行寫操作。數據的正確性主要取決于寫入操作,只要保證寫入時,線程便是安全的,即便讀取操作是并發的,也可以保證數據的正確性。dispatch_barrier_async 使得操作在并發隊列里“有序進行”,保證了寫入操作的任務是在串行隊列里,即必須等所有讀操作執行完畢后再執行寫操作。注意這里的 隊列必須是dispatch_queue_create創建的同一個隊列 ,如果dispatch_barrier_async中傳入的是全局并發隊列,該函數就等同于dispatch_async效果。

@property (strong, nonatomic) dispatch_queue_t queue;
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }


如上圖,隨著時間的推移有一堆讀和寫操作,當執行到 dispatch_barrier_async 時會在寫的兩邊加上屏障(柵欄),使其和讀操作隔離。

另外提示,如果僅僅只是對寫操作加鎖,讀操作不做任何處理,并不能保證線程安全,僅對寫操作加鎖僅僅只能保證不會同時出現兩個或多個寫操作,并不能避免同一時刻既有寫操作又有讀操作。實現正在進行讀操作,此時來了第一個寫操作,但是相關鎖并沒有加鎖,所以讀寫操作可同時進行。

補充:對于線程安全方案,除了加鎖之外,還可以借助串行隊列確保代碼執行的順序,保證線程安全。

dispatch_queue_t writerRecordsQueue = dispatch_queue_create([@"serialQueue.yawei" UTF8String], DISPATCH_QUEUE_SERIAL);
- (void)clearRecords
{
    dispatch_async(writerRecordsQueue, ^{
        [self.records removeAllObjects];
    });
}

- (void)writeData:(id)data
{
    if (!data) {
        return;
    }
    dispatch_async(writerRecordsQueue, ^{
        [self.records addObject:data];
    });
}

五十四、死鎖

所謂死鎖,通常指有兩個線程 T1 和 T2 都卡住了,并等待對方完成某些操作。T1 不能完成是因為它在等待 T2 完成。T2 也不能完成,因為在等待 T1 完成。于是大家都完不成,就導致了死鎖(DeadLock),就類似一條比較窄的馬路有兩輛車相向而行,互相等著對方過了之后再過。

重要:sync 函數當前串行隊列 中添加任務會卡住當前線程,產生死鎖。這里要額外注意 當前串行隊列

案列一:

- (void)ViewDidLoad{
   NSLog(@"1");// 任務1
  dispatch_sync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任務2
  });
  NSLog(@"3");// 任務3
}
結合上述重要提示分析:

上述情況會產生死鎖。 ViewDidLoad 存在于主隊列,向主隊列 (當前串行隊列) 中添加了 NSLog(@"2"); 任務,主而隊列又是串行隊列,所以導致死鎖。

結合線程和隊列實際情況分析:

首先要知道線程和隊列的關系。執行代碼邏輯時,線程從隊列中取出任務并放入線程中執行,當線程中對應模塊執行完畢時,隊列同時移除對應模塊。


述方法展開后,主線程存在任務 1 、sync 、任務3,主隊列中原本僅存在viewDidLoad ,當主線程從任務 1 依次執行到 sync 時,此時會往主隊列中追加任務 2 。dispatch_sync 有個特點,要求立馬在當前線程執行任務。

  • 從主隊列方面看,按照隊列先進先進先出的原則,主隊列中的viewDidLoad 沒執行完,任務 2 只能等待 viewDidLoad 執行完畢再去執行;
  • 從主線程方面來看,因為dispatch_sync要求立馬在當前線程執行任務,任務 3 只能等待。

如此一來造成一個循環,任務3 要等同步線程中熱任務2執行完才能執行,而任務 2 排在任務 3 后面需要等待任務 3 執行完 ,最終誰也無法執行完形成死鎖。可另外參照下圖加深理解。

案列二:

- (void)ViewDidLoad{
   NSLog(@"1");// 任務1
  dispatch_ync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任務2
  });
  NSLog(@"3");// 任務3
}

dispatch_sync 改為 dispatch_asyncdispatch_async 不會要求立馬在當前線程執行任務,所以同案列一相比,不存在任務 3 等待 dispatch_async 的情況,所以這里不會產生死鎖。代碼執行順序為任務1、任務3、任務2。

案列三:

- (void)ViewDidLoad{
  dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_async(globalQueue, ^{ // 0
        [self test];
    });
 }
- (void)test{
  NSLog(@"1");// 任務1
  dispatch_sync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任務2
  });
  NSLog(@"3");// 任務3
}

同案列一相比,上述代碼放到子線程線程中執行,而不是放在 viewDidLoad(主線程) 中執行。任務1、dispatch_sync、任務1當前所處隊列為 全局并發隊列,而 dispatch_sync 是向 主隊列添加任務,所以不會產生死鎖。

案列四:

- (void)test{
    //手動創建的串行隊列
  dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ 
        NSLog(@"執行任務1");
        //往當前串行隊列添加任務
        dispatch_sync(queue, ^{ 
            NSLog(@"執行任務2");
        });
        NSLog(@"執行任務3");
    });
}

無論 test 方法在任何地方調用都會產生死鎖,因為上述方法已經滿足dispatch_sync當前串行隊列添加任務的條件,這里的當前串行隊列指手動創建的串行隊列 queue,同案列一相比,這里只是將主隊列替換為手動創建的串行隊列。

案列五:

- (void)test{
    //手動創建的串行隊列1
  dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//手動創建的串行隊列2
dispatch_queue_t queue2 = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ 
        NSLog(@"執行任務1");
        //往當前串行隊列添加任務
        dispatch_sync(queue2, ^{ 
            NSLog(@"執行任務2");
        });
        NSLog(@"執行任務3");
    });
}

此種情況不會產生死鎖,因為這里有兩個串行隊列,任務1、dispatch_sync、任務1當前所處隊列為 queue1 串行隊列,而 dispatch_sync 是向 queue2 串行隊列添加任務。此種情況同案列三類似。

五十五、如何理解代理和協議?

實際面試過程中有問到應試者對協議和代理的理解,個別應試者只知道代理和協議的用法,連協議和代理的意義都說不清楚。舉個簡單的例子:一位導演很忙,因為他要把主要精力放到電影創作上。因此需要找代理人把重要的瑣事分擔出去,或者說把重要的瑣事讓”代理人”去做。其中的代理人就是代碼中代理,協議主要是規定了代理人要做的事。 協議的用處還有很多,可看看此篇文章

五十六、MVP && MMVM

MVP

MVP 同 MVC 相比,本質上是將 Controller 的職責給分離出去,按照功能和業務邏輯劃分為若干個 Presenter。Controller 中引入 Presenter ,Presenter 中同樣也引入 Controller,Presenter 中處理各種業務邏輯,必要的時候再通過代理或 block 等形式回傳到 Controller 中。要注意,為了避免循環引用 Presenter 要弱引用 Controller。

@interface ViewController ()
@property (strong, nonatomic) Presenter *presenter;
@end
@interface Presenter()
@property (weak, nonatomic) UIViewController *controller;
@end
MVVM

MVVM 總的來說和 MVP 非常類似,唯一不同點在于 View 和 ViewModel 雙向綁定。實際開發通常是 Controller 中引入 ViewModel, ViewModel 中引入 Model,ViewModel 中會進行網絡請求并進行數據處理邏輯。View 中會引入 ViewModel 給 View 設置內容,并且 View 還會監聽 ViewModel 的變化,當 ViewModel 數據變化時,通過監聽更新 View 上對應內容,實現雙向綁定。因為 UI 的操作事件中可以動態改變模型,但是模型的改變不是很直接的體現到界面上,所以通常需要在 View 中監聽 ViewModel 的變化。這種監聽也可以通過監聽實現,可以通過 RAC 實現,但是 RAC 過重,有一定的學習和維護成本。建議使用 KVOController 實現這種監聽,如下一段代碼是 View 中引入 ViewModel ,重寫 ViewModel 的 set 方法,并監聽 ViewModel 的變化刷新 UI 。筆者認為沒有絕對好的架構模式,適合特定業務場景的架構模式才是好的架構。MVVM 特別適合那種模型和視圖雙向反饋較多的場景,比如列表頁面的選中和非選中狀態,通過改變 ViewModel 很輕松就能實現數據和界面的統一。 但是對于一般的業務場景而言(雙向反饋較少的場景),MVVM 同 MVC 相比處理能拆分 Controller 的業務邏輯之外,貌似也沒太多的優點,反而會增加調試的難度。假設出現一些 bug ,該 bug 可能源于視圖也可能源于 ViewModel,會增加 bug 定位的難度。

- (void)setViewModel:(ViewModel *)viewModel{
    _viewModel = viewModel;
    __weak typeof(self) waekSelf = self;
    [self.KVOController observe:viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        waekSelf.nameLabel.text = change[NSKeyValueChangeNewKey];
    }];
}

五十七、簡單工廠和工廠模式

簡單工廠和工廠模式都屬于類創建型模式。

簡單工廠模式

簡單工廠主要有三個部分組成:

  • 抽象產品:抽象產品是工廠所創建的所有產品對象的父類,負責聲明所有產品實例所共有的公共接口。
  • 具體產品:具體產品是工廠所創建的所有產品對象類,它以自己的方式來實現其共同父類聲明的接口。
  • 工廠類:實現創建所有產品實例的邏輯。
//抽象產品
//Operate.h文件
@interface Operate : NSObject
@property(nonatomic,assign)CGFloat numOne;
@property(nonatomic,assign)CGFloat numTwo;
- (CGFloat)getResult;
@end
//Operate.m文件
@implementation Operate
- (CGFloat)getResult{
    return 0.0;
}
@end
//具體產品1
//OperateAdd.m文件
@implementation OperateAdd
- (CGFloat)getResult{
    return self.numOne + self.numTwo;
}
@end
//具體產品2
//OperateSub.m文件
@implementation OperateSub
- (CGFloat)getResult{
    return self.numOne - self.numTwo;
}
@end

//工廠類
//OperateFactory.h文件
@class Operate;
@interface OperateFactory : NSObject
+ (Operate *)createOperateWithStr:(NSString *)str;
@end

//OperateFactory.m文件
@implementation OperateFactory
+ (Operate *)createOperateWithStr:(NSString *)str{
    if ([str isEqualToString:@"+"]) {
        OperateAdd *operateAdd = [[OperateAdd alloc] init];
        return operateAdd;
    }else if ([str isEqualToString:@"-"]){
        OperateSub *operateSub = [[OperateSub alloc] init];
        return operateSub;
    }else{
        return [[Operate alloc]init];
    }
}
@end
//使用
- (void)simpleFactoryTest{
    Operate *operate = [OperateFactory createOperateWithStr:@"+"];
    operate.numOne = 1;
    operate.numTwo = 2;
    NSLog(@"%f",[operate getResult]);
}

優點:最大的優點在于工廠類中包含了必要的判斷邏輯,根據客戶端的選擇條件動態實例化相關的類,對于客戶端而言去除了與具體產品的依賴。有了簡單工廠類后,客戶端在使用的時候只需要傳入“+” 或“-”即可,使用上相對來說簡單了很多。
缺點: 試想此時如果想在上述例子的基礎上增加乘法或除法操作,除了增加相應的子類之外,開發人員還需要在工廠類中改寫 if else 分支,至少要更改兩處地方。顯然,工廠類的改動違背了開放-封閉原則(對擴展是開放的,對更改是封閉的)。正因如此,才出現了所謂的工廠模式,工廠模式僅僅需要添加新的具體產品和新的具體工廠就能實現,原有代碼無需改動。

筆者在實際開發過程中使用過簡單工廠模式,具體說來:UICollectionView上有很多可動態配置的模塊,本地代碼提前寫好不同的模塊,然后根據后端接口返回的數據所包含的不同模塊標志,用工廠類動態創建不同的模塊,從而實現模塊的動態配置。每個模塊實際是一個 UICollectionViewCell ,它們統一繼承一個基類,基類中包含一個統一渲染的方法,由于各個不同模塊的基本參數配置一直,所以比較適合走統一抽象渲染接口。另外,類簇是簡單工廠的應用如:NSNumber 的工廠方法傳入不同類型的數據,則會返回不同數據所對應的 NSNumber 的子類。

工廠模式

工廠模式主要由四部分組成。

  • 抽象產品:同簡單工廠。
  • 具體產品:同簡單工廠。
  • 抽象工廠:聲明具體工廠的創建產品的接口。
  • 具體工廠:負責創建特定的產品,每一個具體產品對應一個具體工廠。

上述三個抽象產品和具體產品類無變化,即 Operate、OperateAdd 和 OperateSub 三個類無變化。

//抽象工廠
//OperationFactoryProtocol協議
@class Operate;
@protocol OperationFactoryProtocol <NSObject>
+ (Operate *)createOperate;
@end
//具體工廠1
//AddFactory.h文件
@interface AddFactory : NSObject<OperationFactoryProtocol>
@end
//AddFactory.m文件
@implementation AddFactory
+ (Operate *)createOperate{
    return [[OperateAdd alloc]init];
}
@end
//具體工廠2
//SubFactory.h文件
@interface SubFactory : NSObject<OperationFactoryProtocol>
@end
//SubFactory.m文件
@implementation SubFactory
+ (Operate *)createOperate{
    return [[OperateSub alloc]init];
}
@end

優點

  • 工廠模式相比簡單工廠而言,在擴展新的具體產品時候代碼改動更小。
  • 用戶只需要關心其所需產品對應的具體工廠是哪一個即可,不需要關心產品的創建細節,也不需要知道具體產品類的類名。
    缺點
  • 當系統中加入新產品時,除了需要提供新的產品類之外,還要提供與其對應的具體工廠類。隨著類的個數增加,系統復雜度也會有所增加。
  • 簡單工廠類只有一個工廠類,該工廠類可以創建多個對象;工廠模式中每個子類對應一個工廠類,每個工廠僅能創建一個對象。

五十八、適配器模式概念及應用

適配器設計模式數據接口適配相關設計模式。實際開發中有個場景特別使用適配器設計模式,一個封裝好的視圖組件可能在工程中不同的地方使用到,但是不同的地方使用的數據模型并不相同,此時可以借助對象適配器,創建新的適配器模型數據,而不應該在組件內部引入不同的數據模型,依據類型值進行判斷,使用不同模型的不同數據。如電商網站中的加減按鈕可能在不同的頁面中使用到,但不同頁面依賴的數據模型不同,此種情況就特別適合使用適配器模式。
兩個模型類。

@interface DataModel : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *phoneNumber;
@property (nonatomic, strong)UIColor *lineColor;
@end

@interface NewDataModel : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *phoneNumber;
@end

適配器協議。

@protocol BusinessCardAdapterProtcol <NSObject>
- (NSString *)name;
- (NSString *)phoneNumber;
@end

適配器類。

//.h 文件
@interface ModelAdapter : NSObject<BusinessCardAdapterProtcol>
@property (nonatomic, weak)id data;
- (instancetype)initWithData:(id)data;
@end
//.m 文件
- (instancetype)initWithData:(id)data{
    self = [super init];
    if (self) {
        self.data = data;
    }
    return self;
}
//根據類名適配
- (NSString *)name{
    NSString *name = nil;
    if ([self.data isMemberOfClass:[DataModel class]]) {
         DataModel *data = self.data;
         name = data.name;
    }else if ([self.data isMemberOfClass:[NewDataModel class]]){
        NewDataModel *data = self.data;
        name = data.name;
    }
    return name;
}
- (NSString *)phoneNumber{
    NSString *phoneNumber = nil;
    if ([self.data isMemberOfClass:[DataModel class]]) {
        DataModel *data = self.data;
        phoneNumber = data.phoneNumber;
    }else if ([self.data isMemberOfClass:[NewDataModel class]]){
        NewDataModel *data = self.data;
        phoneNumber = data.phoneNumber;
    }
    return phoneNumber;
}

視圖。

//.h 文件
@interface BusinessCardView : UIView
- (void)loadData:(id<BusinessCardAdapterProtcol>)data;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *phoneNumber;
@end

//.m 文件
- (void)loadData:(id<BusinessCardAdapterProtcol>)data{
    self.name = [data name];
    self.phoneNumber = [data phoneNumber];
}
- (void)setName:(NSString *)name{
    _name = name;
    _nameLabel.text = name;
}
- (void)setPhoneNumber:(NSString *)phoneNumber{
    _phoneNumber = phoneNumber;
    _phoneNumberLabel.text = phoneNumber;
}

使用。

- (void)viewDidLoad {
    [super viewDidLoad];
    // 創建UI控件
    cardView = [[BusinessCardView alloc] initWithFrame:CGRectMake(0, 0, 375, 667.5)];
    cardView.center = self.view.center;
    [self.view addSubview:cardView];
    // 初始化兩種不同d類型的模型
    model = [[DataModel alloc] init];
    model.name = @"測試一";
    model.phoneNumber = @"電話1";

    newmodel = [[NewDataModel alloc]init];
    newmodel.name = @"測試二";
    newmodel.phoneNumber = @"電話2";
    //設置初始數據
    BusinessCardAdapter *adapter = [[BusinessCardAdapter alloc] initWithData:model];
    [cardView loadData:adapter];
    UISwitch *btn = [[UISwitch alloc]initWithFrame:CGRectMake(50, 340, 50, 20)];
    [btn addTarget:self action:@selector(change:) forControlEvents:UIControlEventValueChanged];
    [self.view addSubview:btn];
}
- (void)change:(UISwitch *)btn{
    //切換數據
    ModelAdapter *adapter;
    if (btn.on == YES) {
       adapter = [[ModelAdapter alloc] initWithData:newmodel];
    }else{
       adapter = [[ModelAdapter alloc] initWithData:model];
    }
    //cardView與適配器連接
    [cardView loadData:adapter];
}

五十九、外觀模式概念及應用

外觀模式相對比較好理解,主要為子系統中的一組接口提供一個統一的接口。外觀模式定義了一個更高層次的接口,這個接口使得這一子系統更加容易使用。以下情況下可以考慮使用外觀模式:

  • 設計初期階段,應該有意識的將不同層分離,層與層之間建立外觀模式。
  • 開發階段,子系統越來越復雜,增加外觀模式提供一個簡單的調用接口。
  • 維護一個大型遺留系統的時候,可能這個系統已經非常難以維護和擴展,但又包含非常重要的功能,為其開發一個外觀類,以便新系統與其交互。

說的再直白一些,外觀模式就相當于在客戶端和子系統中間加了一個中間層。使用外觀模式可以使項目更好的分層,增強了代碼的擴展性。另外,客戶端屏蔽了子系統組件,使客戶端和子系統之間實現了松耦合關系。即使將后來想替換子系統客戶端也無需改動。

六十、策略模式概念及應用

策略模式由三部分組成:抽象策略、具體策略以及引入策略的主體。實際開發中有一種場景特別適合使用策略模式,輸入框 UITextField 的輸入規則可以使用該設計模式,判斷是輸入電話號碼、郵箱等格式是否正確。
抽象策略:

//.h 文件
@interface InputValidator : NSObject
@property (strong, nonatomic)NSString *errorMessage;
- (BOOL)validateInput:(UITextField *)input;
@end
//.m 文件
@implementation InputValidator
- (BOOL)validateInput:(UITextField *)input {
    return NO;
}
@end

兩個具體策略:

//郵箱策略
@implementation EmailValidator
- (BOOL)validateInput:(UITextField *)input {
    if (input.text.length <= 0) {
        self.errorMessage = @"沒有輸入";
    } else {
        BOOL isMatch = [input.text isEqualToString:@"1214729173@qq.com"];
        if (isMatch == NO) {
            self.errorMessage = @"請輸入正確的郵箱";
        } else {
            self.errorMessage = nil;
        }
    }
    return self.errorMessage == nil ? YES : NO;
}
@end

//電話號碼策略
@implementation PhoneNumberValidator
- (BOOL)validateInput:(UITextField *)input {
    if (input.text.length <= 0) {
        self.errorMessage = @"沒有輸入";
    } else {
        BOOL isMatch = [input.text isEqualToString:@"15201488116"];
        if (isMatch == NO) {
            self.errorMessage = @"請輸入正確的手機號碼";
        } else {
            self.errorMessage = nil;
        }
    }
    return self.errorMessage == nil ? YES : NO;
}
@end

引入策略的主體:

//.h 文件
@interface CustomTextField : UITextField
//抽象的策略
@property (strong, nonatomic) InputValidator *validator;
//初始化
- (instancetype)initWithFrame:(CGRect)frame;
//驗證輸入合法性
- (BOOL)validate;
@end

//.m 文件
@implementation CustomTextField
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}
- (void)setup {
    UIView *leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, self.frame.size.height)];
    self.leftView = leftView;
    self.leftViewMode = UITextFieldViewModeAlways;
    self.font = [UIFont fontWithName:@"Avenir-Book" size:12.f];
    self.layer.borderWidth = 0.5f;
}
- (BOOL)validate {
    return [self.validator validateInput:self];
}
@end

外部使用:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self initButton];
    [self initCustomTextFields];
}
- (void)initCustomTextFields {
    self.emailTextField = [[CustomTextField alloc] initWithFrame:CGRectMake(30, 80, Width - 60, 30)];
    self.emailTextField.placeholder = @"請輸入郵箱";
    self.emailTextField.delegate = self;
    self.emailTextField.validator = [EmailValidator new];
    [self.view addSubview:self.emailTextField];
    
    self.phoneNumberTextField = [[CustomTextField alloc] initWithFrame:CGRectMake(30, 80 + 40, Width - 60, 30)];
    self.phoneNumberTextField.placeholder = @"請輸入電話號碼";
    self.phoneNumberTextField.delegate = self;
    self.phoneNumberTextField.validator = [PhoneNumberValidator new];
    [self.view addSubview:self.phoneNumberTextField];
}
#pragma mark - 文本框代理
- (void)textFieldDidEndEditing:(UITextField *)textField {
    CustomTextField *customTextField = (CustomTextField *)textField;
    if ([customTextField validate] == NO) {
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:customTextField.validator.errorMessage preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *alertAction = [UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        }];
        [alertController addAction:alertAction];
        [self presentViewController:alertController animated:YES completion:nil];
    }
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.view endEditing:YES];
}

六十一、界面卡頓原因

屏幕成像的過程如下圖:



按照60FPS的刷幀率,每隔16ms就會有一次 VSync 到來(垂直同步信號)。VSync 到來意味著要將 GPU 渲染好的數據拿出來顯示到屏幕上,但是下圖中紅色區域中,由于CPU + GPU 的處理時間在 VSync 之后,所以此時紅色框右邊的時間段顯示的始終是上一幀的畫面,因此出現卡頓現象。所以實際開發中無論是 CPU 還是 GPU 消耗資源較多都可能造成卡頓現象。


六十二、[UIApplication sharedApplication].delegate.window&& [UIApplication sharedApplication].keyWindow的區別

參考此篇文章,實際開發中要格外留意 [UIApplication sharedApplication].keyWindow 的坑。

六十三、單例注意事項

創建單例的時候除了要考慮對象的唯一性和線程安全之外,還要考慮alloc initcopymutableCopy 方法返回同一個實例對象。關于allocWithZone可看此篇文章

+ (instancetype)sharedInstance {
    return [[self alloc] init];
}
- (instancetype)init {
    if (self = [super init]) {
        
    }
    return self;
}
//當執行 `alloc` 的時候,系統會自動調用分配內存地址的方法`allocWithZone:`。
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    static LogManager * _sharedInstanc = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstanc = [super allocWithZone:zone];//最先執行,只執行了一次
    });
    return _sharedInstanc;
}
-(id)copyWithZone:(struct _NSZone *)zone{
    return [LogManager sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone{
    return [LogManager sharedInstance];
}

六十四、性能優化總結

待更新。。。。。

六十五、內存區域

  • 1、棧:局部變量(基本數據類型、指針變量)作用域執行完畢之后,就會被系統立即收回,無需程序員管理(分配地址由高到低分配)。
  • 2、堆:程序運行的過程中動態分配的存儲空間(創建的對象),需要主動申請和釋放。
  • 3、BSS 段:沒有初始化的全局變量和靜態變量,一旦初始化就會從 BSS 段中收回掉,轉存到數據段中。
  • 4、(全局區)數據段:存放已經初始化的全局變量和靜態變量,以及常量數據,直到程序結束才會被立即收回。
  • 5、代碼段:程序編譯后的代碼內容,直到結束程序才會被收回。

六十六、符號表

iOS 構建時產生的符號表,是內存地址、函數名、文件名和行號的映射表。格式大概是:

<起始地址> <結束地址> <函數> [<文件名:行號>]

Crash 時的堆棧信息,全是二進制的地址信息。如果利用這些二進制的地址信息來定位問題是不可能的,因此我們需要將這些二進制的地址信息還原成源代碼種的函數以及行號,這時候符號表就起作用了。利用符號表將原始的 Crash 的二進制堆棧信息還原成包含行號的源代碼文件信息,可以快速定位問題。iOS 中的符號表文件(DSYM) 是在編譯源代碼后,處理完 Asset Catalog 資源和 info.plist 文件后開始生成,生成符號表文件(DSYM)之后,再進行后續的鏈接、打包、簽名、校驗等步驟。

六十七、指針和引用

在 C 和 OC 語言中,使用指針(Pointer)可以間接獲取、修改某個變量的值,C++中,使用引用(Reference)可以起到跟指針類似的功能。引用相當于是變量的別名,對引用做計算,就是對引用所指向的變量做計算,在定義的時候就必須初始化,一旦指向了某個變量,就不可以再改變從一而終。所以這也是存在的價值之一:比指針更安全、函數返回值可以被賦值。引用的本質就是指針,只是編譯器削弱了它的功能,所以引用就是弱化了的指針。

六十八、static & const & extern


  • static修飾局部變量:讓局部變量永遠只初始化一次。將局部變量的本來分配在棧區改為分配在靜態存儲區,靜態存儲區伴隨著整個應用,也就延長了局部變量的生命周期。
  • static修飾全局變量:本來是在整個源程序的所有文件都可見,static修飾后,改為只在申明自己的文件可見,即修改了作用域。

  • const:修飾變量主要強調變量是不可修改的。const 修飾的是其右邊的值,也就是 const 右邊的這個整體的值不能改變。
//如下代碼無法編譯通過
//const修飾str指針,所以str指針的內存地址無法改變,也即str指針不能改變內存地址指向。
 NSString * const str = @"test";
 //該行代碼表示:str指針指向了其它的內存
 str = @"123";

//const修飾 *str,也即str指針指向的內存地址,所以對修改str指針的指向無任何影響。
NSString const *str = @"test";
//該行代碼表示:str指針指向了其它的內存
 str = @"123";

一般聯合使用static和const來定義一個只能在本文件中使用的,不能修改的變量。相對于用#define來定義的話,優點就在于它指定了變量的類型。

//防止 reuseIdentifier 指針指向其它內存
static NSString * const reuseIdentifier = @"reuseIdentifier";

  • extern:主要是用來引用全局變量,先在本文件中查找,本文件中查找不到再到其他文件中查找。常把 extern 和 const 聯合使用在項目中創建一個文件,這個文件中包含整個項目中都能訪問的全局常量。

六十九、枚舉

枚舉的目的只是為了增加代碼的可讀性。iOS6 中引入了兩個宏來重新定義枚舉類型 NS_ENUM 與 NS_OPTIONS ,兩者在本質上并沒有差別,NS_ENUM多用于一般枚舉, NS_OPTIONS 則多用于帶有移位運算的枚舉。

NS_ENUM
typedef NS_ENUM(NSInteger, Test){
    TestA = 0,
    TestB,
    TestC,
    TestD
};
NS_OPTIONS
typedef NS_OPTIONS(NSUInteger, Test) {
    TestA = 1 << 0,
    TestB = 1 << 1,
    TestC = 1 << 2,
    TestD = 1 << 3
};

使用按位或(|)為枚舉 變量test 同時賦值枚舉成員TestATestBTestC

Test test = TestA | TestB;
test |= TestC;

使用按位異或(^)為枚舉 變量 test 去掉一個枚舉成員 TestC。ps: 兩者相等為0,不等為1。

Test test = TestA | TestB | TestC;
test ^= TestC;

使用按位與(&)判斷枚舉 變量test 是否賦值了枚舉成員 TestA

Test test = TestA | TestB;
if (test & TestA){
    NSLog(@"yes");
}else{
    NSLog(@"no");
}

七十、驗證碼的作用

待更新。。。。

七十一、幀率優化

Color Blended Layers(red)

png 圖片是支持透明的,對系統性能也會有影響的。最好不要設置透明度,因為透明的圖層和其他圖層重疊在一塊的部分,CPU 會做處理圖層疊加顏色計算,這種處理是比較消耗資源的。

Color Copied Images (cyan)

蘋果的 GPU 只解析 32bit 的顏色格式。
如果一張圖片,顏色格式不是 32bit ,CPU 會先進行顏色格式轉換,再讓 GPU 渲染。 就算異步轉換顏色,也會導致性能損耗,比如電量增多、發熱等等。解決辦法是讓設計師提供 32bit 顏色格式的圖片。圖片顏色科普文章:圖片的顏色深度/顏色格式(32bit,24bit,12bit)

Color Misaligned Images 像素對齊(yellow)

iOS設備上,有邏輯像素(point)和 物理像素(pixel)之分,像素對齊指的是物理像素對齊,對齊就是像素點的值是整數。UI 設計師提供的設計稿標注以及中的 frame 是 邏輯像素。GPU在渲染圖形之前,系統會將邏輯像素換算成 物理像素。point 和 pixel 的比例是通過[[UIScreen mainScreen] scale] 來制定的。在沒有視網膜屏之前,1point = 1pixel;但是2x和3x的視網膜屏出來之后,1point = 2pixel 或 3pixel

邏輯像素乘以 2 或 3 得到整數值就像素對齊了,反之則像素不對齊。像素不對齊會導致 GPU 渲染時,對沒對齊的邊緣進行插值計算插值計算會有性能損耗。

原圖片大小和視圖控件大小不一致,圖片為了對應在控件的相應的位置就需要做一些計算,然后確定圖片的位置,該種情況也比較消耗資源。一般可以通過繪制指定尺寸大小、不透明的圖片來優化性能。

Color Off-screen Rendered (yellow)

cornerRadius 屬性只應用于 layer 的背景色和邊線。將 masksToBounds 屬性設置為 YES 才能把內容按圓角形狀裁剪。同時設置 cornerRadiusmasksToBounds = YES ,并且屏幕中同時顯示的圓角個數過多,就會明顯感覺到卡頓和跳幀,只是設置 cornerRadius 并不會觸發此種現象。當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制,所以就需要屏幕外渲染被喚起。使用離屏渲染的時候會很容易造成性能消耗,因為在 OpenGL 里離屏渲染會單獨在內存中創建一個屏幕外緩沖區并進行渲染,而屏幕外緩沖區跟當前屏幕緩沖區上下文切換是很耗性能的。iOS9 之后系統設置圓角不再產生離屏渲染。設置 shadow***相關陰影屬性也會產生離屏渲染,解決方法是設置陰影路徑 shadowPath

無法避免離屏渲染的時候可嘗試使用光柵化來進一步做優化。光柵化是指將圖轉化為一個個柵格組成的圖象。shouldRasterize = YES 在其他屬性觸發離屏渲染的同時,會將光柵化后的內容緩存起來,如果對應的layer 及其 sublayers 沒有發生改變,在下一幀的時候可以直接復用,從而減少渲染的頻率。當使用光柵化時,可以在 Core Animation 開啟 Color Hits Green and Misses Red 來檢查該場景下光柵化操作是否是一個好的選擇。綠色表示緩存被復用,紅色表示緩存在被重復創建。如果光柵化的層變紅得太頻繁那么光柵化對優化可能沒有多少用處,反之就可以開啟。

七十二、內存數據擦除

敏感數據不想一直保留在內存中,可以通過特定的 API 擦除內存中的數據,比如 NSString:

@implementation NSString (MemoryClear)
/**
 內存數據及時擦除
 */
-(void)memoryClearStirng{
    const char*string = (char *)CFStringGetCStringPtr((CFStringRef)self,CFStringGetSystemEncoding());
    memset(&string, 0, sizeof(self));
}
@end

七十三、找不到方法怎么辦

交叉替換消息轉發機制的 forwardingTargetForSelector: 方法。

@implementation NSObject (Exception)

+ (void)load {
    //防止外部手動調用load方法,固load方法中最好都要寫上dispatch_once
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        @autoreleasepool {
           //交叉方法
           [objc_getClass("NSObject") swizzleMethod:@selector(forwardingTargetForSelector:) swizzledSelector:@selector(replace_forwardingTargetForSelector:)];
        }
    });
}
//替換后的消息轉發階段
- (id)replace_forwardingTargetForSelector:(SEL)aSelector{
    //先處理自身能處理的消息(這一塊的邏輯和一般的交叉方法有點區別:一般的交叉方法先處理異常,再調用之前的方法,這里相反)
    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];
    if ([self respondsToSelector:aSelector] || signature) {
        return [self replace_forwardingTargetForSelector:aSelector];
    }
    //返回其他消息處理對象,并在內部動態添加方法
    FakeForwardTargetObject *fakeTaget = [[FakeForwardTargetObject alloc] initWithSelector:aSelector];
    return fakeTaget;
}
@end

FakeForwardTargetObject 類。

id fakeIMP(id sender,SEL sel,...){
    return nil;
}
@interface FakeForwardTargetObject : NSObject
- (instancetype)initWithSelector:(SEL)aSelector;
@end

@implementation FakeForwardTargetObject
- (instancetype)initWithSelector:(SEL)aSelector{
    if (self = [super init]) {
        if(class_addMethod([self class], aSelector, (IMP)fakeIMP, NULL)) {
            MCLog(@"add Fake Selector:[instance %@]", NSStringFromSelector(aSelector));
            NSString *string = [NSString stringWithFormat:@"[%s:%d行]",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__];
             showExceptionAlert(string);
        }
    }
    return self;
}
@end

七十四、卡頓代碼監測原理

所謂的卡頓一般是在主線程做了耗時操作,卡頓監測的主要原理是在主線程的 RunLoop 中添加一個 observer,檢測從 即將處理Source(kCFRunLoopBeforeSources)即將進入休眠 (kCFRunLoopBeforeWaiting) 花費的時間是否過長。如果花費的時間大于某一個闕值,則認為卡頓,此時可以輸出對應的堆棧調用信息。具體可以參考此篇文章

七十五、同時實現 set & get

setget 方法單獨重寫任意一個方法都不會報錯,但是同時重寫會報錯。主要是因為重寫 getset 方法之后 @property 默認生成的 @synthesize 就不起作用,也就意味著對應的類不會自動生成成員變量,解決方案是手動添加成員變量。

七十六、main 中的 UIApplicationMain 函數

 int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
 }

UIApplicationMain 函數主要有以下兩個作用:

  • 創建一個應用程序以及創建應用程序代理
  • 建立一個事件循環來捕捉處理用戶的行為

UIApplicationMain 函數參數說明:

  • 1、參數 argcargv 是 C 標準的 main 函數的參數。其中, argc 表示參數個數;argv 表示參數指針,是指向指針的指針,也可以替換為 char **argv
  • 2、principalClassName 是應用程序對象所屬的類,該類必須繼承自 UIApplication 類。如果所屬類字符串的值為 nil, UIKit 就缺省使用UIApplication 類。
  • 3、delegateClassName 是應用程序類的代理類,該函數跟據 delegateClassName 創建一個 delegate對象,并將 UIApplication 對象中的 delegate 屬性設置為 delegate 對象。

七十七、nil、Nil、NULL、NSNull

  • object = nil 表示把這個對象釋放掉,稱為“空對象”。對于這種空對象,所有關于 retain 的操作都會引起程序崩潰,例如字典添加鍵值或數組添加新原素等。
  • NSNullnil 的區別在于,nil 是一個空對象,已經完全從內存中消失了,而如果想表達“我們需要有這樣一個容器,但這個容器里什么也沒有”的觀念時,就用到NSNull,稱之為值為空的對象NSNull 繼承自 NSObject,并且只有一個 null 類方法。這就說明 NSNull 對象擁有一個有效的內存地址,所以在程序中對它的引用不會導致程序崩潰。
  • nilNil 在使用上是沒有嚴格限定的,也就是說凡是使用 nil 的地方都可以用 Nil 來代替,反之亦然。
  • NULL就是典型 C 語言的語法,它表示一個空指針。如:int *ponit = NULL

七十八、iOS 系統結構

待更新。。。。

七十九、鑰匙串訪問

UUID 保存到 KeyChain 里面,即使APP刪了再裝回來,也可以從KeyChain中讀取回來,常第三方庫SSKeychain。使用keyChain Sharing還可以保證同一個開發商的所有程序針對同一臺設備能夠獲取到相同的不變的UDID。但是刷機或重裝系統后 UUID 還是會改變。假如項目 2 想使用項目 1 的 Keychain ,項目 2 要開啟Keychain Sharing 且 Keychain Groups 要包含項目 1。

八十、動態規劃思路

動態規劃算法一般有兩種求解方式:1、自頂向下的備忘錄法 2、自底向上。比如斐波拉契數列(Fibonacci)問題中,使用動態規劃的思路解決問題,可以避免類似遞歸解決方案的重復計算問題。保留計算結果,避免重復計算這也是動態規劃和分治策略的最大區別所在。

八十一、主線程更新 UI 原因

UI 必須放在主線程是因為 UIKit 為了提升性能,壓根沒加鎖,所以 UIKit 不是線程安全的。如果不在主線程里面操作,會出現什么樣的 UI ,誰也不敢保證。

八十二、設計原則

待更新。。。。

八十三、@selector、_cmd、SEL

@selector 是指向實際執行的函數指針(function pointer)的一個C字符串。
_cmd 實際上等價于 @selector(getter),兩者都是 SEL類型,都是方法選擇器,用于在類結構的方法分發表中搜索指定名字的方法實現/地址。

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)

// 隱式參數 _cmd == @selector(getter)
objc_getAssociatedObject(obj, _cmd)
objc_getAssociatedObject(obj, @selector(getter))

八十四、Clang & LLVM & GCC

LLVM 架構

LLVM 架構

LLVM 架構不同的前后端使用統一的中間代碼 LLVM Intermediate Representation (LLVM IR)。如上圖,如果需要支持一種新的編程語言(如C、Fortran、Haskell),只需要實現一個新的編譯器前端;如果需要支持支持一種新的硬件設備(如X86、PowerPC、ARM),只需要實現一個新的編譯器后端即可。而傳統的變異架構因為沒有LLVM IR,如果需要支持C、Fortran、Haskell三種語言和X86、PowerPC、ARM三種硬件,則需要九個編譯器(3*3 = 9,不區分前后端),而在 LLVM 架構中之需要支持三種編譯器前端和三種編譯器后端即可。

Clang

Clang 是 LLVM中的一個子項目,是基于 LLVM 架構的C/C++/Objective-C編譯器前端。

LLVM && Clang

LLVM是構架編譯器(compiler)的框架系統,以C++編寫而成,可以處理C、C++、OC等。在理解LLVM時,可以認為它包括了一個狹義的LLVM和一個廣義的LLVM。

  • 廣義的LLVM其實就是指整個LLVM編譯器架構,包括了前端(Clang)、后端(LLVM)、優化器、眾多的庫函數以及很多的模塊;
  • 狹義的LLVM其實就是聚焦于編譯器后端功能(代碼生成、代碼優化、JIT等)的一系列模塊和庫。Clang 就是一個編譯前端工具,而 LLVM 則負責后端處理。


    Clang & LLVM
Clang為前端,LLVM為后端

GCC

GCC 是另一個知名工具 GCC(GNU Compile Collection)則是一個套裝,包攬了前后端的所有任務,可以處理C、C++。

Clang 相比于 GCC 優勢:

  • Clang是一個高度模塊化開發的輕量級編譯器
  • 編譯速度快
  • 占用內存小、已與擴展(非常方便進行二次開發)。

八十五、生命周期

面試過程中,突然有人問到應用生命周期,瞬間懵掉。平時開發很少涉及,面試之前也沒看,自然不容易想起來。按照 App 操作,一般分為下面三種情況:

從非運行到前臺活躍:

  • [AppDelegate application:didFinishLaunchingWithOptions:]
  • [AppDelegate applicationDidBecomeActive:]

前臺活躍到退出:

  • [AppDelegate applicationWillResignActive:]
  • [AppDelegate applicationDidEnterBackground:]
  • [AppDelegate applicationWillTerminate:]

后臺到前臺:

  • [AppDelegate applicationWillEnterForeground:]
  • [AppDelegate applicationDidBecomeActive:]
順便補充下控制器生命周期:
1. initWithCoder:通過 nib 文件初始化時觸發
2. awakeFromNib:nib 文件被加載的時候,會發生一個awakeFromNib的消息到nib文件中的每個對象。     
3. loadView:開始加載視圖控制器自帶的 view
4. viewDidLoad:視圖控制器的 view 被加載完成  
5. viewWillAppear:視圖控制器的view將要顯示在 window上
6. updateViewConstraints:視圖控制器的 view 開始更新 AutoLayout 約束
7. viewWillLayoutSubviews:視圖控制器的 view 將要更新內容視圖的位置
8. viewDidLayoutSubviews:視圖控制器的 view 已經更新視圖的位置
9. viewDidAppear:視圖控制器的view已經展示到 window 上 
10. viewWillDisappear:視圖控制器的 view 將要從 window 上消失
11. viewDidDisappear:視圖控制器的 view 已經從 window 上消失

八十六、dealloc中取weakSelf

參考

八十七、assign 和 weak

如果以MRC和ARC進行區分修飾符使用情況,可以按照如下方式進行分組:

  • MRC:assign、retain、copy、readwrite、readonly、nonatomic、atomic 等。
  • ARC:assign、strong、weak、copy、readwrite、readonly、nonatomic、atomic 等。
    MRC 和 ARC 都可以用assign,但 weak 只存在于 ARC 中。assign 和weak 的主要區在于:當它們指向的對象釋放以后,weak 會被自動設置為nil,而 assign 不會,所以會導致野指針的出現,可能會導致crash。實際面試過面試官可會問把 delegate 的屬性修飾符 weak 改為 assign 會有什么影響。

八十八、#import & @class

#import#include 無需多說,二者功能基本相同,不過 #import 避免了重復引用的問題,在引用文件的時候不用自己進行重復引用處理。重點說一下 #import 和 @class 在實際開發中的注意注意事項(提高編譯速度、避免編譯錯誤)。

  • @class 只是告訴編譯器,其后面聲明的名稱是類的名稱,至于這些類是如何定義的,暫時不用考慮。但 #import 會鏈入該頭文件的全部信息,包括實體變量和方法等,但是這樣做會對編譯效率造成影響。比如有100個類都 #import ClassA.h, 那么在編譯的時候這100個類都會去對 ClassA 處理。如果 ClassA 被修改,那么這 100 個類都需要重新進行編譯,無疑增加了編譯時間不利于開發效率的提升。一般的做法是:頭文件中一般只需要知道被引用的類的名稱就可以了, 不需要知道其內部的實體變量和方法,所以在頭文件中一般使用 @class 來聲明這個名稱是類的名稱; 實現文件中,如果需要引用這個類的實體變量或者方法就用 #import,否則不做任何處理。這種做法可以避免掉一些不必要的 #import 操作,一定程度上可能提高編譯效率。

  • 如果有循環依賴關系,如: A–>B , B–>A 這樣的相互依賴關系,如果使用 #import 來相互包含,就會出現編譯錯誤;如果使用@class在兩個類的頭文件中相互聲明,則不會有編譯錯誤出現。

八十九、IQKeyBoard 原理

待更新。。。。

九十、self.name = 和 name =

待更新。。。。

九十一、ipa 包構成

  • _CodeSignature:文件的 hash 列表。里面有一個文件 CodeResources ,它是一個屬性列表,包含 bundle 中所有其他文件的列表。內部是一個字典,key 為文件名,value 是 Base64 格式的散列值。主要用于判斷一個應用程序的完整性,可防止修改文件。
  • AppIcon 圖片
  • LaunchImage 圖片
  • bundle 資源文件
  • Assets.car 資源文件
  • info.plist
  • nib 文件
  • 相應程序的 Unix 代碼可執行文件
  • embedded.mobileprovision 描述文件
  • 其它(工程中引入的 xml 文件、iconfont.ttf、gif 圖等)

九十二、MRC和ARC的關系

自動引用計數管理中(ARC),內存的申請、使用和釋放過程都交給系統自動實現,開發者不用關心里面的過程,事實上還是 MRC 的原理,只是系統幫我們做了管理。MRC 和 ARC 使用的編譯器也不相同前者是 GCC 后者是 LLVM 3.0。

ARC 是 LLVM 編譯器和 Runtime 相互協作的結果。ARC相對于MRC,不是在編譯時添加retain/release/autorelease這么簡單。應該是編譯期和運行期兩部分共同幫助開發者管理內存。

  • 編譯時期:ARC 利用LLVM編譯器幫助我們自動生成類似MRC 的 release、retain、autorelease 代碼。
  • 運行時期:類似弱引用指向的對象釋放問題和 Runtime 相關。程序運行過程中,監測到對象銷毀的時候,把對象對應的弱引用清空。

九十三、Foundation 和 CoreFoundation

Foundation對象是Objective-C對象,而Core Foundation對象是C對象,二者比較相似, Foundation下的類基本都是NS開頭,Core Foundation下的類基本是CF開頭的。

這里說一下兩者在iOS中的內存管理問題,以前在MRC情況下,都是開發人員手動管理對象內存,二者區別不大。但在ARC情況下,由于Foundation框架是OC對象,所以由系統自動管理內存,而Core Foundation框架是C對象,所以需要開發人員手動管理內存,不然會引起內存泄露。在ARC下,可以對兩個框架的類進行相互轉換,以NSString為例,有與之對應的CFStringRef,兩者之間可以通過__bridge、__bridge_transfer、__bridge_retained轉換:

  • __bridge:用于NSString與CFStringRef相互轉換,不改變對象的管理權所有者,按照本來對象的內存管理。本來是NSString,轉換為CFStringRef類型,依舊由系統管理;本來是CFStringRef,轉換為NSString,由開發人員管理。
  • __bridge_transfer:用于CFStringRef轉換成NSString,進行管理權移交,由系統自動管理。
  • __bridge_retained:用于NSString轉換成CFStringRef,剝奪了ARC管理權,需要開發人員手動管理。

九十四、簡單對象 MRC

不借助 autorelease。

Person *person2 = [[Person alloc] init];
[person2 release];//直接釋放

借助 autorelease(實際 MRC 開發)。

Person *person1 = [[[Person alloc] init] autorelease];//適當時機自動釋放

類似 [NSArray arrayWithObject:@""] 內部一般是有 autorelease 操作。

九十五、成員為對象 MRC 管理

不借助 autorelease 。

//.h文件
@interface Person : NSObject
{
   Dog *_dog;
}
- (void)setDog:(Dog *)dog;
- (Dog *)dog;


//.m文件
//person擁有dog,非autorelease技術,先release之前的再持有現在的
- (void)setDog:(Dog *)dog{
    //如果中途換了dog,不是之前的dog,先釋放之前的dog(對現在的dog沒影響),再retain現在的dog
    if (_dog != dog) {
        [_dog release];
        //該處的retain 和 dealloc 中的 release 對應,也即引用計數誰+1,最終誰負責-1。
        _dog = [dog retain];
    }
    //如果dog是現在的dog:第一次調用setDog方法時,即dog為nil的時候,已經對dog進行retain操作了,之后就無需再retain,因為第一次的結果導致dog的引用計數大于0,肯定不會被釋放,所以之后增加引用計數是無意義的
}
- (Dog *)dog{
    return _dog;
}

//先釋放成員變量,在釋放父類本身
- (void)dealloc{
    [_dog release];
    _dog = nil;
    //self.dog = nil;
    // 父類的dealloc放到最后
    [super dealloc];
}
Dog *dog = [[Dog alloc] init]; // dog:1
    
    Person *person = [[Person alloc] init];
    [person setDog:dog]; // dog:2
    [dog release]; // dog:1 (對應alloc)
    
    //因為是person操作,不能造成崩潰,所以setDog方法內部需要retain操作
    //如果中途換了dog不是之前的dog,要先釋放之前的dog,再retain操作
    [person setDog:dog];// dog:1
    
    //set內部有判斷,首次已經retain操作了,
    [person setDog:dog];// dog:1(引用計數不變)
    [person setDog:dog];// dog:1(set內部有判斷,引用計數不變) 
    
    [person release]; // dog:0

借助 autorelease(實際 MRC 開發)。

//.h 文件
@interface Person : NSObject
//注意這里的retain屬性修飾符
@property (nonatomic, retain) MJDog *dog;
+ (instancetype)person;
@end


//.m 文件
@implementation MJPerson
+ (instancetype)person{
    return [[[self alloc] init] autorelease];
}
- (void)dealloc{
    self.dog = nil;
    [super dealloc];
}
@end
//使用
 MJPerson *person = [MJPerson person];

九十六、UIScrollView 多頁面側滑返回手勢沖突

相關參考
參考
參考
iOS 觸控事件 UITouch 和手勢識別 UIGestureRecognizer

#import "UIScrollView+PanGesture.h"
@implementation UIScrollView (PanGesture)
//location_X可自己定義,其代表的是滑動返回距左邊的有效長度
- (BOOL)panBack:(UIGestureRecognizer *)gestureRecognizer {
    //是滑動返回距左邊的有效長度
    int location_X =0.15*[MCDevice screenWidth];
    if (gestureRecognizer == self.panGestureRecognizer) {
        UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint point = [pan translationInView:self];
        UIGestureRecognizerState state = gestureRecognizer.state;
        if (UIGestureRecognizerStateBegan == state ||UIGestureRecognizerStatePossible == state) {
            CGPoint location = [gestureRecognizer locationInView:self];
            //允許每個頁面都可實現滑動返回
            int temp1 = location.x;
            int temp2 =[MCDevice screenWidth];
            NSInteger XX = temp1 % temp2;
            if (point.x > 0 && XX < location_X) {
                return YES;
            }
            //只允許在第一張時滑動返回生效
            //            if (point.x > 0 && location.x < location_X && self.contentOffset.x <= 0) {
            //                return YES;
            //            }
        }
    }
    return NO;
}
/*
 開始進行手勢識別時調用的方法,返回NO則結束識別,不再觸發手勢,用處:可以在控件指定的位置使用手勢識別
 該功能中禁止scrollView 自己的側滑返回手勢,走導航控制器的側滑返回手勢
 */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    if ([self panBack:gestureRecognizer]) {
        return NO;
    }
    return YES;
}
@end

九十七、gestureRecognizer 和 UIControl 的關系

待更新。。。。

九十八、FDFullscreenPopGesture 原理

+ (void)load
{
    // Inject "-pushViewController:animated:"
    Method originalMethod = class_getInstanceMethod(self, @selector(pushViewController:animated:));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_pushViewController:animated:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        
        // Add our own gesture recognizer to where the onboard screen edge pan gesture recognizer is attached to.
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];

        // Forward the gesture events to the private handler of the onboard gesture recognizer.
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];

        // Disable the onboard gesture recognizer.
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    // Handle perferred navigation bar appearance.
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    // Forward to primary implementation.
    [self fd_pushViewController:viewController animated:animated];
}
  • load 方法交叉替換 NavigationController 的 pushViewController:animated: 方法。
    系統中每一個 NavigationController 默認有一個 interactivePopGestureRecognizer,但是這里把系統的 interactivePopGestureRecognizer 設置為禁用方式。創建自定義 pan 手勢并添加到 interactivePopGestureRecognizer 對應的View上,interactivePopGestureRecognizer 會操作一個指定的 target 、action(handleNavigationTransition), 通過Runtime動態獲取到指定的target 和 action添加到自定義的手勢上。

九十九、判斷模擬器和真機的坑

//正確的方法
#if TARGET_IPHONE_SIMULATOR  //模擬器

#elif TARGET_OS_IPHONE      //真機

#endif
//錯誤的方法
#if TARGET_OS_IPHONE      //真機

#endif

上述第二種方法無論是在真機還是模擬器環境,中間代碼都會執行,是蘋果的一個坑。

一百、內存泄漏檢測原理 (待補充)

MLeaksFinder 為基類 NSObject 添加一個方法 -willDealloc 方法,該方法的作用是,先用一個弱指針指向 self,并在一小段時間 (3 秒) 后,通過這個弱指針調用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接彈框提醒該對象可能存在內存泄漏。當我們認為某個對象應該要被釋放了,在釋放前調用這個方法,如果 3 秒后它被釋放成功,weakSelf 就指向 nil,不會調用到 -assertNotDealloc 方法,也就不會彈框提示泄漏;如果它沒被釋放(泄露了),-assertNotDealloc 就會被調用,具體是遍歷基于 UIViewController 的整棵 View-ViewController 樹,通過 UIViewController 的 presentedViewController 和 view 屬性,UIView 的 subviews 屬性等遞歸遍歷,依次調 -willDealloc,若 3 秒后沒被釋放,則存在泄漏。可參考該篇文章;

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}
例外機制

對于有些 ViewController,在被 pop 或 dismiss 后,不會被釋放(比如單例),因此需要提供機制讓開發者指定哪個對象不會被釋放,這里可以通過重載上面的 -willDealloc 方法,直接 return NO 即可。

手動擴展

MLeaksFinder目前只檢測 ViewController 跟 View 對象。為此,MLeaksFinder 提供了一個手動擴展的機制,你可以從 UIViewController 跟 UIView 出發,去檢測其它類型的對象的內存泄露。如下所示,我們可以檢測 UIViewController 內的 View Model。宏 MLCheck() 做的事就是為傳進來的對象建立 View-ViewController stack 信息,并對傳進來的對象調用 -willDealloc 方法。

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    MLCheck(self.viewModel);
    return YES;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。