前10題面試題見iOS進階面試題(一)
11.用過 Swift 嗎?如何評價 String index 的設(shè)計?
解答:Q1略過
Q2:如何評價 String index 的設(shè)計 ?
字符串的索引:
可擴展的字符群集可以組成一個或者多個 Unicode 標量。這意味著不同的字符以及相同字符的不同表示方式可能需要不同數(shù)量的內(nèi)存空間來存儲。所以 Swift 中的字符在一個字符串中并不一定占用相同的內(nèi)存空間數(shù)量。因此在沒有獲取字符串的可擴展的字符群的范圍時候,就不能計算出字符串的字符數(shù)量。如果您正在處理一個長字符串,需要注意characters屬性必須遍歷全部的 Unicode 標量,來確定字符串的字符數(shù)量。
另外需要注意的是通過characters屬性返回的字符數(shù)量并不總是與包含相同字符的NSString的length屬性相同。NSString的length屬性是利用 UTF-16 表示的十六位代碼單元數(shù)字,而不是 Unicode 可擴展的字符群集。
前面提到,不同的字符可能會占用不同數(shù)量的內(nèi)存空間,所以要知道Character的確定位置,就必須從String開頭遍歷每一個 Unicode 標量直到結(jié)尾。因此,Swift 的字符串不能用整數(shù)(integer)做索引。
使用startIndex屬性可以獲取一個String的第一個Character的索引。使用endIndex屬性可以獲取最后一個Character的后一個位置的索引。因此,endIndex屬性不能作為一個字符串的有效下標。如果String是空串,startIndex和endIndex是相等的。
通過調(diào)用 String 的 index(before:) 或 index(after:) 方法,可以立即得到前面或后面的一個索引。您還可以通過調(diào)用 index(_:offsetBy:) 方法來獲取對應偏移量的索引,這種方式可以避免多次調(diào)用 index(before:) 或 index(after:) 方法。
let stringIndexStr = "hello word!"
let starIndexC = stringIndexStr[stringIndexStr.startIndex]//獲取字符串第一個索引的 字符
print("startIndexValue:\(stringIndexStr.startIndex)")//打印 startIndexValue:Index(_base: Swift.String.UnicodeScalarView.Index(_position: 0), _countUTF16: 1)
print("starIndexCharacter:\(starIndexC)")//打印 starIndexCharacter:h
//endIndex屬性不能作為一個字符串的有效下標,運行時會崩潰
//let endIndexC = stringIndexStr[stringIndexStr.endIndex]//試圖獲取越界索引對應的 Character,將引發(fā)一個運行時錯誤。
//print("endIndexCharacter:\(endIndexC)")
12.假設(shè) iPhone 上有一個與服務(wù)器的 TCP 連接,此時 iPhone 忽然斷網(wǎng),服務(wù)器能在短時間內(nèi)知會 iPhone 的離線嗎?
解答
- 一般來說不能
中斷連接可以是客戶端,也可以是服務(wù)端。
假設(shè)由客戶端中斷連接,即客戶端發(fā)起FIN報文中斷連接請求,告訴服務(wù)器“我沒有數(shù)據(jù)發(fā)給你了”。這時,客戶端進入FIN_WAIT_1狀態(tài)。服務(wù)器收到FIN報文后,如果還有數(shù)據(jù)沒有發(fā)送完成,則不會急著關(guān)閉SOCKET,會繼續(xù)發(fā)送數(shù)據(jù)。這時,服務(wù)器會發(fā)送ACK告訴客戶端“我收到你的斷開請求了,但是我還沒有準備好,請等我消息”。這時,客戶端就進入FIN_WAIT_2狀態(tài),繼續(xù)等待服務(wù)器的FIN報文。當服務(wù)器確認數(shù)據(jù)已經(jīng)發(fā)送完成,則向客戶端發(fā)送FIN報文,告訴客戶端“好了,我這邊的數(shù)據(jù)發(fā)完了,準備斷開連接”。客戶端收到FIN報文后就知道斷開連接,但是它不相信網(wǎng)絡(luò),怕服務(wù)器不知道斷開,所以發(fā)送ACK后進入TIME_WAIT狀態(tài)。如果服務(wù)器沒有收到ACK報文,則客戶端會繼續(xù)重傳ACK,服務(wù)器收到ACK報文后就知道可以斷開連接了。客戶端等待2MSL后依然沒有收到回復,則證明服務(wù)器已正常關(guān)閉。那好,我客戶端也可以關(guān)閉連接了。OK,TCP連接就這樣關(guān)閉了。在服務(wù)器的TCP連接沒有完全斷開之前不允許重新監(jiān)聽,某些情況下可能是不合理的。
【例】
服務(wù)器需要處理大量的客戶端的連接,每個連接的生存時間可能很短,但是每秒都有很多客戶端來請求。這個時候,如果由服務(wù)器主動關(guān)閉連接,比如某些客戶端不活躍,就需要被服務(wù)器主動清理掉,這時會產(chǎn)生大量的TIME_WAIT連接。由于我們的請求量很大,就可能導致TIME_WAIT的連接數(shù)很多而導致服務(wù)器的端口不夠用,無法處理新的連接。
3.如何解決TIME_WAIT狀態(tài)引起的bind失敗?
答:使用setsockopt()設(shè)置socket描述符的選項SO_REUSEADDR為1,表示允許創(chuàng)建端口號相同,但是IP地址不同的多個socket描述符。即,在server代碼的socket函數(shù)和bind函數(shù)調(diào)用之間插入如下代碼:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
最后, 我們來簡要說說另外一種網(wǎng)絡(luò)結(jié)構(gòu), 假設(shè)把pc1和pc2直接用網(wǎng)線相連, 建立起世界最小局域網(wǎng), 并形成tcp連接。如果在客戶端和服務(wù)端都沒有心跳機制,那么實驗結(jié)果如下
1. 如果斷掉其中的網(wǎng)線, 客戶端和服務(wù)端都沒有感知。
2. 客戶端突然斷電, 則服務(wù)端沒有感知。
3.服務(wù)端突然斷電, 則客戶端沒有感知。
13.為什么 Wireshark 不能直接抓取 SSL 的原始數(shù)據(jù)?
- 必要的加密解密基礎(chǔ)知識
1)對稱加密算法:就是加密和解密使用同一個密鑰的加密算法。因為加密方和解密方使用的密鑰相同,所以稱為稱為對稱加密,也稱為單鑰加密方法。
優(yōu)點是:加密和解密運算速度快,所以對稱加密算法通常在消息發(fā)送方需要加密大量數(shù)據(jù)時使用;
缺點是:安全性差,如果一方的密鑰遭泄露,那么整個通信就會被破解。另外加密之前雙方需要同步密鑰;
常用對稱加密算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK、AES等;
2)非對稱加密算法:而非對稱加密算法需要兩個密鑰來進行加密和解密,這兩個秘鑰是公開密鑰(public key,簡稱公鑰)和私有密鑰(private key,簡稱私鑰)。
公鑰和私鑰是一對:公鑰用來加密,私鑰解密,而且公鑰是公開的,私鑰是自己保存的,不需要像對稱加密那樣在通信之前要先同步秘鑰。
有點是:安全性更好,私鑰是自己保存的,不需要像對稱加密那樣在通信之前要先同步秘鑰。
缺點是:加密和解密花費時間長、速度慢,只適合對少量數(shù)據(jù)進行加密。
常用的非對稱加密算法有:RSA、Elgamal、Rabin、D-H、ECC等;
3)HASH算法:也稱為消息摘要算法。將任意長度的二進制值映射為較短的固定長度的二進制值,該二進制值稱為哈希值。
常用于檢驗數(shù)據(jù)的完整性,檢驗數(shù)據(jù)沒有被篡改過。常見的又 MD5(MD系列),SHA-1(SHA系列)
HTTPS 使用到了上面全部三種加密算法。
- HTTPS 的作用
HTTPS簡單而言,即使建立在SSL/TLS協(xié)議之上的HTTP。不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文傳播,帶來了三大風險。
(1) 竊聽風險(eavesdropping):第三方可以獲知通信內(nèi)容。
(2) 篡改風險(tampering):第三方可以修改通信內(nèi)容。
(3) 冒充風險(pretending):第三方可以冒充他人身份參與通信。
SSL/TLS協(xié)議是為了解決這三大風險而設(shè)計的,希望達到:
(1) 所有信息都是加密傳播,第三方無法竊聽。
(2) 具有校驗機制,一旦被篡改,通信雙方會立刻發(fā)現(xiàn)。
(3) 配備身份證書,防止身份被冒充。
互聯(lián)網(wǎng)是開放環(huán)境,通信雙方都是未知身份,這為協(xié)議的設(shè)計帶來了很大的難度。而且,協(xié)議還必須能夠經(jīng)受所有匪夷所思的攻擊,這使得SSL/TLS協(xié)議變得異常復雜。
4. 基本的運行過程
HTTPS 的基本運行過程:
1)利用對稱加密算法來加密網(wǎng)頁內(nèi)容,那么如何保證對稱加密算法的秘鑰的安全呢?
2)使用非對稱加密算法來獲得對稱加密算法的秘鑰,從而保證了對稱加密算法的秘鑰的安全,也就保證了對稱加密算法的安全。
這里這樣安排使用的原理是,利用了對稱加密算法和非對稱加密算法優(yōu)點,而避免了它們的缺點。利用了對稱加密算法速度快,而非對稱加密算法安全的優(yōu)點;同時巧妙的避免了對稱加密算法的不安全性,以及需要同步密鑰的缺點,也避免了非對稱加密算法的速度慢的缺點。實在是巧妙了。
這里有兩個問題:
(1)如何保證非對稱加密算法公鑰不被篡改?
解決方法:將公鑰放在數(shù)字證書中。只要證書是可信的,公鑰就是可信的。
(2)公鑰加密計算量太大,如何減少耗用的時間?
解決方法:每一次對話(session),客戶端和服務(wù)器端都生成一個"對話密鑰"(session key),用它來加密信息。由于"對話密鑰"是對稱加密算法,所以運算速度非常快,而服務(wù)器公鑰只用于加密"對話密鑰"本身,這樣就減少了加密運算的消耗時間。(也就是網(wǎng)頁內(nèi)容的加密使用的是對稱加密算法)
因此,SSL/TLS協(xié)議的基本過程是這樣的:
(1) 客戶端向服務(wù)器端索要并驗證非對稱加密算法的公鑰。
(2) 雙方協(xié)商生成對稱加密算法的"對話密鑰"。
(3) 雙方采用對稱加密算法和它的"對話密鑰"進行加密通信。
上面過程的前兩步,又稱為"握手階段"(handshake)。
5. 握手階段的詳細過程
"握手階段"涉及四次通信,我們一個個來看。需要注意的是,"握手階段"的所有通信都是明文的。
1) 客戶端發(fā)出請求(ClientHello)
首先,客戶端(通常是瀏覽器)先向服務(wù)器發(fā)出加密通信的請求,這被叫做ClientHello請求。
在這一步,客戶端主要向服務(wù)器提供以下信息。
(1) 瀏覽器支持的SSL/TLS協(xié)議版本,比如TLS 1.0版。
(2) 一個瀏覽器客戶端生成的隨機數(shù),稍后用于生成對稱加密算法的"對話密鑰"。
(3) 瀏覽器支持的各種加密方法,對稱的,非對稱的,HASH算法。比如RSA非對稱加密算法,DES對稱加密算法,SHA-1 hash算法。
(4) 瀏覽器支持的壓縮方法。
這里需要注意的是,客戶端發(fā)送的信息之中不包括服務(wù)器的域名。也就是說,理論上服務(wù)器只能包含一個網(wǎng)站,否則會分不清應該向客戶端提供哪一個網(wǎng)站的數(shù)字證書。這就是為什么通常一臺服務(wù)器只能有一張數(shù)字證書的原因。
對于虛擬主機的用戶來說,這當然很不方便。2006年,TLS協(xié)議加入了一個Server Name Indication擴展,允許客戶端向服務(wù)器提供它所請求的域名。
2) 服務(wù)器回應(SeverHello)
服務(wù)器收到客戶端請求后,向客戶端發(fā)出回應,這叫做SeverHello。服務(wù)器的回應包含以下內(nèi)容。
(1) 確認使用的加密通信協(xié)議版本,比如TLS 1.0版本。如果瀏覽器與服務(wù)器支持的版本不一致,服務(wù)器關(guān)閉加密通信。
(2) 一個服務(wù)器生成的隨機數(shù),稍后用于生成對稱加密算法的"對話密鑰"。
(3) 確認使用的各種加密方法,比如確認算法使用:RSA非對稱加密算法,DES對稱加密算法,SHA-1 hash算法
(4) 服務(wù)器證書。
除了上面這些信息,如果服務(wù)器需要確認客戶端的身份,就會再包含一項請求,要求客戶端提供"客戶端證書"。比如,金融機構(gòu)往往只允許認證客戶連入自己的網(wǎng)絡(luò),就會向正式客戶提供USB密鑰,里面就包含了一張客戶端證書。
3) 客戶端回應
客戶端收到服務(wù)器回應以后,首先驗證服務(wù)器證書。如果證書不是可信機構(gòu)頒布、或者證書中的域名與實際域名不一致、或者證書已經(jīng)過期,就會向訪問者顯示一個警告,由其選擇是否還要繼續(xù)通信。
如果證書沒有問題,客戶端就會從證書中取出服務(wù)器的非對稱加密算法的公鑰。然后,向服務(wù)器發(fā)送下面三項信息。
(1) 一個隨機數(shù)。該隨機數(shù)用服務(wù)器發(fā)來的公鑰進行的使用非對稱加密算法加密,防止被竊聽。
(2) 編碼改變通知,表示隨后的信息都將用雙方商定的加密方法和密鑰發(fā)送(比如確認使用:RSA非對稱,DES對稱,SHA-1 hash算法)。
(3) 客戶端握手結(jié)束通知,表示客戶端的握手階段已經(jīng)結(jié)束。這一項同時也是前面發(fā)送的所有內(nèi)容的hash值,用來供服務(wù)器校驗。
上面第一項的隨機數(shù),是整個握手階段出現(xiàn)的第三個隨機數(shù),又稱"pre-master key"。有了它以后,客戶端和服務(wù)器就同時有了三個隨機數(shù),接著雙方就用事先商定的對稱加密算法,各自生成本次會話所用的同一把"會話密鑰"。也就是說瀏覽器和服務(wù)器各自使用同一個對稱加密算法,對三個相同的隨機數(shù)進行加密,獲得了,用來加密網(wǎng)頁內(nèi)容的 對稱加密算法的秘鑰。(注意:這里瀏覽器的三個隨機數(shù)都是明文的,但是服務(wù)端獲得的"pre-master key"是密文的,所以服務(wù)器需要使用非對稱加密算法的私鑰,來先解密獲得"pre-master key"的明文,在來生成對稱加密算法的秘鑰。這樣的目的是為了防止:"pre-master key"被竊聽,因為發(fā)送明文會被竊聽,但是發(fā)生的是非對稱加密算法的加密過后的密文,因為竊聽者不知道私鑰,所以即使竊聽了,也無法解密出其對應的明文。從而保證了最后生成的:用于加密網(wǎng)頁內(nèi)容的對稱加密算法的秘鑰的安全性!!!)
至于為什么一定要用三個隨機數(shù),來生成"會話密鑰"?
"不管是客戶端還是服務(wù)器,都需要隨機數(shù),這樣生成的密鑰才不會每次都一樣。由于SSL協(xié)議中證書是靜態(tài)的,因此十分有必要引入一種隨機因素來保證協(xié)商出來的密鑰的隨機性。
對于RSA密鑰交換算法來說,pre-master-key本身就是一個隨機數(shù),再加上hello消息中的隨機,三個隨機數(shù)通過一個密鑰導出器最終導出一個對稱密鑰。
pre master的存在在于SSL協(xié)議不信任每個主機都能產(chǎn)生完全隨機的隨機數(shù),如果隨機數(shù)不隨機,那么pre master secret就有可能被猜出來,那么僅適用pre master secret作為密鑰就不合適了,因此必須引入新的隨機因素,那么客戶端和服務(wù)器加上pre master secret三個隨機數(shù)一同生成的密鑰就不容易被猜出了,一個偽隨機可能完全不隨機,可是是三個偽隨機就十分接近隨機了,每增加一個自由度,隨機性增加的 可不是一。"
這里:其實如果 pre-master-key 和 瀏覽器生成的隨機數(shù)都可能被猜出來,那么最后生成的對稱加密算法的秘鑰就是不安全的。因為三個隨機數(shù)都可能被竊聽到了。
此外,如果前一步,服務(wù)器要求客戶端證書,客戶端會在這一步發(fā)送證書及相關(guān)信息。
4) 服務(wù)器的最后回應
服務(wù)器收到客戶端的第三個隨機數(shù)pre-master key之后,計算生成本次會話所用的對稱加密算法的"會話密鑰"。然后,向客戶端最后發(fā)送下面信息。
(1)編碼改變通知,表示隨后的信息都將用雙方商定的對稱加密算法和密鑰進行加密。
(2)服務(wù)器握手結(jié)束通知,表示服務(wù)器的握手階段已經(jīng)結(jié)束。這一項同時也是前面發(fā)送的所有內(nèi)容的hash值,用來供客戶端校驗。
至此,整個握手階段全部結(jié)束。接下來,客戶端與服務(wù)器進入加密通信,就完全是使用普通的HTTP協(xié)議,只不過用"會話密鑰"加密內(nèi)容。
總結(jié):
1)HTTPS 結(jié)合使用了 非對稱加密算法,對稱加密算法,hash算法,分別利用他們的優(yōu)勢,避免他們的缺點。利用非對稱加密算法獲得對稱加密算法的秘鑰,保證他的安全性;然后實際的網(wǎng)頁內(nèi)容的加密使用的是對稱加密算法,利用了對稱加密算法速度快的優(yōu)勢,hash算法主要是防止篡改的發(fā)生,是一種校驗機制,最后數(shù)字證書,保證了服務(wù)器在將非對稱加密算法的公鑰傳給瀏覽器時的安全性(不會被中間人篡改),同時也標志了服務(wù)器的身份。
2)HTTPS的四大金剛:
非對稱加密算法(對稱加密算法的秘鑰) + 對稱加密算法(加密內(nèi)容) + 數(shù)字證書(防止篡改非對稱加密算法的公鑰) + HASH算法(防止篡改消息)== HTTPS
3)HTTPS的本質(zhì)是什么?
HTTPS的本質(zhì)就是在HTTP連接發(fā)起之前,先使用SSL/TLS協(xié)議,協(xié)調(diào)客戶端和服務(wù)端,在兩端各自生產(chǎn)一個對稱加密算法的秘鑰,
然后使用普通的HTTP協(xié)議傳輸 經(jīng)過對稱加密算法加密的網(wǎng)頁內(nèi)容。因為對稱加密算法的秘鑰是安全的,所以對稱加密算法加密的網(wǎng)頁內(nèi)容也是安全的。
14.backtrace 是在用戶態(tài)實現(xiàn)的嗎?能否講講實現(xiàn)它的大致思路?
顯示函數(shù)調(diào)用關(guān)系(backtrace/callstack)是調(diào)試器必備的功能之一,比如在gdb里,用bt命令就可以查看backtrace。在程序崩潰的時候,函數(shù)調(diào)用關(guān)系有助于快速定位問題的根源,了解它的實現(xiàn)原理,可以擴充自己的知識面,在沒有調(diào)試器的情況下,也能實現(xiàn)自己backtrace。更重要的是,分析backtrace的實現(xiàn)原理很有意思。現(xiàn)在我們一起來研究一下:
glibc提供了一個backtrace函數(shù),這個函數(shù)可以幫助我們獲取當前函數(shù)的backtrace,先看看它的使用方法,后面我們再仿照它寫一個。
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#define MAX_LEVEL 4
static void test2()
{
int i = 0;
void* buffer[MAX_LEVEL] = {0};
int size = backtrace(buffer, MAX_LEVEL);
for(i = 0; i < size; i++)
{
printf("called by %p/n", buffer[i]);
}
return;
}
static void test1()
{
int a=0x11111111;
int b=0x11111112;
test2();
a = b;
return;
}
static void test()
{
int a=0x10000000;
int b=0x10000002;
test1();
a = b;
return;
}
int main(int argc, char* argv[])
{
test();
return 0;
}
編譯運行它:
gcc -g -Wall bt_std.c -o bt_std
./bt_std
屏幕打印:
called by 0×8048440
called by 0×804848a
called by 0×80484ab
called by 0×80484c9
上面打印的是調(diào)用者的地址,對程序員來說不太直觀,glibc還提供了另外一個函數(shù)backtrace_symbols,它可以把這些地址轉(zhuǎn)換成源代碼的位置(通常是函數(shù)名)。不過這個函數(shù)并不怎么好用,特別是在沒有調(diào)試信息的情況下,幾乎得不什么有用的信息。這里我們使用另外一個工具addr2line來實現(xiàn)地址到源代碼位置的轉(zhuǎn)換:
運行:
./bt_std |awk ‘{print “addr2line “$3″ -e bt_std”}’>t.sh;. t.sh;rm -f t.sh
屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:12
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:28
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:39
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:48
backtrace是如何實現(xiàn)的呢? 在x86的機器上,函數(shù)調(diào)用時,棧中數(shù)據(jù)的結(jié)構(gòu)如下:
參數(shù)N
參數(shù)… 函數(shù)參數(shù)入棧的順序與具體的調(diào)用方式有關(guān)
參數(shù) 3
參數(shù) 2
參數(shù) 1
EIP 完成本次調(diào)用后,下一條指令的地址
EBP 保存調(diào)用者的EBP,然后EBP指向此時的棧頂。
----------------新的EBP指向這里---------------
臨時變量1
臨時變量2
臨時變量3
臨時變量…
臨時變量5
(說明:下面低是地址,上面是高地址,棧向下增長的)
調(diào)用時,先把被調(diào)函數(shù)的參數(shù)壓入棧中,C語言的壓棧方式是:先壓入最后一個參數(shù),再壓入倒數(shù)第二參數(shù),按此順序入棧,最后才壓入第一個參數(shù)。
然后壓入EIP和EBP,此時EIP指向完成本次調(diào)用后下一條指令的地址 ,這個地址可以近似的認為是函數(shù)調(diào)用者的地址。EBP是調(diào)用者和被調(diào)函數(shù)之間的分界線,分界線之上是調(diào)用者的臨時變量、被調(diào)函數(shù)的參數(shù)、函數(shù)返回地址(EIP),和上一層函數(shù)的EBP,分界線之下是被調(diào)函數(shù)的臨時變量。
最后進入被調(diào)函數(shù),并為它分配臨時變量的空間。gcc不同版本的處理是不一樣的,對于老版本的gcc(如gcc3.4),第一個臨時變量放在最高的地址,第二個其次,依次順序分布。而對于新版本的gcc(如gcc4.3),臨時變量的位置是反的,即最后一個臨時變量在最高的地址,倒數(shù)第二個其次,依次順序分布。
為了實現(xiàn)backtrace,我們需要:
1.獲取當前函數(shù)的EBP。
2.通過EBP獲得調(diào)用者的EIP。
3.通過EBP獲得上一級的EBP。
4.重復這個過程,直到結(jié)束。
通過嵌入?yún)R編代碼,我們可以獲得當前函數(shù)的EBP,不過這里我們不用匯編,而且通過臨時變量的地址來獲得當前函數(shù)的EBP。我們知道,對于gcc3.4生成的代碼,當前函數(shù)的第一個臨時變量的下一個位置就是EBP。而對于gcc4.3生成的代碼,當前函數(shù)的最后一個臨時變量的下一個位置就是EBP。
有了這些背景知識,我們來實現(xiàn)自己的backtrace:
#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/
int backtrace(void** buffer, int size)
{
int n = 0xfefefefe;
int* p = &n;
int i = 0;
int ebp = p[1 + OFFSET];
int eip = p[2 + OFFSET];
for(i = 0; i < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[0];
eip = p[1];
}
return size;
}
對于老版本的gcc,OFFSET定義為0,此時p+1就是EBP,而p[1]就是上一級的EBP,p[2]是調(diào)用者的EIP。本函數(shù)總共有5個int的臨時變量,所以對于新版本gcc, OFFSET定義為5,此時p+5就是EBP。在一個循環(huán)中,重復取上一層的EBP和EIP,最終得到所有調(diào)用者的EIP,從而實現(xiàn)了backtrace。
現(xiàn)在我們用完整的程序來測試一下(bt.c):
寫個簡單的Makefile:
#include <stdio.h>
#define MAX_LEVEL 4
#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/
int backtrace(void** buffer, int size)
{
int n = 0xfefefefe;
int* p = &n;
int i = 0;
int ebp = p[1 + OFFSET];
int eip = p[2 + OFFSET];
for(i = 0; i < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[0];
eip = p[1];
}
return size;
}
static void test2()
{
int i = 0;
void* buffer[MAX_LEVEL] = {0};
backtrace(buffer, MAX_LEVEL);
for(i = 0; i < MAX_LEVEL; i++)
{
printf("called by %p/n", buffer[i]);
}
return;
}
static void test1()
{
int a=0x11111111;
int b=0x11111112;
test2();
a = b;
return;
}
static void test()
{
int a=0x10000000;
int b=0x10000002;
test1();
a = b;
return;
}
int main(int argc, char* argv[])
{
test();
return 0;
}
CFLAGS=-g -Wall
all:
gcc34 $(CFLAGS) bt.c -o bt34
gcc $(CFLAGS) -DNEW_GCC bt.c -o bt
gcc $(CFLAGS) bt_std.c -o bt_std
clean:
rm -f bt bt34 bt_std
編譯然后運行:
make
./bt|awk ‘{print “addr2line “$3″ -e bt”}’>t.sh;. t.sh;
屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:37
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:51
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:62
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:71
對于可執(zhí)行文件,這種方法工作正常。對于共享庫,addr2line無法根據(jù)這個地址找到對應的源代碼位置了。原因是:addr2line只能通過地址偏移量來查找,而打印出的地址是絕對地址。由于共享庫加載到內(nèi)存的位置是不確定的,為了計算地址偏移量,我們還需要進程maps文件的幫助:
通過進程的maps文件(/proc/進程號/maps),我們可以找到共享庫的加載位置,如:
…
00c5d000-00c5e000 r-xp 00000000 08:05 2129013 /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so
00c5e000-00c5f000 rw-p 00000000 08:05 2129013 /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so
…
libbt_so.so的代碼段加載到0×00c5d000-0×00c5e000,而backtrace打印出的地址是:
called by 0xc5d4eb
called by 0xc5d535
called by 0xc5d556
called by 0×80484ca
這里可以用打印出的地址減去加載的地址來計算偏移量。如,用 0xc5d4eb減去加載地址0×00c5d000,得到偏移量0×4eb,然后把0×4eb傳給addr2line:
addr2line 0×4eb -f -s -e ./libbt_so.so
15.malloc 的指針 double free 產(chǎn)生的異常與訪問 freed 指針有可能產(chǎn)生的異常有什么區(qū)別?為什么訪問 freed 指針不一定產(chǎn)生異常?
Double Free其實就是同一個指針free兩次。雖然一般把它叫做double free。其實只要是free一個指向堆內(nèi)存的指針都有可能產(chǎn)生可以利用的漏洞。
double free的原理其實和堆溢出的原理差不多,都是通過unlink這個雙向鏈表刪除的宏來利用的。只是double free需要由自己來偽造整個chunk并且欺騙操作系統(tǒng)。實現(xiàn)對漏洞的有效利用,攻擊者利用成功可導致權(quán)限提升。
一、內(nèi)存問題歸類
1、野指針。 指針對象指向了無效的地址,這個地址被其它對象持有了,已經(jīng)屬于其它對象; 或者還有一種可能, 這塊內(nèi)存已經(jīng)被 系統(tǒng)回收了。 總而言之, 指針指向的內(nèi)存塊不能用了,要么是被別人用了,要么是被系統(tǒng)回收了
2、Method cache corrupted. This may be a message to an invalid object, or a memory error somewhere else. 這其實也是內(nèi)存問題的一種。 這個對象你明明申請了空間,現(xiàn)在卻說你這個對象是個無效的對象,這說明你這個對象在創(chuàng)建的時候,占用的內(nèi)存塊有問題,在內(nèi)存上發(fā)生了沖突。 舉例: 你調(diào)用一個系統(tǒng)api,將一個結(jié)構(gòu)體里的buffer數(shù)據(jù),轉(zhuǎn)為另一個結(jié)構(gòu)體。 你在初始化目標結(jié)構(gòu)體的時候,給結(jié)構(gòu)體里的data申請了1024個字節(jié),可是系統(tǒng)api中,有參數(shù)指定了要拷貝4000個字節(jié)的源結(jié)構(gòu)體的data到目標結(jié)構(gòu)體。 這個時候, 目標結(jié)構(gòu)體就會多去占用2976個字節(jié)的空間,可是你卻只申請了1024個字節(jié),這樣,當你程序在運行的時候,創(chuàng)建對象的時候,如果又去指向那多占的2976個內(nèi)存空間, 就會出現(xiàn)對象無效。
3、 incorrect checksum for freed object - object was probably modified after being freed. 對象被釋放后,又進行了修改。
4、重復釋放。 一個對象的內(nèi)存已經(jīng)被釋放了一次,又再一次去釋放。 總而言之;都是內(nèi)存管理不當。
16.RunLoop 是一個不停歇在運行的死循環(huán)嗎?為什么?
1.RunLoop的基本作用
1.保持程序的持續(xù)運行
2.處理app中的各種事件(比如觸摸事件、定時器事件、selector事件
3.節(jié)省CPU資源,提高程序性能,有事情就做事情,沒事情就休息
2.RunLoop與線程
1.關(guān)系:一個Runloop對應著一條唯一的線程
2.創(chuàng)建:主線程Runloop已經(jīng)創(chuàng)建好了,子線程的runloop需要手動創(chuàng)建 創(chuàng)建方式:
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
子線程創(chuàng)建的runloop還需要手動的開啟:
[currentRunloop run];
3.生命周期:Runloop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀
3.RunLoop運行模式對NSTimer定時器的影響
1.首先創(chuàng)建一個定時器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
這里的task為一個普通的打印操作,這時候往控制器的view里拖一個Text View,
2.把定時器對象添加到runloop中,并且制定runloop的運行模式為默認:只有當runloop的運行模式為NSDefaultRunLoopMode的時候定時器才工作,也就是說這時候如果滑動Text View,定時器就不工作了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
3.如果想在滾動Text View的時候,定時器也工作,可以:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
但是如果這樣做的話,當我們停止?jié)L動的時候定時器又不工作了
4.有時候我們需要在默認情況下以及在滾動的時候都讓定時器工作,這時候我們就可以:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
由此可見:NSRunLoopCommonModes = NSDefaultRunLoopMode & UITrackingRunLoopMode
拓展:
①scheduledTimerWithTimeInterval方法:創(chuàng)建定時器并默認添加到當前線程的Runloop中指定默認運行模式
②timerWithTimeInterval:創(chuàng)建定時器,如果該定時器要工作還需要添加到runloop中并指定相應的運行模式
通過以上代碼我們不難看出,NSTimer的定時器是受運行模式影響的,,而開發(fā)中我們有時候徹底去除這種影響,很顯然,NSTimer定時器不能做到這點,這時,我們可以使用GCD的定時器。
5.GCD定時器
GCD定時器是不受運行模式的影響的,因此,以后盡量使用此定時器,,該定時器的具體參數(shù)如下所示:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//01 創(chuàng)建定時器對象
/*
第一個參數(shù):是一個宏,表示要創(chuàng)建的是一個定時器
第二個參數(shù)和第三個參數(shù):默認總是傳0 描述信息
第四個參數(shù):隊列 決定代碼塊(dispatch_source_set_event_handler)在哪個線程中調(diào)用(主隊列-主線程)
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
//02 設(shè)置定時器
/*
第一個參數(shù):定時器對象
第二個參數(shù):定時器開始計時的時間(開始時間)
第三個參數(shù):設(shè)置間隔時間 GCD的時間單位:納秒
第四個參數(shù):精準度
*/
//這句代碼是設(shè)置開始兩秒之后再調(diào)用
dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);
dispatch_source_set_timer(timer, t, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//03 GCD定時器時間到了之后要執(zhí)行的任務(wù)
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD----%@",[NSThread currentThread]);
});
//04 默認是在掛起狀態(tài),需要手動恢復執(zhí)行
dispatch_resume(timer);
//如果沒有一個強指針指向,一創(chuàng)建就回被釋放。
self.timer = timer;
}
6. CFRunLoopModeRef
- a.基本說明:
CFRunloopModeRef代表著Runloop的運行模式
一個Runloop中可以有多個mode,一個mode里面又可以有多個source\observer\timer等等
每次runloop啟動的時候,只能指定一個mode,這個mode被稱為該Runloop的當前mode
如果需要切換mode,只能先退出當前Runloop,再重新指定一個mode進入
這樣做主要是為了分割不同組的定時器等,讓他們相互之間不受影響 - b.Model的分類:
kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
7.CFRunloopSourceRef
分類(根據(jù)函數(shù)調(diào)用棧來區(qū)分):
1.Source0:非基于Port的 :凡是用戶主動觸發(fā)事件都是Source0事件
2.Source1:基于Port的:凡是系統(tǒng)事件都是Source1事件
8. CFRunLoopObserverRef
作用:監(jiān)聽運行循環(huán)的狀態(tài)
9. selecter事件與RunLoop之間的關(guān)系
默認情況下,selecter事件是被添加到當前的runloop中執(zhí)行的,并且指定了運行模式為默認,由此可見,performSelecter事件是受運行模式的影響的,仔細查看以下代碼,看看有什么問題:
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
-(void)task{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
NSLog(@"+++++");
}
一個顯而易見的問題就是我們在子線程中來設(shè)置顯示圖片,然而拋開這個問題不管,圖片依舊不會被設(shè)置上去,因為在task中缺少一個運行循環(huán),我們需要手動開啟一個子運行循環(huán)才可以。
繼續(xù)查看一下代碼,圖片會被設(shè)置到imageView上面嗎?
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
-(void)task{
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
NSLog(@"+++++");
}
答案是否定的,因為我們雖然開啟了子運行循環(huán),但是當我們開啟這個循環(huán)的時候,當前循環(huán)里既沒有source事件(包括timer事件),也沒有selecter事件,于是循環(huán)立刻就退出了。正確的書寫方式如下:
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
-(void)task{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
NSLog(@"+++++");
}
注意:runLoop是一個死循環(huán),因此++++是不會打印的
10.使用RunLoop實現(xiàn)常駐線程
眾所周知,我們手動開啟的子線程在執(zhí)行完任務(wù)之后就會銷毀,而有時候我們需要一個子線程在執(zhí)行完當前任務(wù)后,不要銷毀,等我們需要的時候再來執(zhí)行其它任務(wù),這就用到了常駐線程。
- 假設(shè)我們又這樣一個需求,但我們點擊按鈕1的時候會開啟一條子線程來執(zhí)行run1任務(wù),當我們點擊按鈕2的時候,再讓剛才的線程來執(zhí)行run2任務(wù),具體實現(xiàn)代碼如下:
- (IBAction)btn1:(id)sender {
//01 創(chuàng)建線程,執(zhí)行任務(wù)
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
//02 執(zhí)行任務(wù)
[thread start];
self.thread = thread;
}
為了不讓剛開啟的線程銷毀,我們需要給它添加一個運行循環(huán),保證它不釋放:
-(void)run1
{
NSLog(@"run1---%@",[NSThread currentThread]);
//001 獲得當前線程對應的runloop對象
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
//002 為runloop添加input soucre或者是timer souce或selecter事件(最好就是一個基于端口的事件,這樣就不會去執(zhí)行不必要的方法)
[currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//以下為其它保證程序運行的方案,不推薦使用。
//[self performSelector:@selector(run3) withObject:nil afterDelay:3.0];
//[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
//003 啟動runloop
//runUntilDate |run 內(nèi)部都指定了運行模式為默認
[currentRunloop run];
}
按鈕2:
- (IBAction)goOnBtnClick:(id)sender {
//讓之前創(chuàng)建的線程繼續(xù)執(zhí)行
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)run2
{
NSLog(@"run2---%@",[NSThread currentThread]);
}
11.RunLoop的自動釋放池
runloop的自動釋放池什么時候創(chuàng)建釋放?
-
當runloop進入的時候會創(chuàng)建一個自動釋放池。
2)當runloop退出的時候會把之前的自動釋放池銷毀。
3)當runloop即將進入休眠的時候會把之前的自動釋放池先銷毀,然后創(chuàng)建一個新的自動釋放池。
image.png
17.看過 runtime 的源碼嗎?源碼中常有的 fastpath、slowpath 是什么?
fastpath和slowpath的區(qū)別在于,fastpath要求zone的unmapped file page必須大于zone規(guī)定的min_unmapped_pages,slab reclaimable大于min_slab_pages,回收內(nèi)存的的頁數(shù)為2^order個頁數(shù)和是32個之間的最大值,而kswapd回收內(nèi)存需要回收到所有的zone都滿足free page大于high watermark值或者zone的high watermark值個頁面。同時fastpath不會進行回寫,也不會回收mapped的page。fastpath主要靠zone_reclaim來完成快速的內(nèi)存回收。kswap則主要從balance_pgdat()來完成
direct reclaim和kswapd的差別在于只能回收32個頁面,同時kswapd進行回寫頁面需要滿足特定條件(當前有很多的頁面需要等待writeback),而direct reclaim判斷當回收過程中掃描的總頁數(shù)大于48個時就會啟動flush線程來進行臟頁的回寫操作。
我們在調(diào)性能時候都會盡量的提高extra_free_kbytes的值來防止過多的出現(xiàn)direct reclaim,因為direct reclaim會進行臟頁的回寫,這里再IO性能不是很好的時候會造成系統(tǒng)嚴重卡頓。
18.runtime 中 SideTables(不是 SideTable)存在的意義是什么?
1.數(shù)據(jù)結(jié)構(gòu)分析(* SideTables、RefcountMap、weak_table_t*)
為了管理所有對象的引用計數(shù)和weak指針,蘋果創(chuàng)建了一個全局的SideTables,雖然名字后面有個"s"不過他其實是一個全局的Hash表,里面的內(nèi)容裝的都是SideTable結(jié)構(gòu)體而已。它使用對象的內(nèi)存地址當它的key。管理引用計數(shù)和weak指針就靠它了。
????因為對象引用計數(shù)相關(guān)操作應該是原子性的。不然如果多個線程同時去寫一個對象的引用計數(shù),那就會造成數(shù)據(jù)錯亂,失去了內(nèi)存管理的意義。同時又因為內(nèi)存中對象的數(shù)量是非常非常龐大的需要非常頻繁的操作SideTables,所以不能對整個Hash表加鎖。蘋果采用了分離鎖技術(shù)。
舉例分析
假設(shè)有80個學生需要咱們安排住宿,同時還要保證學生們的財產(chǎn)安全。應該怎么安排?
顯然不會給80個學生分別安排80間宿舍,然后給每個宿舍的大門上加一把鎖。那樣太浪費資源了鎖也挺貴的,太多的宿舍維護起來也很費力氣。
我們一般的做法是把80個學生分配到10間宿舍里,每個宿舍住8個人。假設(shè)宿舍號分別是101、102 、... 110。然后再給他們分配床位,01號床、02號床等。然后給每個宿舍配一把鎖來保護宿舍內(nèi)同學的財產(chǎn)安全。為什么不只給整個宿舍樓上一把鎖,每次有人進出的時候都把整個宿舍樓鎖上?顯然這樣會造成宿舍樓大門口阻塞。
OK假如現(xiàn)在有人要找102號宿舍的2號床的人聊天。這個人會怎么做?
1、找到宿舍樓(SideTables)的宿管,跟他說自己要找10202(內(nèi)存地址當做key)。
2、宿管帶著他SideTables[10202]找到了102宿舍SideTable,然后把102的門一鎖lock,在他訪問102期間不再允許其他訪客訪問102了。(這樣只是阻塞了102的8個兄弟的訪問,而不會影響整棟宿舍樓的訪問)
3、然后在宿舍里大喊一聲:"2號床的兄弟在哪里?"table.refcnts.find(02)你就可以找到2號床的兄弟了。
4、等這個訪客離開的時候會把房門的鎖打開unlock,這樣其他需要訪問102的人就可以繼續(xù)進來訪問了。
SideTables == 宿舍樓
SideTable == 宿舍
RefcountMap里存放著具體的床位
蘋果之所以需要創(chuàng)造SideTables的Hash沖突是為了把對象放到宿舍里管理,把鎖的粒度縮小到一個宿舍SideTable。RefcountMap的工作是在找到宿舍以后幫助大家找到正確的床位的兄弟。
2. SideTable
當我們通過SideTables[key]來得到SideTable的時候
1.一把自旋鎖。spinlock_t??slock;
自旋鎖比較適用于鎖使用者保持鎖時間比較短的情況。正是由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高于互斥鎖。信號量和讀寫信號量適合于保持時間較長的情況,它們會導致調(diào)用者睡眠,因此只能在進程上下文使用,而自旋鎖適合于保持時間非常短的情況,它可以在任何上下文使用。
它的作用是在操作引用技術(shù)的時候?qū)ideTable加鎖,避免數(shù)據(jù)錯誤。
2.引用計數(shù)器 RefcountMap??refcnts;
對象具體的引用計數(shù)數(shù)量是記錄在這里的。
????這里注意RefcountMap其實是個C++的Map。為什么Hash以后還需要個Map?其實蘋果采用的是分塊化的方法。
????舉個例子
????假設(shè)現(xiàn)在內(nèi)存中有16個對象。
0x0000、0x0001、...... 0x000e、0x000f
????咱們創(chuàng)建一個SideTables[8]來存放這16個對象,那么查找的時候發(fā)生Hash沖突的概率就是八分之一。
????假設(shè)SideTables[0x0000]和SideTables[0x0x000f]沖突,映射到相同的結(jié)果。
SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一個SideTable
- RefcountMap的工作是在找到宿舍以后幫助大家找到正確的床位的兄弟。
- 維護weak指針的結(jié)構(gòu)體 weak_table_t ??weak_table;
這里是一個兩層結(jié)構(gòu)。
第一層結(jié)構(gòu)體中包含兩個元素。
????第一個元素weak_entry_t *weak_entries;是一個數(shù)組,上面的RefcountMap是要通過find(key)來找到精確的元素的。weak_entries則是通過循環(huán)遍歷來找到對應的entry。
????(上面管理引用計數(shù)器蘋果使用的是Map,這里管理weak指針蘋果使用的是數(shù)組,有興趣的朋友可以思考一下為什么蘋果會分別采用這兩種不同的結(jié)構(gòu))
????第二個元素num_entries是用來維護保證數(shù)組始終有一個合適的size。比如數(shù)組中元素的數(shù)量超過3/4的時候?qū)?shù)組的大小乘以2。第二層weak_entry_t的結(jié)構(gòu)包含3個部分
1.referent:
被指對象的地址。前面循環(huán)遍歷查找的時候就是判斷目標地址是否和他相等。
2.referrers
可變數(shù)組,里面保存著所有指向這個對象的弱引用的地址。當這個對象被釋放的時候,referrers里的所有指針都會被設(shè)置成nil。
3.inline_referrers
只有4個元素的數(shù)組,默認情況下用它來存儲弱引用的指針。當大于4個的時候使用referrers來存儲指針。
19.為什么 ARC 環(huán)境下不允許我們調(diào)用 [super dealloc]?
1.dealloc 的使用
a. 什么情況下會調(diào)用呢?
當對象的引用計數(shù)為0,系統(tǒng)會自動調(diào)用dealloc方法,回收內(nèi)存。
//調(diào)用方法
-(void)dealloc{
// [super dealloc]; //ARC環(huán)境下不需要調(diào)用。因為系統(tǒng)會 自動調(diào)用該方法幫助釋放父類對象。
}
b.調(diào)用的順序
一般說調(diào)用的順序是,當子類的對象釋放完時,然后再釋放父類的所擁有的實例。這一點與調(diào)用初始化方法,正好相反。
2.dealloc 誤區(qū)
我們在開發(fā)過程中,用到dealloc,卻因不會意識得到對象的引用計數(shù)是不是為0,dealloc到底走了沒走,因而導致內(nèi)存暴增,還會遇到很多奇怪的問題。我們需要知道dealloc不被調(diào)用的幾種情況?
1.controller中使用了計時器 NSTimer 使用后沒有銷毀 導致循環(huán)引用
self.playerTimer = [NSTimerscheduledTimerWithTimeInterval:1target:selfselector:@selector(playProgressAction)userInfo:nilrepeats:YES];
使用后記得銷毀
[_playerTimerinvalidate]
_playerTimer =nil;
2.協(xié)議delegate 應該使用weak修飾,否則會引起循環(huán)引用 不能釋放內(nèi)存
@property (nonatomic,weak)id<checkDelegate>delegate;
3.使用到block的地方,block回調(diào)中不能直接使用self 否則可能引起循環(huán)引用。
__weaktypeof(self) weakSelf =self;
_audioStream.onCompletion=^(){
[weakSelf nextButtonAction];
};
- NSNotificationCenter 記得注銷
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"delectOrGoDownProject" object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"changerInfoItem" object:nil];
}
20.Objective-C 是如何保證系統(tǒng)升級后的 ABI 穩(wěn)定性的?
自由發(fā)揮