iOS基礎面試知識點總結

Deft_MKJing宓珂璟發表於2017-02-28

基礎面試知識

##1.對屬性修飾符的理解
MRC下

  • assign
    主要用於修飾基本資料型別,setter方法也只是簡單的賦值,例如NSInter,CGFloat 如果用來修飾物件,並不持有物件,那麼物件的引用計數不變,如果這個時候物件被釋放了,他就可能成為野指標,不手動置為nil,在堆上很容易造成崩潰,而如果修飾的不是物件,那麼棧上的記憶體系統會自動處理,不會造成野指標
  • retain
    修飾物件,不能修飾基本資料型別 ,引用計數+1,適用於NSObject及子類

ARC下

  • Strong
    對應的setter方法是先release,然後再把引數retain,最後成員變數賦值,類似於retain
    表示指向並擁有物件,其修飾的物件引用計數會+1,該物件只要引用計數不為0則不會被銷燬,當然強行置為nil可以銷燬他
  • Copy
    對應的setter方法是先release,然後copy引數內容,建立一個新的記憶體地址,因此修改之前的地址不會修改到副本,減少對上下文的依賴,copy一般用於修飾具有可變應用型別的不可變物件身上,例如NSArray,NSString,NSDictionary,,為什麼呢?看下例子
	@property (nonatomic, strong) NSString *name;
	self.name = @"mikejing";
    NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"mutablejiaojiao"];
    self.name = mStr;
    [mStr appendString:@"111"];
    
    NSLog(@"%@",self.name);

可以看到,本身是不可變型別NSString,但是它會有可變型別的屬性在,如果你用Strong修飾他,當該指標指向了NSMutableString型別的資料,Strong只是把源資料的多了一個新的指標指向而已,當源資料改變,那麼原來設定的name不可變型別,就可以變了。這就是上面遇到修改之前的資料改變了現有的資料,上下文依賴了,因此copy來減少上下文依賴,如果本身是不可變型別NSstring,那麼Strong也一樣,如果變成可變型別了,那麼減少上下文依賴,copy就會對可變型別執行深copy,從而讓資料來源更乾淨

strong 與copy都會使引用計數加1,但strong是兩個指標指向同一個記憶體地址,copy會在記憶體裡拷貝一份物件,兩個指標指向不同的記憶體地址

舉個簡單的例子
@property (nonatomic,copy) NSString *name;
- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}

- (NSString *)name
{
    return [[_name copy] autorelease];
}
針對copy這個屬性,以string型別為例,copy就是普通的指標copy(淺copy),而mutableCopy就是內容copy(深copy),如果是mutableString型別的話,那麼copy就全是內容copy了 如果是容器型別,例如NSArray和NSDictionary,copy屬性操作的指標針對容器本身,而容器內部的都還是同一個指標引用,並不會根據容器的copy或者strong而變化,指標根據自己的屬性修飾符有關
- weak 不會保留傳入的屬性,不會使物件的引用計數+1,類似assign,但是當物件被釋放時,weak自動置nil,這就是weak區別assgin最大的區別 小知識:當屬性是readwrite的時候,如果重寫了getter的方法,那麼setter方法是系統提供的,那麼系統順便幫我們實現了成員變數,但是如果你重寫了setter和getter方法,那麼這個時候你把系統的兩個方法都實現了,那麼系統就需要你自己去實現成員變涼了,不然是無法獲取到的,你可以再implementation或者extension裡面進行實現,或者在h檔案實現,前兩者是@private的,後者是共有的
2018 2月補充 weak 和 assign的理解 1.assign適用於基本資料型別,weak是適用於NSObject物件,並且是一個弱引用。 2.assign其實也可以用來修飾物件。那麼我們為什麼不用它修飾物件呢?因為被assign修飾的物件(一般編譯的時候會產生警告:Assigning retained object to unsafe property; object will be released after assignment)在釋放之後,指標的地址還是存在的,也就是說指標並沒有被置為nil,造成野指標。物件一般分配在堆上的某塊記憶體,如果在後續的記憶體分配中,剛好分到了這塊地址,程式就會崩潰掉。 3.那為什麼可以用assign修飾基本資料型別?因為基礎資料型別一般分配在棧上,棧的記憶體會由系統自己自動處理,不會造成野指標。 4.weak修飾的物件在釋放之後,指標地址會被置為nil。所以現在一般弱引用就是用weak。weak使用場景: 5.在ARC下,在有可能出現迴圈引用的時候,往往要通過讓其中一端使用weak來解決,比如: delegate代理屬性,通常就會宣告為weak。 自身已經對它進行一次強引用,沒有必要再強引用一次時也會使用weak。比如:自定義 IBOutlet控制元件屬性一般也使用weak,當然也可以使用strong。

__block與__weak的區別
__block是用來修飾一個變數,這個變數就可以在block中被修改

__block:使用 __block修飾的變數在block程式碼塊中會被retain(ARC下會retain,MRC下不會retain)

__weak:使用__weak修飾的變數不會在block程式碼塊中被retain
同時,在ARC下,要避免block出現迴圈引用 __weak typedof(self)weakSelf = self;

這兩個都是和block相關,block就是OC對閉包的實現,閉包就是匿名函式,或者理解為指向函式的指標

block變數定義時為什麼用copy?block是放在哪裡的?
block本身是像物件一樣可以retain,和release。但是,block在建立的時候,它的記憶體是分配在棧(stack)上,可能被隨時回收,而不是在堆(heap)上。他本身的作於域是屬於建立時候的作用域,一旦在建立時候的作用域外面呼叫block將導致程式崩潰。通過copy可以把block拷貝(copy)到堆,保證block的宣告域外使用。

特別需要注意的地方就是在把block放到集合類當中去的時候,如果直接把生成的block放入到集合類中,是無法在其他地方使用block,必須要對block進行copy。

[array addObject:[[^{
    NSLog(@"hello!");
} copy] autorelease]];

Atomic和NonAtomic
預設是是執行緒安全的Atomic,加鎖操作,但是速度會很慢,我們就會寫上NonAtomic,保證速度,但是不保證多執行緒安全問題

//@property(nonatomic, retain) UITextField *userName;
//系統生成的程式碼如下:

- (UITextField *) userName {
    return userName;
}

- (void) setUserName:(UITextField *)userName_ {
    [userName_ retain];
    [userName release];
    userName = userName_;
}
/@property(retain) UITextField *userName;
//系統生成的程式碼如下:

- (UITextField *) userName {
    UITextField *retval = nil;
    @synchronized(self) {
        retval = [[userName retain] autorelease];
    }
    return retval;
}

- (void) setUserName:(UITextField *)userName_ {
    @synchronized(self) {
      [userName release];
      userName = [userName_ retain];
    }
}

為什麼不用atomic?其實atomic也不是執行緒安全的看如下程式碼

@property (atomic, assign) NSInteger count;


    self.count = 0;
    
    NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
    [threadA start];
    
    NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
    [threadB start];
}


- (void)doSomething {
    for (NSInteger i = 0; i < 10; i++) {
        [NSThread sleepForTimeInterval:1.0];
        self.count++;
        NSLog(@"self.count = %@ %@", @(self.count), [NSThread currentThread]);
    }
}
2018-08-27 16:37:35.619901+0800 test[16093:320829] self.count = 8 <NSThread: 0x60c0000756c0>{number = 4, name = (null)}
2018-08-27 16:37:36.621834+0800 test[16093:320828] self.count = 9 <NSThread: 0x60c000074880>{number = 3, name = (null)}
2018-08-27 16:37:36.621837+0800 test[16093:320829] self.count = 9 <NSThread: 0x60c0000756c0>{number = 4, name = (null)}
2018-08-27 16:37:37.623387+0800 test[16093:320829] self.count = 10 <NSThread: 0x60c0000756c0>{number = 4, name = (null)}

上面程式碼中,把屬性 count 宣告為 atomic 的。在 viewDidLoad 中建立了兩個執行緒 threadA 和 threadB,都去執行 doSomething 方法。在 doSomething 方法中,去給 self.count 的值通過每次迴圈 +1 增加 10 次,然後列印 self.count 的值。為了讓異常情況出現的概率提高,加入一句 [NSThread sleepForTimeInterval:1.0];。

執行上面的程式碼,會發現列印的結果中,最後一條 self.count 的值往往是小於 20 的,在中間的某些列印日誌中,會發現有些數字被重複列印的兩次。

self.count++;

這句程式碼做了兩件事,先讀取 self.count 的值,然後把讀取到的值 + 1 後賦值給 self.count。

由於 atomic 僅僅能保證寫是執行緒安全的,而不是保證 讀 -> +1 -> 寫,這個整體是執行緒安全的。

當兩個執行緒都執行到讀取完 self.count 的值後,再去寫,就會寫成一樣的值。

所以大部分情況下,為了保證執行緒安全,還是要自己加鎖,可以根據需要來保證某塊程式碼整體的執行緒安全。

執行緒安全的程式碼:

- (void)doSomething {
    for (NSInteger i = 0; i < 10; i++) {
        [NSThread sleepForTimeInterval:1.0];
        @synchronized(self){
            self.count++;
            NSLog(@"self.count = %@ %@", @(self.count), [NSThread currentThread]);
        }
    }
}

這就是為什麼不用atomic的原因了,反正加了也避免不了,索性都用Nonatomic,自己手動加鎖即可
http://liuduo.me/2018/02/08/objective-c-atomic/
https://stackoverflow.com/questions/8382523/setter-and-getter-for-an-atomic-property

##1.1說說你對Weak關鍵字的理解
Runtime維護了一個weak表,用於儲存指向某個物件的所有weak指標。weak表其實是一個hash(雜湊)表,Key是所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件的地址)陣列。

1、初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。

2、新增引用時:objc_initWeak函式會呼叫 objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。

3、釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。

追問的問題一:

1.實現weak後,為什麼物件釋放後會自動為nil?

runtime 對註冊的類, 會進行佈局,對於 weak 物件會放入一個 hash 表中。 用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為 0 的時候會 dealloc,假如 weak 指向的物件記憶體地址是 a ,那麼就會以 a 為鍵, 在這個 weak 表中搜尋,找到所有以 a 為鍵的 weak 物件,從而設定為 nil 。

追問的問題二:

2.當weak引用指向的物件被釋放時,又是如何去處理weak指標的呢?

1、呼叫objc_release

2、因為物件的引用計數為0,所以執行dealloc

3、在dealloc中,呼叫了_objc_rootDealloc函式

4、在_objc_rootDealloc中,呼叫了object_dispose函式

5、呼叫objc_destructInstance

6、最後呼叫objc_clear_deallocating,詳細過程如下:

a. 從weak表中獲取廢棄物件的地址為鍵值的記錄

b. 將包含在記錄中的所有附有 weak修飾符變數的地址,賦值為 nil

c. 將weak表中該記錄刪除

d. 從引用計數表中刪除廢棄物件的地址為鍵值的記錄

通俗來說就是物件引用計數為0的時候,也就是被釋放的時候會呼叫dealloc,然後在之前根據這個物件地址註冊建立的weak標中找到以該物件地址為key的記錄,將這個key對應value陣列的左右變數地址都置為nil,然後把value陣列清空,然後把key的記錄也移除,清乾淨

##1.3OC中堆和棧的區別

一個由C/C++編譯的程式佔用的記憶體分為以下幾個部分
1、棧區(stack)— 由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其
操作方式類似於資料結構中的棧。
2、堆區(heap) — 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回
收 。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列,呵呵。
3、全域性區(靜態區)(static)—,全域性變數和靜態變數的儲存是放在一塊的,初始化的
全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另
一塊區域。 - 程式結束後由系統釋放。
4、文字常量區 —常量字串就是放在這裡的。 程式結束後由系統釋放
5、程式程式碼區—存放函式體的二進位制程式碼。

申請方式:

棧:系統分配

堆:程式設計師自己申請

申請後系統的響應

棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢

出。

堆:首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,

會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列

中刪除,並將該結點的空間分配給程式,

申請大小的限制

棧:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。

堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。

申請效率的比較

棧由系統自動分配,速度較快。

堆是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便.

1.4 iOS記憶體管理理解

OC採用引用計數器對記憶體進行管理,當一個物件的引用計數(retainCount)為0,則被釋放。
MRC:每當alloc/new/copy/mutableCopy生成物件的時候引用計數+1,後續對變數的管理相關的方法有:retain, release 和 autorelease。retain 和 release 方法操作的是引用記數,當引用記數為零時,便自動釋放記憶體,執行dealloc。並且可以用 NSAutoreleasePool 物件,對加入自動釋放池(autorelease 呼叫)的變數進行管理,當 drain 時回收記憶體。

ARC:自動管理引用計數
雖然Apple自動幫我們管理了,但是也會有情況出現記憶體洩露
1.Block迴圈引用
2.WebView註冊的JS未釋放 Delegate沒有置nil
3.CoreFoundation框架下有些還是要手動管理

上面是很淺的回答,先看下Demo

UILabel *label = [UILabel new];
    
    CFIndex widx = CFGetRetainCount((__bridge CFTypeRef)label);
    NSLog(@"初始值count:%ld",widx);
    NSLog(@"");
    __weak typeof(label) weakLabel = label;
    CFIndex idx11 = CFGetRetainCount((__bridge CFTypeRef)label);
    CFIndex widx22 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
    NSLog(@"count:%ld",idx11);
    NSLog(@"count:%ld",widx22);
    NSLog(@"");
    __strong typeof(label) strongLabel = weakLabel;
    CFIndex idx111 = CFGetRetainCount((__bridge CFTypeRef)label);
    CFIndex widx222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
    CFIndex widx333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
    NSLog(@"count:%ld",idx111);
    NSLog(@"count:%ld",widx222);
    NSLog(@"count:%ld",widx333);
    
    NSLog(@"");
    __weak UILabel *lb = label;
    CFIndex idx1111 = CFGetRetainCount((__bridge CFTypeRef)label);
    CFIndex widx2222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
    CFIndex widx3333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
    CFIndex widx4444 = CFGetRetainCount((__bridge CFTypeRef)lb);
    NSLog(@"count:%ld",idx1111);
    NSLog(@"count:%ld",widx2222);
    NSLog(@"count:%ld",widx3333);
    NSLog(@"count:%ld",widx4444);
    
    
    2018-09-03 14:59:29.804428+0800 retaincount[27022:536855] 初始值count:1
    2018-09-03 14:59:29.804553+0800 retaincount[27022:536855]
    2018-09-03 14:59:29.804626+0800 retaincount[27022:536855] count:1
    2018-09-03 14:59:29.804719+0800 retaincount[27022:536855] count:2
    2018-09-03 14:59:29.804796+0800 retaincount[27022:536855]
    2018-09-03 14:59:29.804870+0800 retaincount[27022:536855] count:2
    2018-09-03 14:59:29.804944+0800 retaincount[27022:536855] count:3
    2018-09-03 14:59:29.805023+0800 retaincount[27022:536855] count:2
    2018-09-03 14:59:29.805125+0800 retaincount[27022:536855]
    2018-09-03 14:59:29.805193+0800 retaincount[27022:536855] count:2
    2018-09-03 14:59:29.805268+0800 retaincount[27022:536855] count:3
    2018-09-03 14:59:29.805337+0800 retaincount[27022:536855] count:2
    2018-09-03 14:59:29.805436+0800 retaincount[27022:536855] count:3

上面的程式碼可以看出
__weak修飾的物件,如果列印原指標,不會增加原指標的引用計數,如果列印__weak型別指標,會再原始基礎上+1,如論你加多少個weak,你列印weak指標總是原引用計數+1,這是為什麼呢?看下面的文章 底層程式碼
https://juejin.im/post/5b4c59a55188251ac9767872
裡面有個程式碼塊

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

這裡獲取引用計數的時候,還是會去判斷一下是否 has_sidetable_rc 也就是是有有weak hash表,我猜測當我們用strong型別訪問的時候,就為no,如果是weak型別指標呼叫的時候就是yes,這也就是上面的引用計數會在weak修飾下weak列印會+1,而strong還是原來的引用計數

如果用__unsafe_unretain來修飾就不會,因為這裡沒有維護weak hash表,不會有這一層判斷,但是最後一個weak修飾列印weak指標就還是比原來的引用計數+1

UILabel *label = [UILabel new];
    
    CFIndex widx = CFGetRetainCount((__bridge CFTypeRef)label);
    NSLog(@"初始值count:%ld",widx);
    NSLog(@"");
    __unsafe_unretained typeof(label) weakLabel = label;
    CFIndex idx11 = CFGetRetainCount((__bridge CFTypeRef)label);
    CFIndex widx22 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
    NSLog(@"count:%ld",idx11);
    NSLog(@"count:%ld",widx22);
    NSLog(@"");
    __strong typeof(label) strongLabel = weakLabel;
    CFIndex idx111 = CFGetRetainCount((__bridge CFTypeRef)label);
    CFIndex widx222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
    CFIndex widx333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
    NSLog(@"count:%ld",idx111);
    NSLog(@"count:%ld",widx222);
    NSLog(@"count:%ld",widx333);
    
    NSLog(@"");
    __weak UILabel *lb = label;
    CFIndex idx1111 = CFGetRetainCount((__bridge CFTypeRef)label);
    CFIndex widx2222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
    CFIndex widx3333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
    CFIndex widx4444 = CFGetRetainCount((__bridge CFTypeRef)lb);
    NSLog(@"count:%ld",idx1111);
    NSLog(@"count:%ld",widx2222);
    NSLog(@"count:%ld",widx3333);
    NSLog(@"count:%ld",widx4444);
    
    2018-09-03 16:00:17.617836+0800 retaincount[28912:574549] 初始值count:1
    2018-09-03 16:00:17.618007+0800 retaincount[28912:574549]
    2018-09-03 16:00:17.618091+0800 retaincount[28912:574549] count:1
    2018-09-03 16:00:17.618171+0800 retaincount[28912:574549] count:1
    2018-09-03 16:00:17.618246+0800 retaincount[28912:574549]
    2018-09-03 16:00:17.618330+0800 retaincount[28912:574549] count:2
    2018-09-03 16:00:17.618411+0800 retaincount[28912:574549] count:2
    2018-09-03 16:00:17.618486+0800 retaincount[28912:574549] count:2
    2018-09-03 16:00:17.618564+0800 retaincount[28912:574549]
    2018-09-03 16:00:17.618638+0800 retaincount[28912:574549] count:2
    2018-09-03 16:00:17.618714+0800 retaincount[28912:574549] count:2
    2018-09-03 16:00:17.618800+0800 retaincount[28912:574549] count:2
    2018-09-03 16:00:17.618884+0800 retaincount[28912:574549] count:3

##2.NSTimer為什麼不準
NSTimer會受到Runloop的影響 CADisplaylink一樣會受到影響
用GCD建立就不會

dispatch_source_create 建立
dispatch_source_set_timer 設定時間
dispatch_source_set_event_handler 設定時間回撥
disaptch_resume 啟動

##3.記憶體管理
alloc/new/copy/mutableCopy進行物件的建立
retain 引用計數 + 1
release -1
當引用計數為0的時候,物件被釋放,呼叫dealloc

  • 自己建立的物件,自己持有
id objc = [[NSObject alloc] init];
id objc1 = [NSObject new];
  • 非自己建立的物件,自己也能持有
id obj = [NSArray array];
[obj retain];
  • 在不需要持有物件的時候,釋放
id objc = [[NSObject alloc] init];
[objc release];
指向物件的指標仍舊保留在ob這個變數中,但是物件已經被釋放,不可以訪問
  • 非自己持有的物件無法釋放
id obj = [NSArray array];
[obj release];

##4.Runloop瞭解下
概念
一般來講執行緒只會執行一次任務,然後就會退出,因此就出現了一個機制,讓執行緒隨時能處理事件但並不退出。所以Runloop就是一個物件,管理需要處理的訊息和事件,並提供一個入口函式啟動do while迴圈。該迴圈能隨時處理事件和訊息,處理完進入休眠避免資源佔用,有訊息來時立刻被喚醒,除非有特定條件下就會退出

Runloop他的本質就是一個do,while迴圈
(1)保持程式的持續執行,當有事做時做事,
(2)負責監聽處理App中的各種事件(比如觸控事件、定時器事件、Selector事件)
(3)節省CPU資源,提高程式效能,需要的時候處理事件和訊息,否則進入休眠等待喚醒

與執行緒的關係
每個執行緒都有一個Run Loop,主執行緒的Run Loop會在App執行時自動執行,子執行緒中需要手動獲取執行,建立是發生在第一次獲取時,RunLoop 的銷燬是發生線上程結束時。獲取執行緒對應的Runloop的時候,全域性變數有一個字典和自旋鎖,字典存放執行緒對應的Runloop,按需懶載入建立Runloop和執行緒繫結,獲取的時候通過自旋鎖加鎖保證執行緒安全。

Runloop結構

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次呼叫 RunLoop 的主函式時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。當建立的loop,沒有任何上面的屬性,就會直接退出。
Run的時候必須制定mode,那麼這裡有個知識點是CommonMode,不能指定該Mode進行執行,但是可以把事件標記到該屬性,例如NSTimer時間標記到ModeItems裡面,而且系統的Default和TrackingMode都是Common屬性的,當切換Mode的時候,如果是Common屬性,會把新增到ModeItems裡面的item同步到對應的Mode,那麼無論切換到哪個CommonMode都會執行

內部執行邏輯
在這裡插入圖片描述

Apple應用
1.AutoreleasePool
App啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer。
第一個 Observer 監視的事件是 Entry(即將進入Loop),執行方法去建立pool,優先順序最高
第二個Observer監聽兩個事件,BeforeWaiting(準備進入休眠) 時呼叫pop and push,先清除舊pool,然後建立新pool,或者再退出的時候釋放自動釋放池子,優先順序最低

2.啟動時註冊了一些觀察者和事件回撥來處理事件響應,手勢識別和UI更新
2.1事件響應處理成source1,然後根據回撥方法分發處理

2.2手勢觸發會進行標記,通過新增BeforeWaiting/Exit的觀察者,在休眠前把標記的手勢處理

2.3頁面更新,也就是CADIsplayLink中iOS螢幕重新整理頻率1/60,當UI操作的時候改變UI層級,設定Frame,或者手動呼叫UIView/CALayer的SetNeedsLayout/SetNeedsDisaplay的時候,標記為待處理,放入全域性容器,Apple註冊了BeforeWaiting和Exit的的監聽,會在對應的回撥函式中遍歷所有標記的UI進行頁面更新。

3.NSTimer這個很容易理解,Runloop程式碼中也有很明確的處理函式

4.GCD中的dispatch_getmainqueue,libDispatch會向主執行緒Runloop傳送訊息,Runloop被喚醒,這裡也有對應的程式碼可以看到,一般喚醒無非就是處理NSTimer,dispatch_getmainqueue和source事件

開發者應用:
1.常駐執行緒 處理網路請求的回撥,子執行緒懶載入獲取並開啟loop,這裡需要手動新增一個mach事件,保活
2.Common在不同模式下也執行NSTimer
3.新增Observe屬性的觀察,在即將進入休眠的ASDK的原理,以下就是AS框架的Runloop應用,這裡把排版,繪製等操作放到非同步做,然後儲存起來
在kCFRunLoopBeforeWaiting和kCFRunLoopExit兩個activity的回撥中將之前非同步完成的工作同步到主執行緒中去。

使用者操作卡頓以及螢幕重新整理頻率的理解
1.iOS顯示系統是由VSync驅動的,VSync由硬體生成,也就是每秒鐘60次,
在這裡插入圖片描述
在 VSync 訊號到來後,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主執行緒開始在 CPU 中計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時螢幕會保留之前的內容不變。這就是介面卡頓的原因。
對應到Runloop裡面就是在App啟動的Runloop裡面註冊回撥函式CFRunLoopSource接收mach_port傳遞過來的時鐘訊號通知。隨後source的回撥驅動整個App的動畫和UI顯示。

Core Animation 在 RunLoop 中註冊了一個 Observer,監聽了 BeforeWaiting 和 Exit 事件。這個 Observer 的優先順序是 2000000,低於常見的其他 Observer。當一個觸控事件到來時,RunLoop 被喚醒,App 中的程式碼會執行一些操作,比如建立和調整檢視層級、設定 UIView 的 frame、修改 CALayer 的透明度、為檢視新增一個動畫;這些操作最終都會被 CALayer 捕獲,並通過 CATransaction 提交到一箇中間狀態去(CATransaction 的文件略有提到這些內容,但並不完整)。當上面所有操作結束後,RunLoop 即將進入休眠(或者退出)時,關注該事件的 Observer 都會得到通知。這時 CA 註冊的那個 Observer 就會在回撥中,把所有的中間狀態合併提交到 GPU 去顯示;如果此處有動畫,CA 會通過 DisplayLink 等機制多次觸發相關流程。

亦或者
當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動呼叫了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全域性的容器去。

蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回撥去執行一個很長的函式:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函式裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 介面。

OK 下面按我個人理解解釋下VSync這個圖卡頓是如何在Runloop中影響的。
一次VSync只是把緩衝區內計算好渲染好的影象顯示出來,根據上面的介紹,UI的點選事件之類的其實是由Source1來觸發的,手勢就比如各種UI操作就是把手勢或者UIView/CALayer標記為Dirty,新增Observer,在BeforeWaiting/Exit的時候,系統設有回撥函式操作更新UI(這裡的回撥函式會在App啟動的主執行緒Runloop結構體新增),VSync是1/60一次重新整理頻率,這是由mach_port傳遞硬體訊號給App註冊的回撥函式,也就是上面提到的Source0來喚醒Runloop,可以理解為主執行緒的Runloop一直在處於休眠喚醒切換,一秒鐘理論上能切換60次,隨後Source0的對應的回撥顯示動畫和UI,那麼問題就來了,一次手勢或者一些其他操作導致需要UI的繪製,排版,渲染以及圖片的壓縮,然後交給GPU進行合成和渲染,如果時間過長,超過了1/60,那麼就會跳幀。通常來說,計算機系統中 CPU、GPU、顯示器是以上面這種方式協同工作的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成後將渲染結果放入幀緩衝區,隨後視訊控制器會按照 VSync 訊號逐行讀取幀緩衝區的資料,經過可能的數模轉換傳遞給顯示器顯示。也就是一次耗時的計算會錯過時間把結果放入緩衝區,就會掉幀。
1.Runloop do while程式碼中可以看到,如果App處於靜止狀態,系統硬體發出的時鐘通知VSync訊號,通過Source1喚醒Runloop,這個時候Runloop的週期可以認為是一個VSync,但這是不準確的,Runloop沒有周期限制,然後會檢測Timer,Obeserve,Source等事件,其中Observer中會有BeforeWaiting/Exit的狀態被監聽,例如手勢和Core Animation的,UIView和CALayer更新等,這一次喚醒的這些UI處理例如繪製,排版,圖片壓縮處理等,以及GPU的合成和渲染,如果正常情況下都在主執行緒的話就會很耗時,這裡Timer沒有,Source沒有,Observer的事件就是被手勢和UI處理包攬了,因此,Runloop在喚醒執行一次迴圈後立馬進入mach_msg()進入休眠,之前就會發出通知處理繪製等耗時操作,如果這些操作沒能在一次VSync中處理完,GPU就沒能把新的影象資源提交給緩衝區,就會跳幀,就卡頓了。因此ASDK就把這些耗時操作丟到了後臺執行緒,他會在系統當 RunLoop 進入休眠前、CA 處理完事件後(一定是系統方法處理完之後),然後把UI處理的耗時操作結果從容器中提取出來即可(耗時操作已經被丟到了後臺),這麼做如果資料沒處理完,緩衝區也會有資料,只是頁面是空的而已,但是不會像上面一樣連結果都沒來得及放入緩衝區,直接卡頓了。
注意:這裡一次VSync的觸發,就會跑一次Runloop程式碼,正常下沒有Timer和Source的耗時操作,會直接進入BeforeWaiting的Observer通知,這個通知被監聽的函式會執行繪製操作,這就是那些CPU或者GPU耗時操作,預設在主執行緒,無法完成把資料放入GPU處理後的緩衝區,會錯過下一次VSync的顯示,卡頓

2.如果App使用者在操作,手勢等,可以理解為 VSync-------VSync之間,使用者在滑動螢幕看東西,靜止狀態下一次CPU和GPU處理完提交給緩衝區的影象時間是1/60,如果使用者在滑動,時間就會少於1/60了,怎麼理解呢?就是上面靜止狀態下,一次Runloop的喚醒會執行Timer,Observer和Source事件,靜止狀態下可以理解為沒有Timer和Source0,只有Source1,loop會馬上進入Observer傳送BeforeWaiting的通知,讓監聽者處理耗時(Core Animation等),現在操作情況下無非就是多了Source0的時間,在BeforeWaiting之前還要標記下UI(SetNeedsLayout或SetNeedsDisplay),多了這一步而已,然後繼續進入BeforeWaiting,CoreAnimtion會把通話提交到一箇中間態,UIView和CALayer(手勢操作的更改物件)會遍歷把標記為需要更新的物件進行繪製和調整,然後交給GPU處理完給緩衝區,如果UI的繪製等耗時操作更多,就更容易卡頓,因此,更需要把這些耗時的操作丟到後臺執行緒,避免主執行緒的CPU和GPU計算卡頓,導致來不及把下一幀資料顯示出來,即便是空的資料,那也是能顯示,不會說緩衝區沒資料,就會卡頓了。

3.VSync是用來喚醒一次Runloop週期的,針對UI處理來講,期間Timer,Observer和Source0,Source1都會在這個迴圈方法內進行物件的標記(SetNeedsLayout等),標記後Runloop就會馬上進去休眠狀態併發出BeforeWaiting的通知,觀察者(Core Animation,UIView/CALayler,ASDK等)處理耗時操作,如果這些操作是在主執行緒並且真的太耗時,很明顯,下一次VSync到來的時候,CPU或者GPU沒能把資料放入緩衝區,顯示不了,就掉幀,卡爆了。。。。。。這個時候ASDK這種耗時操作,就能把操作放入後臺執行緒的在顯示的時候例如CALayer的dispalay,直接放進去後臺併發佇列,在最後BeforeWaiting的時候去處理好的佇列裡面取結果即可,不在主執行緒做任何複雜的計算,從而提高流暢的滑動體驗。

燈神ASDK
ASDK goole的UI框架原理
一個自稱用cell實現Runloop應用的人

__unsafe_unretained __typeof__(self) weakSelf = self;
    void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      [weakSelf processQueue];
    };
    _runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock);
    CFRunLoopAddObserver(_runLoop, _runLoopObserver,  kCFRunLoopCommonModes);

##4.什麼是Runtime
Runtime是用C++編寫的執行時庫,為 C 新增了物件導向的能力,這也是區別於C語言這樣靜態語言的重要特性,C語言函式呼叫在編譯期間就已經決定了,在編譯完成之後直接順序執行。OC是動態語言(多型和執行時),函式呼叫就是訊息傳送,在編譯期間不知道具體呼叫哪個函式,所以Runtime就是去解決執行時找到呼叫哪個方法的問題
多型:OC中的多型是不同物件對同一訊息的不同響應方式,子類通過重寫父類的方法來改變同一方法的實現,體現多型性
總結:三個能力
1.物件導向能力
2.動態載入類資訊,進行訊息的分發
3.訊息轉發

順序:
instance—>class---->method----->sel----->imp---->實現函式

例項物件中存放isa指標,有isa指標就可以找到實力變數對應的所屬類,類中存放著例項方法列表,SEL為Key,IMP為value,在編譯期間,根據方法名會生成一個唯一的int識別符號,這就是SEL標識,IMP就是函式指標,指向最終函式實現。runtime核心就是objc_msgSend函式,通過給SEL傳遞訊息,找到匹配的IMP

##4.1 iOS訊息轉發機制
Runtime機制詳解
訊息轉發機制如何保護程式找不到方法而崩潰
訊息轉發機制基本分為三個步驟:

1、動態方法解析 resolveInstanceMethod 和 resolveClassMethod

2、備用接受者 forwardingTargetForSelector

3、完整轉發 forwardInvocation 和 methodSignatureForSelector
這裡寫圖片描述

1.動態方法解析
物件在接收到未知的訊息時,首先會呼叫所屬類的類方法+resolveInstanceMethod:(例項方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知訊息新增一個”處理方法”“。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在執行時通過class_addMethod函式動態新增到類裡面就可以了。
可以理解為動態在該所屬類下面根據Sel重新指定對應的IMP(class_addMethod)

2.備用接收者
動態方法解析無法處理訊息,則會走備用接受者。這個備用接受者只能是一個新的物件,不能是self本身,否則就會出現無限迴圈。如果我們沒有指定相應的物件來處理aSelector,則應該呼叫父類的實現來返回結果
可以理解為上面resolveInstanceMethod沒有在本類實現,那麼該方法就在另一個類來進行實現

3.最終完整訊息轉發
如果動態方法解析和備用接受者都沒有處理這個訊息,那麼就會走完整訊息轉發:
該方法我感覺和上面的備用接收是一樣的操作,至少備用接收者無法設定為self,而該方法可以設定任意物件進行訊息的實現

具體的程式碼上面兩個連結都有,這補充一個可用的知識點
如何避免出現unrecognize selector的崩潰
1.物件未接收到訊息時會進行動態訊息轉發,第一步就是動態方法解析,例如例項化方法會呼叫resolveInstanceMethod 進行攔截,一般不再這裡處理
2.這裡不處理的話就會走到下一步,也就是備用接收者,forwardingTargetSelector來進行訊息攔截(因此,給一個Çategory就會是非常好的轉發接收者來處理避免找不到方法而崩潰)

// A Selector for a method that the receiver does not implement.
// 當category重寫類已有的方法時會出現此警告。
// Category is implementing a method which will also be implemented by its primary class
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"unrecognized selector : classe:%@ sel:%@",NSStringFromClass([self class]),NSStringFromSelector(aSelector));

    // 元類 meta class 建立 重新指定Selector 防止崩潰  http://ios.jobbole.com/81657/
//    1、為”class pair”分配記憶體 (使用objc_allocateClassPair).
//    2、新增方法或成員變數到有需要的類裡 (我已經使用class_addMethod新增了一個方法).
//    3、建立出來

    // 用objc_allocateClassPair建立一個自定義名字的元類
    Class class = objc_allocateClassPair(NSClassFromString(@"NSObject"), "UnrecognizedSel", 0);

    // 類新增方法 Sel 和 Imp
    class_addMethod(class, aSelector, class_getMethodImplementation([self class], @selector(customMethod)), "v@:");
//    class_addIvar(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
//    objc_registerClassPair(class)
    // 建立
    id tempObject = [[class alloc] init];
    return tempObject;
}
#pragma clang diagnostic pop


- (void)customMethod{
    NSLog(@"呵呵");
}

runtime可以做的幾個點:
1.json轉model
2.Category動態關聯屬性
3.MethodSwizzling (1.全域性VC容器 2.無碼埋點 3.全域性拖翻手勢 4.避免崩潰越界啥的替換原有方法 5.通過hook alloc new dealloc等,主要思路是在一個時間切片內檢測物件的宣告週期以觀察記憶體是否會無限增長。通過 hook 掉 alloc,dealloc,retain,release 等方法,來記錄物件的生命週期。)
這個hook可以操作很多東西
4.JSPatch
5.訊息轉發失敗檢測

##5.setNeedLayout,setNeedsDisplay,layoutIfNeed的區別

  • setNeedLayout–> 會在恰當的時候呼叫LayoutSubViews
    自定義textView,當外面先呼叫Placeholder的時候,如果自己寫個方法進行寬度高度的bounding計算,但是設定frame是在這句程式碼之後的,那麼這個時候你得到的frame是不正確的,你一定要在LayoutSubView才能得到最正確的frame,因此,你設定placeHolder的時候呼叫setNeedLayout,他就會在恰當的時候呼叫layoutSubViews,然後計算出正確的該顯示的frame
  • setNeedsDisplay --> 會呼叫drawInRect的方法進行重繪
  • layoutIfNeed—> 標記了UI佈局,會在恰當的時候進行佈局,例如你更新約束,修改屬性沒有變化的時候,你直接呼叫他,強行繪製,會在下一個Runloop重新整理週期的時候進行重繪 1/60秒 setNeedLayout只是標記,正常情況下在下一幀的時候重新整理佈局,如果呼叫layoutIfNeed,就會立馬進行佈局

setNeedsDisplay和setNeedsLayout
1,UIView的setNeedsDisplay和setNeedsLayout方法
首先兩個方法都是非同步執行的。而setNeedsDisplay會呼叫自動呼叫drawRect方法,這樣可以拿到 UIGraphicsGetCurrentContext,就可以畫畫了。而setNeedsLayout會預設呼叫layoutSubViews,
就可以 處理子檢視中的一些資料。
綜上所訴,setNeedsDisplay方便繪圖,而layoutSubViews方便出來資料。
layoutSubviews在以下情況下會被呼叫:
1、init初始化不會觸發layoutSubviews。
2、addSubview會觸發layoutSubviews。
3、設定view的Frame會觸發layoutSubviews,當然前提是frame的值設定前後發生了變化。
4、滾動一個UIScrollView會觸發layoutSubviews。
5、旋轉Screen會觸發父UIView上的layoutSubviews事件。
6、改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件。
7、直接呼叫setLayoutSubviews。

drawRect在以下情況下會被呼叫:
1、如果在UIView初始化時沒有設定rect大小,將直接導致drawRect不被自動呼叫。drawRect呼叫是在Controller->loadView, Controller->viewDidLoad 兩方法之後掉用的.所以不用擔心在控制器中,這些View的drawRect就開始畫了.這樣可以在控制器中設定一些值給View(如果這些View draw的時候需要用到某些變數值).
2、該方法在呼叫sizeToFit後被呼叫,所以可以先呼叫sizeToFit計算出size。然後系統自動呼叫drawRect:方法。
3、通過設定contentMode屬性值為UIViewContentModeRedraw。那麼將在每次設定或更改frame的時候自動呼叫drawRect:。
4、直接呼叫setNeedsDisplay,或者setNeedsDisplayInRect:觸發drawRect:,但是有個前提條件是rect不能為0。
以上1,2推薦;而3,4不提倡

drawRect方法使用注意點:
1、若使用UIView繪圖,只能在drawRect:方法中獲取相應的contextRef並繪圖。如果在其他方法中獲取將獲取到一個invalidate的ref並且不能用於畫圖。drawRect:方法不能手動顯示呼叫,必須通過呼叫setNeedsDisplay 或者 setNeedsDisplayInRect,讓系統自動調該方法。
2、若使用calayer繪圖,只能在drawInContext: 中(類似於drawRect)繪製,或者在delegate中的相應方法繪製。同樣也是呼叫setNeedDisplay等間接呼叫以上方法
3、若要實時畫圖,不能使用gestureRecognizer,只能使用touchbegan等方法來掉用setNeedsDisplay實時重新整理螢幕


##**6.Xcode黃色資料夾和藍色資料夾的區別** 當你勾選Çreatgroup的時候就是出來黃色資料夾,個人理解為ipa包之外的引用

勾選creat folders refrence的時候是藍色資料夾,這個意思是直接存放到包裡面,別人下載了你的app,就能開啟包內容,看到裡面的資源

當你做app換膚的時候,這個時候做幾套一樣名字的圖片,然後分別放到A,B,C三個不同的資料夾,然後放到包內部,使用的時候直接用NSBundle mainbundle path載入檔案路徑就行了, skin/%@/%@即可


UIImage imageWithContentOfFIle來獲取,那麼你在外面直接用之前的方法名就好了,你選擇皮膚的時候,只是更改了沙盒裡面的欄位,在方法內部進行一層包裝,方法還是一樣的呼叫,只是儲存一個本地的切換選擇

那麼如何實現下載皮膚?
之前已經有幾套皮膚了,但是自己下載的話可以給標識,預設的幾套皮膚是放在之前的NSBundle包裡面的,根據路徑獲取,但是下載皮膚是放在沙盒裡面的,根據NSUserDefault判斷去哪裡獲取

##7.iOS的響應鏈 事件傳遞和響應過程分析
https://www.jianshu.com/p/2e074db792ba
注意:如果父控制元件不能接收觸控事件,那麼子控制元件就不能接收
首先肯定是UIResponder的子類才能接受和處理事件因為有 begin move end cancel等幾個方法

  1. 事件傳遞(查詢最合適的控制元件處理事件)
    產生觸控事件 → UIApplication事件佇列 → UIWindow的hitTest:withEvent:→ UIView的hitTest:withEvent: → 子View的hitTest:withEvent: → 子View的hitTest:withEvent:
    事件傳遞主要是查詢最合適響應事件的View
    1.1.首先判斷主視窗(keyWindow)自己是否能接受觸控事件
    1.2.觸控點是否在自己身上
    1.3.從後往前遍歷子控制元件,重複前面的兩個步驟(首先查詢陣列中最後一個元素)
    1.4.如果沒有符合條件的子控制元件,那麼就認為自己最合適處理
    注:在物件執行hitTest:withEvent:的過程中,如果物件自己的pointInside: withEvent:方法返回NO,就返回nil,否則開始查詢所有的子View,一旦沒有子View或者子View全部返回nil,就會把自己作為最合適View返回,UIWindow拿到最合適的View

  2. 事件分發(然後把具體事件傳遞)
    UIApplication sendEvent: → UIWindow sendEvent: → 最合適的view開始響應

  3. 事件響應(開始響應)
    根據事件型別呼叫對應方法,以touchBegan為例:
    最合適的view touchesBegan: withEvent: → 所在ViewController touchesBegan: withEvent:→ parentView touchesBegan: withEvent: → … → UIWindow touchesBegan: withEvent: → UIAplication touchesBegan: withEvent: → AplicationDelegate touchesBegan: withEvent: → 結束
    注:如果某個View或ViewController未呼叫super touchesBegan: withEvent:則響應結束 touchs事件呼叫super預設是向上一層層傳遞的過程,需要在對應層實現就重寫,結束傳遞就不要呼叫super,如果傳遞到application還是沒人處理,結束丟棄即可

##8.load和initialize的區別
load:
檔案被程式載入時也就是第一次呼叫的時候會呼叫,例如編譯器跑檔案進去的時候,優先是父類,子類
但是load載入時的環境不安全,我們應該儘量避免減少load方法的邏輯,load是執行緒安全的,內部有鎖,因此應該避免
執行緒阻塞在這裡,能做的就是在load方法的時候進行method_swizzing,其他就比搞了

initialize
這個方法是在第一次在給某個類傳送訊息是呼叫(比如例項化一個物件),只會呼叫一次

1.兩者都是在例項化物件之前呼叫,以main為分割,前者main函式之前呼叫,後者在之後呼叫,都是自動的

2.load用來method_swizzle方法,initialize用於初始化變數和靜態變數

3.都是執行緒安全的,內部都加了鎖,因此操作的時候儘可能簡單,避免阻塞

##9.KVC和KVO
KVC
key value coding 鍵值編碼 不通過getter或者setter方法進行訪問,而是通過屬性字串間接訪問屬性的機制
1.首先查詢有無屬性,setter getter的,若有直接使用
2.若無直接訪問例項變數,若無,就直接丟擲異常undefinedkey進行異常丟擲,除非你重寫

擴充套件:可以根據這個思路搞一個容器接收查詢不到的屬性值,實現相容

KVO (KVO內部實現機制的簡單程式碼思路
觀察者模式,對屬性的setter方法進行觀察,期間會衍生出子類進行正真的通知,可以用來進行view和model的繫結

1.基本理解
KVO是基於Runtime機制實現的,當某個類的屬性物件第一次被觀察時,系統就會在執行期動態地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內實現真正的通知機制

2.深入理解
1.Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。當觀察物件A時,KVO機制動態建立一個新的名為: NSKVONotifying_A的新類,該類繼承自物件A的本類,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會負責在呼叫原 setter 方法之前和之後,通知所有觀察物件屬性值的更改情況。

2.NSKVONotifying_A類剖析:在這個過程,被觀察物件的 isa 指標從指向原來的A類,被KVO機制修改為指向系統新建立的子類 NSKVONotifying_A類,來實現當前類屬性值改變的監聽;

3.所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對KVO的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們建立一個新的名為“NSKVONotifying_A”的類(),就會發現系統執行到註冊KVO的那段程式碼時程式就崩潰,因為系統在註冊監聽的時候動態建立了名為NSKVONotifying_A的中間類,並指向這個中間類了。

4.(isa 指標的作用:每個物件都有isa 指標,指向該物件的類,它告訴 Runtime 系統這個物件的類是什麼。所以物件註冊為觀察者時,isa指標指向新子類,那麼這個被觀察的物件就神奇地變成新子類的物件(或例項)了。) 因而在該物件上對 setter 的呼叫就會呼叫已重寫的 setter,從而啟用鍵值通知機制。
下面的程式碼片段是首先給類建立一個Çategory,當我們例項化該類的時候,呼叫下這個方法,內部會用object_setClass重新把物件的isa指標指向新建立的子類,當外部呼叫setter方法修改屬性的時候,會重新跑到子類裡面進行setter的重寫,最終在子類裡面通知觀察者,由於這個是Category,觀察者只能通過runtime的objc_setAssociatedObject 來儲存觀察者,進行最後的通知訊息轉發

- (void)mkj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    // 修改呼叫者的isa指標為動態建立的子類,能讓set方法改變的時候,去子類進行操作
    object_setClass(self, [MKJKVONotifying_CalculateManager class]);

    // 由於observr沒有任何儲存,子類set方法改變之後如何呼叫observe實現的方法?runtime
    // 動態給物件關聯一個屬性
    objc_setAssociatedObject(self, "hehe", observer, OBJC_ASSOCIATION_RETAIN);
}

5.子類setter方法剖析:KVO的鍵值觀察通知依賴於 NSObject 的兩個方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數值的前後分別呼叫2個方法: 被觀察屬性發生改變之前,willChangeValueForKey:被呼叫,通知系統該 keyPath 的屬性值即將變更;當改變發生後, didChangeValueForKey: 被呼叫,通知系統該 keyPath 的屬性值已經變更;之後, observeValueForKey:ofObject:change:context: 也會被呼叫。且重寫觀察屬性的setter 方法這種繼承方式的注入是在執行時而不是編譯時實現的。

- (Class)class
{
    return [CalculateManager class];
}

- (void)setName:(NSString *)name
{
    // 呼叫父類
    [super setName:name];
    // 根據關聯欄位拿出關聯的觀察者
    id obj = objc_getAssociatedObject(self, "hehe");
    // 子類最終通知呼叫觀察者的observer方法
    [obj observeValueForKeyPath:@"name" ofObject:self change:@{@"change":name} context:nil];
}

這裡寫圖片描述

##10.如何用NSValue包裝C語言結構體

typedef struct mystruct{
    int count;
    float height;
}mystruct

如何將結構體轉為NSValue,並存入陣列
mystruct mst;
mst.count = 10;
mst.height = 100;
NSValue *value = [NSValue value:&mst withObjcType:@encode(mystruct)];

##11.自己寫一個NSString的實現

+ (id)initWithCString:(char *)nullTerminatedCString encoding:(NSStringEncoding)encoding;
+ (id) stringWithCString: (char*)nullTerminatedCString
encoding: (NSStringEncoding)encoding
{
NSString *obj;
obj = [self allocWithZone: NSDefaultMallocZone()];
obj = [obj initWithCString: nullTerminatedCString encoding: encoding];
return AUTORELEASE(obj);
}

##12.靜態庫和動態庫
靜態庫:.a和.framework
連結時完整地拷貝至可執行檔案中,被多次使用就有多份冗餘拷貝。
如果靜態函式庫改變了,那麼你的程式必須重新編譯

特點:模組化,分工合作,可重用

動態庫:.dylib和.framework
連結時不復制,程式執行時由系統動態載入到記憶體,供程式呼叫,系統只載入一次,多個程式共用,節省記憶體

特點:可以將最終可執行檔案體積縮小,多個檔案共享,節省資源,動態更改庫檔案更新程式

.a和framework檔案的區別
.a是二進位制檔案,不能直接拿來使用,需要配合標頭檔案資原始檔一起使用
因為打包靜態庫的時候只能打包程式碼,不能打包資原始檔和標頭檔案,因此用的時候需要.a檔案+需要暴露的標頭檔案+資原始檔

framework除了二進位制檔案之外還有資原始檔,可以直接拿來使用

##13.Base64和MD5
MD5是一種不可逆的訊息摘要演算法。為電腦保安領域廣泛使⽤的一種雜湊函式
效果:
把一個任意長度的位元組串變換成⼀定⻓度的⼗六進位制數字串。
目的是讓⼤容量資訊在⽤數字簽名軟體簽署私⼈金鑰前被"壓縮"成⼀種保密的格式。

應用:
1、一致性驗證:
從網上下載⽂件,軟體,各種資料的時候,有些檔案會提供MD5對照資訊。利⽤MD5校驗軟體來核對下載的⽂件,以此觀測下載得到的檔案與傳送者是否相符。
利用MD5演算法來進⾏檔案校驗的方案被大量應用到軟體下載站、論壇資料庫、系統檔案安全等⽅方⾯面

2、數字證照:
對一段Message(位元組串)產生fingerprint(指紋),以防止被"篡改”。舉個例子,你將一段話寫在一個叫readme.txt檔案中,並對這個readme.txt產生一個MD5的值並記錄在案,然後你可以傳播這個檔案給別人別人如果修改了檔案中的任何內容,你對這個檔案重新計算MD5時就會發現。如果再有一個第三方的認證機構,用MD5還可以防止檔案作者的"抵賴",這 就是所謂的數字簽名應用。
3、安全訪問認證:
用於作業系統的登陸認證上。當使用者登入的時候,系統把⽤戶輸入的密碼進⾏MD5 Hash運算,然後再去和儲存在檔案系統中的MD5值進⾏比較, 進⽽確定輸⼊的密碼是否正確。
通過這樣的步驟,系統在並不知道使用者密碼的明碼的情況下就可以確定使用者登入系統的合法性。這可以避免⽤戶的密碼被具有系統管理員許可權的使用者知道。


概念:
Base64是一種基於64個可列印字元來表示二進位制資料的表示方法。Base64是一種編碼方式。
Base64編碼本質上是一種將二進位制資料轉成文字資料的方案。對於非二進位制資料,是先將其轉換成二進位制形式,然後每連續6位元(2的6次方=64)計算其十進位制值,根據該值在上面的索引表中找到對應的字元,最終得到一個文字字串。

效果:
Base64編碼的思想是是採用64個基本的ASCII碼字元對資料進行重新編碼。
採用Base64編碼具有不可讀性,即所編碼的資料不會被人用肉眼所直接看到。
應用:
Base64主要用於將二進位制資料轉換為文字資料,方便使用HTTP協議等,是可逆的。處理文字資料的場合,表示、傳輸、儲存一些二進位制資料。包括MIME的email,email via MIME,在XML中儲存複雜資料.

注意:base64的主要作用不是加密,而是用來避免“位元組”中不能轉換成可顯示字元的數值。專案中有用到的地方是當你支付完成之後,返回的使用者資訊是進行BASE64加密之後的字串,你需要進行Base64解密,然後把轉換成的二進位制檔案再轉換成NSString

網路相關

##1.Socket相關
Socket實現的一個簡單群聊功能
概念:
1.網路上兩個程式通過雙向通訊連結實現資料交換,這個連結的其中一端就是socket
2.Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議
3.在Internet上的主機一般執行了多個服務軟體,同時提供幾種服務。每種服務都開啟一個Socket,並繫結到一個埠上,不同的埠對應於不同的服務。Socket正如其英文原意那樣,像一個多孔插座。一臺主機猶如佈滿各種插座的房間,每個插座有一個編號,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟體將插頭插到不同編號的插座,就可以得到不同的服務

通訊要素:
1.網路上的請求就是通過Socket來建立連結然後相互通訊
2.IP地址(網路上主機裝置的唯一標示,尋找主機)
3.埠號(定位程式)

  • 標示程式的邏輯地址,不同程式的標示,80就是預設訪問Web應用程式,而不是訪問資料庫
  • 有效埠:0-65535,0-1024是系統的有效保留埠

例如我有一個伺服器(百度的 192.168.10.11)裡面有個
Web的應用程式:80(提供HTTP服務)
MySQL:3306/SQLServer資料庫應用程式

###概括下
TCP 是傳輸控制協議,面向連線,傳輸資料流。是TCP/IP協議的傳輸層協議。

socket本質是(API),對TCP/IP的封裝,可供程式設計師做網路通訊開發是所呼叫。

http 是基於TCP面向網際網路的請求響應模型的一種協議。HTTP協議:—> 網路上資料傳輸的約束,伺服器才知道提交的資料,和XMPP一樣,只是應用層的資料傳輸協議,規定了以何種資料進行傳輸

##2.TCP和UDP
4.傳輸協議(用什麼方式進行互動)
TCP,UDP —>互動方式 前者需要建立連結,後者不需要

概念:
TCP(傳輸控制協議,HTTP的互動方式就是TCP互動方式,需要建立連線)

  • 建立連線,形成資料傳輸通道
  • 在連結中進行大資料傳輸,資料不受限制
  • 通過三次握手完成連結,是可靠協議,安全送達協議
  • 必須建立連線,效率很稍微低

UDP (使用者資料包協議,無連線,不可靠的網路協議,用於多播,廣播,例如上課同步直播)

  • 將資料以及源(我的電腦IP)和目的(別人電腦的IP)封裝成資料包中,不需要建立連線
  • 每個資料包的大小限制在64k之內,為什麼小一點呢,例如你20分鐘的大小發一次,那出錯了,你這20分支都看不到了,
    如果你一秒鐘發一次,你錯過了,下一秒就能繼續接上,所以大小有限制
  • 無需連線,因此是不可靠協議
  • 不需要建立連線,速度快


    1、TCP是面向連結的,雖然說網路的不安全不穩定特性決定了多少次握手都不能保證連線的可靠性,但TCP的三次握手在最低限度上(實際上也很大程度上保證了)保證了連線的可靠性;

而UDP不是面向連線的,UDP傳送資料前並不與對方建立連線,對接收到的資料也不傳送確認訊號,傳送端不知道資料是否會正確接收,當然也不用重發,所以說UDP是無連線的、不可靠的一種資料傳輸協議。

2、也正由於1所說的特點,使得UDP的開銷更小資料傳輸速率更高,因為不必進行收發資料的確認,所以UDP的實時性更好。

知道了TCP和UDP的區別,就不難理解為何採用TCP傳輸協議的MSN比採用UDP的QQ傳輸檔案慢了,但並不能說QQ的通訊是不安全的,

因為程式設計師可以手動對UDP的資料收發進行驗證,比如傳送方對每個資料包進行編號然後由接收方進行驗證啊什麼的,

即使是這樣,UDP因為在底層協議的封裝上沒有采用類似TCP的“三次握手”而實現了TCP所無法達到的傳輸效率。

##3.HTTP
1.HTTP構建於TCP/IP協議之上,預設埠80
2.HTTP是無連線無狀態
 HTTP連線最顯著的特點是客戶端傳送的每次請求都需要伺服器回送響應,在請求結束後,會主動釋放連線。從建立連線到關閉連線的過程稱為“一次連線”。
 

HTTP協議是以ASCII碼傳輸
method request-URL version狀態行
headers 請求頭
entity-body 請求體

200 OK 客戶端請求成功
301 Moved Permanently 請求永久重定向
302 Moved Temporarily 請求臨時重定向
304 Not Modified 檔案未修改,可以直接使用快取的檔案。
400 Bad Request 由於客戶端請求有語法錯誤,不能被伺服器所理解。
401 Unauthorized 請求未經授權。這個狀態程式碼必須和WWW-Authenticate報頭域一起使用
403 Forbidden 伺服器收到請求,但是拒絕提供服務。伺服器通常會在響應正文中給出不提供服務的原因
404 Not Found 請求的資源不存在,例如,輸入了錯誤的URL
500 Internal Server Error 伺服器發生不可預期的錯誤,導致無法完成客戶端的請求。
503 Service Unavailable 伺服器當前不能夠處理客戶端的請求,在一段時間之後,伺服器可能會恢復正常。

HTTP協議採用的是請求-應答的模式,當使用普通模式,非Keep-Alive模式,每個請求和應答客戶端和伺服器都要
重新建立一個連線,完成之後立即斷開,HTTP1.1版本中,Keep-Alive功能使客戶端到服務端的連結持續有效,當出現伺服器後續請求時,Keep-Alive功能避免了建立或者重新建立連結

HTTP Keep-Alive

  • 保持當前TCP連結,避免重新建立連結
  • HTTP長連線不可能一直保持 例如Keep-Alive timeout=5 max=100,表示TCP通道保持5秒,最多100次請求
  • 一個無狀態的協議,每次請求都是獨立的,Keep-alive也沒能改變這個
  • 長連結之後如何知道本次傳輸結束呢?1.判斷資料是否達到了content-length指示的大小

HTTP Pipelining HTTP管線化 預設情況下HTTP協議每次傳輸過程中只能承載一個HTTP請求和響應 - 例如你傳送多個請求 請求1-響應1,請求2-響應2,請求3-響應3,請求4-響應4,管線化是讓HTTP請求整批提交的技術。 變成了請求1,請求2,請求3.。。響應1,響應2.。。
  • 管線化機制通過持久連結完成,僅HTTP 1.1支援此技術 1.0不支援
  • 只有GET和HEAD請求可以進行管線化,POST有所限制
  • 不會影響詳情回來的順序
  • HTTP /1.1要求伺服器支援管線化,但並不要求服務端也對響應進行管線化處理

##4.會話跟蹤
1.什麼是會話?
客戶端開啟與伺服器的連結發出請求到伺服器響應客戶端請求的全過程稱之為會話 session

2.什麼是會話跟蹤?
對同一個使用者對伺服器的連續請求和接受響應的監視

3.為什麼需要會話跟蹤??
瀏覽器和伺服器之間的通訊是通過HTTP協議進行通訊的,無狀態的,不能儲存使用者資訊,響應一次完成之後就斷開了
下一次請求需要重新連線,這樣就需要判斷是否是同一個使用者,才會有會話跟蹤

方法1:URL重寫
URL結尾新增一個附加資料以表示該會話,會把會話ID通過URL資訊傳遞過去,來識別使用者

方法2:Cookie 可以被禁止
Cookie是Web伺服器傳送給客戶端的一小段資訊,客戶端請求時可以讀取該資訊傳送到伺服器,進而進行使用者的識別
對於客戶端的每次請求,伺服器都會把Cookie傳送到客戶端,在客戶端進行儲存,以便下次使用
客戶端有兩種形式儲存Cookie

  • 記憶體中 瀏覽器關閉就沒了
  • 另一種儲存在客戶機的磁碟上,永久Cookie

方法3:Session
每個使用者都有一個不同的session,各個使用者之間不能共享,每個使用者獨享,在session中存放資訊
在伺服器建立一個session物件,產生一個sessionID來表示這個session物件,然後將sessionID放到Cookie中傳送到客戶端,下一次訪問時,sessionID會傳送到伺服器,在伺服器進行識別不同的使用者
Session依賴於Cookie,Cookie被禁止,那麼Session也失效了

##4.1 Session和Cookie
Cookie和Session
Session是在服務端儲存的一個資料結構,用來跟蹤使用者的狀態,這個資料可以儲存在叢集、資料庫、檔案中;
Cookie是客戶端儲存使用者資訊的一種機制,用來記錄使用者的一些資訊,也是實現Session的一種方式
session和cookie的目的相同,都是為了克服http協議無狀態的缺陷,但完成的方法不同。session通過cookie,在客戶端儲存session id,而將使用者的其他會話訊息儲存在服務端的session物件中,與此相對的,cookie需要將所有資訊都儲存在客戶端。

##5.這幾個之間的關係

1.TCP連線與HTTP連線

在網路分層中,HTTP協議是基於TCP協議的,客戶端向服務端傳送一個HTTP請求時,需要先與服務端建立TCP連線,也就是經典的三次握手(通常對使用者來說是很難察覺的),握手成功以後才能進行資料互動。HTTP是基於請求響應模式且無狀態的協議,1.1之前只支援短連線,也就是請求響應一次以後連線中斷,下次請求需要重新進行TCP連線,而1.1之後支援持長連線,即進行一次TCP連線以後,客戶端可以傳送多次的HTTP請求給伺服器端。

小結:HTTP基於TCP

2.TCP連線與Socket連線

Socket是應用層與傳輸層之間的同一個抽象層,它是一套介面,所以Socket連線可以基於TCP連線,也有可能基於UDP。我們知道,TCP協議是可靠的,UDP協議是不可靠的,那麼基於TCP協議的Socket連線同樣是可靠的;基於UDP協議的Socket連線是不可靠的,QQ就是基於主要UDP輔助TCP實現的
登陸採用TCP協議和HTTP協議,你和好友之間傳送訊息,主要採用UDP協議,內網傳檔案採用了P2P技術。總來的說:
1.登陸過程,客戶端client 採用TCP協議向伺服器server傳送資訊,HTTP協議下載資訊。登陸之後,會有一個TCP連線來保持線上狀態。
2.和好友發訊息,客戶端client採用UDP協議,但是需要通過伺服器轉發。騰訊為了確保傳輸訊息的可靠,採用上層協議來保證可靠傳輸。如果訊息傳送失敗,客戶端會提示訊息傳送失敗,並可重新傳送。
3.如果是在內網裡面的兩個客戶端傳檔案,QQ採用的是P2P技術,不需要伺服器中轉
之所以會發生在客戶端明明看到“訊息傳送失敗”但對方又收到了這個訊息的情況,就是因為客戶端發出的訊息伺服器已經收到並轉發成功,但客戶端由於網路原因沒有收到伺服器的應答包引起的。

更恰當的方式應該是:兩種通訊協議同時使用,各有側重。UDP用於保持大量終端的線上與控制,應用與業務則通過TCP去實現。
早期的時候,QQ還是主要使用TCP協議,而後來就轉向了採用UDP的方式來保持線上,TCP的方式來上傳和下載資料。

小結:Socket可基於TCP,亦可UDP

3.HTTP連線與Socket連線

HTTP和TCP UDP一樣,其實都是基於Socket的,Socket只是作業系統提供給網路應用程式之間進行通訊的抽象API,他不決定是否促成長連線還是短連線,Socket確實是HTTP連線的一部分,而且是基於TCP的上層協議,但他確實短連線,1.1之後,有keep-alive屬性進行輔助維持短暫的長連線,複用TCP,避免短時間內的多次三次握手和四次揮手

4.什麼是TCP連線的三次握手

第一次握手:客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;

第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;

第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。

握手過程中傳送的包裡不包含資料,三次握手完畢後,客戶端與伺服器才正式開始傳送資料。

理想狀態下,TCP連線一旦建立,在通訊雙方中的任何一方主動關閉連線之前,TCP 連線都將被一直保持下去。

斷開連線時伺服器和客戶端均可以主動發起斷開TCP連線的請求,斷開過程需要經過“四次握手”(過程就不細寫了,就是伺服器和客戶端互動,最終確定斷開)

5.利用Socket建立網路連線的步驟

建立Socket連線至少需要一對套接字,其中一個執行於客戶端,稱為ClientSocket ,另一個執行於伺服器端,稱為ServerSocket 。

套接字之間的連線過程分為三個步驟:伺服器監聽,客戶端請求,連線確認。

1、伺服器監聽:伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態,等待客戶端的連線請求。

2、客戶端請求:指客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。

為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端套接字提出連線請求。

3、連線確認:當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求時,就響應客戶端套接字的請求,建立一個新的執行緒,把伺服器端套接字的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式建立連線。

而伺服器端套接字繼續處於監聽狀態,繼續接收其他客戶端套接字的連線請求。

6.為什麼建立連線協議是三次握手,而關閉連線卻是四次握手呢?

這是因為服務端的LISTEN狀態下的SOCKET當收到SYN報文的連線請求後,它可以把ACK和SYN(ACK起應答作用,而SYN起同步作用)放在一個報文裡來傳送。但關閉連線時,當收到對方的FIN報文通知時,它僅僅表示對方沒有資料傳送給你了;但未必你所有的資料都全部傳送給對方了,所以你可能未必會馬上會關閉SOCKET,也即你可能還需要傳送一些資料給對方之後,再傳送FIN報文給對方來表示你同意現在可以關閉連線了,所以它這裡的ACK報文和FIN報文多數情況下都是分開傳送的。

##6.HTTP發出網路請求全部詳細過程描述
從這裡摘錄
###1.首先輸入或者app發出請求
###2.瀏覽器根據鬱悶解析IP地址DNS查詢過程:
1)瀏覽器快取:瀏覽器會快取DNS記錄一段時間。 但作業系統沒有告訴瀏覽器儲存DNS記錄的時間,這樣不同瀏覽器會儲存個自固定的一個時間(2分鐘到30分鐘不等)。
2)系統快取:如果在瀏覽器快取裡沒有找到需要的域名,瀏覽器會做一個系統呼叫(windows裡是gethostbyname),這樣便可獲得系統快取中的記錄。
3)路由器快取:如果系統快取也沒找到需要的域名,則會向路由器傳送查詢請求,它一般會有自己的DNS快取。
4)ISP DNS快取:如果依然沒找到需要的域名,則最後要查的就是ISP快取DNS的伺服器。在這裡一般都能找到相應的快取記錄。
###3.客戶端和服務端通過三次握手建立TCP連線
###4.客戶端給服務端傳送一個http請求
###5.伺服器永久重定向響應
伺服器給瀏覽器響應一個301永久重定向響應,這樣瀏覽器就會訪問“http://www.facebook.com/” 而非“http://facebook.com/”。為什麼伺服器一定要重定向而不是直接傳送使用者想看的網頁內容呢?其中一個原因跟搜尋引擎排名有關。如果一個頁面有兩個地址,就像http://www.igoro.com/和http://igoro.com/,搜尋引擎會認為它們是兩個網站,結果造成每個搜尋連結都減少從而降低排名。而搜尋引擎知道301永久重定向是什麼意思,這樣就會把訪問帶www的和不帶www的地址歸到同一個網站排名下。還有就是用不同的地址會造成快取友好性變差,當一個頁面有好幾個名字時,它可能會在快取裡出現好幾次。
###6.瀏覽器跟蹤重定向
現在瀏覽器知道了 “HTTP://www.facebook.com/”才是要訪問的正確地址,所以它會傳送另一個http請求。

###7.伺服器處理請求
伺服器接收到獲取請求,然後處理並返回一個響應。這表面上看起來是一個順向的任務,但其實這中間發生了很多有意思的東西,就像作者部落格這樣簡單的網站,何況像facebook那樣訪問量大的網站呢!web伺服器軟體(像IIS和阿帕奇)接收到HTTP請求,然後確定執行某一請求處理來處理它。請求處理就是一個能夠讀懂請求並且能生成HTML來進行響應的程式(像ASP.NET,PHP,RUBY…)

8.伺服器返回一個HTML響應

9.釋放TCP連線

若connection 模式為close,則伺服器主動關閉TCP 連線,客戶端被動關閉連線,釋放TCP 連線;若connection 模式為keepalive,則該連線會保持一段時間,在該時間內可以繼續接收請求;

10.客戶端瀏覽器解析HTML內容

11.瀏覽器獲取嵌入在HTML中的物件

##7.為什麼要啟用HTTPS,為什麼說他安全
###1)首先為什麼說HTTP是不安全的 對稱加密 AES
http協議屬於明文傳輸協議,互動過程以及資料傳輸都沒有進行加密,通訊雙方也沒有進行任何認證,通訊過程非常容易遭遇劫持、監聽、篡改,嚴重情況下,會造成惡意的流量劫持等問題,甚至造成個人隱私洩露(比如銀行卡卡號和密碼洩露)等嚴重的安全問題。

可以把http通訊比喻成寄送信件一樣,A給B寄信,信件在寄送過程中,會經過很多的郵遞員之手,他們可以拆開信讀取裡面的內容(因為http是明文傳輸的)。A的信件裡面的任何內容(包括各類賬號和密碼)都會被輕易竊取。除此之外,郵遞員們還可以偽造或者修改信件的內容,導致B接收到的信件內容是假的。
一般是採用一種叫做 AES 的演算法來解決的。這種演算法需要一個 金鑰 key 來加密整個資訊,加密和解密所需要使用的 key 是一樣的,所以這種加密一般也被稱為“對稱加密”。AES 在數學上保證了,只要你使用的 key 足夠足夠足夠足夠的長,破解是幾乎不可能的。
我們再回到這個教室,你接著要傳小紙條,你把地址寫上後,把要傳輸的內容用 AES 蹭蹭蹭加密了起來。剛準備傳,問題來了。AES 不是有一個 key 嗎?key 怎麼給目的地啊?如果我把金鑰直接寫在紙條上,那麼中間的人不依然可以解密嗎?在現實中你可以通過一些其它方法來把金鑰安全傳輸給目的地而不被其他人看見,但是在網際網路上,要想這麼做難度就很大了,畢竟傳輸終究要經過這些路由,所以要做加密,還得找一個更復雜的數學方法。

2)非對稱加密RSA為例

現在利用這種非對稱加密的方法,我們來設想一個場景。你繼續想要傳紙條,但是傳紙條之前你先準備把接下來通訊的對稱加密金鑰給傳輸過去。於是你用 RSA 技術生成了一對 k1、k2,你把 k1 用明文傳送了出去,路經有人或許會擷取,但是沒有用,k1 加密的資料需要用 k2 才能解密。而此時,k2 在你自己的手裡。k1 送達目的地後,目的地的人會去準備一個接下來用於對稱加密傳輸的金鑰 key,然後用收到的 k1 把 key 加密了,把加密好的資料傳回來。路上的人就算擷取到了,也解密不出 key。等到了你自己手上,你用手上的 k2 把用 k1 加密的 key 解出來,現在全教室就只有你和你的目的地擁有 key,你們就可以用 AES 演算法進行對稱加密的傳輸啦!這時候你和目的地的通訊將無法再被任何人竊聽!

3)https如何保證安全?

要解決http帶來的問題,就要引入加密以及身份驗證機制。
我們引入了非對稱加解密的概念。在非對稱加解密演算法裡,公鑰加密的資料,有且只有唯一的私鑰才能夠解密
基本的互動,沒有涉及到中間人
當建立SSL連結的時候,伺服器把公鑰用明文的形式傳遞給客戶端,客戶端和服務端可以用公鑰私鑰進行對稱加密用的金鑰進行資料的解密,從而保證安全性,但是還是有中間人攻擊,具體的邏輯可以參考下面的文章
詳細介紹

當然,這時候你可能會問兩個問題。

既然 非對稱加密 可以那麼安全,為什麼我們不直接用它來加密資訊,而是去加密 對稱加密 的金鑰呢?

這是因為 非對稱加密 的密碼對生成和加密的消耗時間比較長,為了節省雙方的計算時間,通常只用它來交換金鑰,而非直接用來傳輸資料。然後再用非對稱加密解出來的密碼進行對稱加密的解密
因此加入了CA證照來保證一定是指定伺服器的互動
我們引入了數字證照的概念。伺服器首先生成公私鑰,將公鑰提供給相關機構(CA),CA將公鑰放入數字證照並將數字證照頒佈給伺服器,此時伺服器就不是簡單的把公鑰給客戶端,而是給客戶端一個數字證照,數字證照中加入了一些數字簽名的機制,保證了數字證照一定是伺服器給客戶端的。中間人傳送的偽造證照,不能夠獲得CA的認證,此時,客戶端和伺服器就知道通訊被劫持了。
非對稱加密演算法(公鑰和私鑰)交換對稱金鑰+數字證照驗證身份(驗證公鑰是否是偽造的)+利用對稱金鑰加解密後續傳輸的資料=安全

基本優化相關

兩幀率之間沒有算完,就出現了跳幀,因此需要進行效能優化
1.網路載入資料的時候提前計算好,在model裡面多一個欄位用來儲存高度,cell載入的時候直接獲取

2.[UIImage imageName:]; 通過一個單例類來根據url儲存UIImage,當你第一次取的時候木有,直接IO獲取,有的話直接從記憶體獲取

3.文字的繪製會有一定的效能消耗,牛B的話直接用core text來做

4.iOS檢視效能優化

  • Color Blended Layers 當先展示到眼前的顏色都是有多層Layer決定而成,因此少用Alpha,減少GPU不必要的計
    算,只要計算最上層就好了,會忽略被壓在下面的layer 開啟開關是紅色的,必須優化
  • Color Misaligned Images 繪製的點無法直接對映到頻幕上的畫素點 系統會進行反鋸齒計算,增加了圖形的負擔,簡單來說就是ImageSize和ImageVIew Size不同就會觸發反鋸齒計算,增加效能損耗,開啟開關是黃色的 必須優化,本地圖片沒問題,但是網路圖片的話最好請求的時候就帶上引數,或者請求回來進行壓縮

5.效能測試之非同步切圓角 通過模擬器檢視UI DEBUG下的blended layer 是否出現紅色,是的話就要優化,UILabel除外,非同步繪製圓角

6.程式碼效能測試,通過單元測試裡面的textPerform來測試時間 instruments的timePtofile和lead來測試

7.對於一些處理完之後沒什麼用的類可以用AutoreleasePool來進行及時處理

8.cell行高的快取

9.不進行UI互動的控制元件能用CAlayer就用CAlayer

10.物件銷燬在後臺執行
NSArray*tem = self.array;
self.array = nil;
dispatch_async(queue,^{
[tem class];
捕獲到物件在後臺銷燬
});

11.圖片儘量別有透明的,不然增加GPU計算的損耗

12.不要動態建立子檢視,要預先建立好,如果不需要顯示就先hidden

13.所有的cell子檢視都必須指定背景顏色,顏色別用alpha

14.非同步繪製
self.layer.drawAsynchronously = YES;

15.柵格化,cell所有內容,生成一張獨立的影象,螢幕滾動式只顯示影象
必須指定解析度 預設使用 x1生成影象
self.layer.shouldRasterize = YES;
self.layer.rasterzationScale = [UIScreen mainScreen].scale
這裡和非同步coretext繪製一個道理,最終繪製上去的控制元件用Image的方式繪製上cell展示

如果需要將當前檢視的子檢視柵格化,也就是將它的全部子檢視與當前檢視壓縮成一個圖層

16.重用大開銷物件
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar
可以新增到Category裡面或者用單例/靜態變數來實現 空間換時間

其實設定NSDateFormatter的速度差不多是和建立新的一樣慢,因此,懶載入就可以

// in your .h or inside a class extension  
@property (nonatomic, strong) NSDateFormatter *formatter;  
// inside the implementation (.m)  
// When you need, just use self.formatter  
- (NSDateFormatter *)formatter {  
    if(!_formatter) {  
        _formatter = [[NSDateFormatter alloc] init];  
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format  
    }  
    return _formatter;  
}
設定一個NSDateFormatter

17.AutoreleasePool
如果迴圈很多次,你會發現你的記憶體一直在被吃掉,這個時候你可以在每次迴圈的時候用
@autorelease進行臨時建立的釋放

18.選擇性快取圖片
第一種imageNamed 會快取圖片
第二種imageWithContentOfFile 僅載入圖片

如果是大圖片,一次性的,第二種就很優秀,如果反覆載入的小圖,需要快取,那麼久用第一種

19.讀取圖片的二進位制檔案的時候系統預設會先解壓縮,因此如果非常卡的話需要強制解壓縮在非同步執行緒中,不然會卡主主執行緒
SDWebDecoder已經非同步做了解壓縮

20.非同步繪製以及按需載入 按需載入cell,cell滾動很快時,只載入範圍內的cell
按需載入優化tableView

21.使用區域性更新(reloadSection進行區域性更新)

22.不要實現無用的代理方法,tableView只遵守兩個協議

23.不要做過多的繪製工作 在實現drawRect:的時候,它的rect引數就是需要繪製的區域,這個區域之外的不需要進行繪製。
例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判斷是否需要繪製image和text,然後再呼叫繪製方法。

24.預渲染影象 當新的影象出現時,仍然會有短暫的停頓現象。解決的辦法就是在bitmap context裡先將其畫一遍,匯出成UIImage物件,然後再繪製到螢幕;
25.多執行緒訪問者鎖的使用會影響效能,自旋鎖雖然消耗CPU忙等,但是效能優於互斥鎖

26.排版 繪製 UI操作,前兩個全部放在非同步執行緒操作,等Runloop監聽到休眠時再統一繪製操作在主執行緒
ASDK原理

27.非同步排版和渲染,Runtime週期重新整理到主執行緒

27.inline函式,inline

28.用FaceBook底層的yoga來操作frame,不用Autolayout

11.main()之前的過程有哪些?

sunny
這裡寫圖片描述
1、main之前的載入過程

1)dyld 開始將程式二進位制檔案初始化 (dyld(the dynamic link editor),Apple 的動態連結器,系統 kernel 做好啟動程式的初始準備後,交給 dyld 負責)

2)交由ImageLoader 讀取 image,其中包含了我們的類,方法等各種符號(Class、Protocol 、Selector、 IMP)

3)由於runtime 向dyld 繫結了回撥,當image載入到記憶體後,dyld會通知runtime進行處理

4)runtime 接手後呼叫map_images做解析和處理

5)接下來load_images 中呼叫call_load_methods方法,遍歷所有載入進來的Class,按繼承層次依次呼叫Class的+load和其他Category的+load方法

6)至此,可執行檔案中和動態庫所有的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功載入到記憶體中,被 runtime 所管理,再這之後,runtime 的那些方法(動態新增 Class、swizzle 等等才能生效)

7)最後dyld呼叫真正的main函式

注意:dyld會快取上一次把資訊載入記憶體的快取,所以第二次比第一次啟動快一點

1.整個事件由 dyld 主導,完成執行環境的初始化後,配合 ImageLoader 將二進位制檔案按格式載入到記憶體, 動態連結依賴庫載入進記憶體,並由 runtime 負責載入成 objc 定義的結構,安層級呼叫Class的load方法,所有初始化工作結束後,dyld 呼叫真正的 main 函式。

12.設計模式

建立型:單利(單態)和 抽象工廠(Class方法 方便繼承維護程式碼,父類暴露介面,各自子類的實現,子類各自形態表現不同),迭代器模式(for in)
結構型:MVC,MVVM,MVP,Category(裝飾模式),門面設計模式(KVO,封裝)
行為型:觀察者(KVO,Notification),Delegate(代理模式)

工廠設計模式的簡單示例


#import <Foundation/Foundation.h>

@interface Animation : NSObject

- (void)startAnimate;

- (void)run;

@end

#import "Animation.h"

@implementation Animation


- (void)startAnimate{
    NSLog(@"父類%s",__func__);
    [self run];
}


- (void)run{
    NSLog(@"父類%s",__func__);
}

@end
#import "Animation.h"

@interface Dog : Animation

@end
#import "Dog.h"

@implementation Dog

- (void)run{
    NSLog(@"子類狗%s",__func__);
}

@end

13.並行和併發

參考1
參考2
“併發”指的是程式的結構,“並行”指的是程式執行時的狀態
併發是能力,並行是狀態
並行指物理上同時執行,併發指能夠讓多個任務在邏輯上交織執行的程式設計(cpu時間片輪轉優先順序排程)

14.instanceType和id的區別

instanceType最重要的作用是那些非關聯返回型別的方法返回所在類的型別。
(1)id在編譯的時候不能判斷物件的真實型別
instancetype在編譯的時候可以判斷物件的真實型別
(2)id可以用來定義變數, 可以作為返回值, 可以作為形參
instancetype只能用於作為返回值

15.FOUNDATION_EXTERN和FOUNDATION_EXPORT的區別

其實都是全域性變數,相對於都是Static,只能在本檔案使用,而前者就是能在所有檔案使用。
只是FOUNDATION_EXPORT是用來相容C++的
如果沒有C++ 就可以用 FOUNDATION_EXTERN
如果你用extern也沒問題 所以相容性好的話用 FOUNDATION_EXPORT即可

16.Block和Delegate對比和用法場景

Block C語言的閉包,帶有區域性變數的匿名函式,Block整體程式碼表現形式就是一個結構體,結構體內部可以捕獲外部變數的值,如果加了__block修飾(變數就是物件,無論什麼型別),也可以捕獲外部變數整個結構體指標,而Block任務塊就是內部C函式的實現,而Block結構體內部就會引用這個匿名函式,呼叫實際就是呼叫結構體內的函式指標。Block有全域性,堆和棧三種,ARC下基本都是幫我們賦值到堆上儲存,這也就是為什麼能捕獲變數,引用變數,Block存在讓變數無法釋放的原因,Block就是物件

Block和Delegate都可以通知外面。
Block更輕,使用更簡單,能夠直接訪問上下文,呼叫和宣告都在同一個地方,程式碼連貫;Delegate更重一些,需要實現介面,它的好處就是方法分離開來,沒有Block連貫。

Block是後面才有的,個人感覺應該優先使用Block。以下兩種情況可以用Delegate
1.有多個相關方法,比如一個網路請求,只有成功和失敗,詳情參見AFN上層封裝,但是底層網路NSURLSession,會有很多情況回撥,比如失敗,成功,異常,進度,回撥處理一系列切點方法,就應該優先採用Delegate

2.為了避免迴圈引用,也可以使用Delegate,使用Block的時候稍微不注意就會引起迴圈引用,導致物件釋放不了,而delegate是分離開的,並不會引用上下文,因此更加安全
如果自己設計一個庫,安全和方便使用,寧可麻煩點,使用Delegate更加友好一點。

將 block 簡單分類,有三種情形。

  • 臨時性的,只用在棧當中,不會儲存起來。比如陣列的 foreach 遍歷,這個遍歷用到的 block 是臨時的,不會儲存起來。
  • 需要儲存起來,但只會呼叫一次,或者有一個完成時期。比如一個 UIView 的動畫,動畫完成之後,需要使用 block 通知外面,一旦呼叫 block 之後,這個 block 就可以刪掉。
  • 需要儲存起來,可能會呼叫多次。比如按鈕的點選事件,假如採用 block 實現,這種 block 就需要長期儲存,並且會呼叫多次。呼叫之後,block 也不可以刪除,可能還有下一次按鈕的點選。對於臨時性的,只在棧中使用的 block, 沒有迴圈引用問題,block 會自動釋放。而只呼叫一次的 block,需要看內部的實現,正確的實現應該是 block 呼叫之後,馬上賦值為空,這樣 block 也會釋放,同樣不會迴圈引用。而多次呼叫時,block 需要長期儲存,就很容易出現迴圈引用問題。Cocoa 中的 API 設計也是這樣的,臨時性的,只會呼叫一次的,採用 block。而多次呼叫的,並不會使用 block。比如按鈕事件,就使用 target-action。有些庫將按鈕事件從 target-action 封裝成 block 介面, 反而容易出問題。

17.AutoreleasePool

相關文章