逆向Appstore应用(二)

准备工作

代码签名 (code signing) 对一个App来讲至关重要,是iOS系统安全的重要组成部分,决定了App的哪些功能是被授权或者禁止的。尽管各种证书、配置文件等让初学这头痛不已,但确实给用户带来了极大的安全保障。

文件及环境:

  • 一个苹果的认证部门 Apple Worldwide Developer Relations CA颁发的Certificate
  • 基于该Certificate生成的mobileprovision文件
  • 系统环境 OSX 10.11.5

证书及文件

Certificate

Certificate是如何生成的呢?

首先需要开发者生成一个CertificateSigningRequest.certSigningRequest文件,具体步骤为钥匙串访问⟶证书助理⟶从证书颁发机构请求证书,按照提示填写相关信息,保存到磁盘即可。

等到CertificateSigningRequest.certSigningRequest后,通过 openssl asn1parse -i -in CertificateSigningRequest.certSigningRequest查看如下:

这个文件包含:
1,申请者信息
2,申请者公钥,此信息是申请者使用的私钥对应的公钥
3,摘要算法(SHA)和公钥加密算法(RSA)

此文件包含了我的信息,使用了sha1摘要算法和RSA公钥加密算法。苹果的Meber Center在拿到certSigningRequest文件后,将信息记录下来,并签发出相关的证书(Certificate),car证书包含哪些信息呢?又是如何使用certSigningRequest文件中的公钥呢?我们用openssl来看一下证书的内容:

openssl x509 -inform der -in ios_distribution.cer -noout -text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
05:33:03:2c:57:2a:ad:6c
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority
Validity
Not Before: Jul 10 07:45:50 2014 GMT
Not After : Jul 9 07:45:50 2017 GMT
Subject:......
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public Key: (2048 bit)
Modulus (2048 bit):
Signature Algorithm: sha1WithRSAEncryption
a8:b8:b5:ea:74:e9:06:d0:52:90:21:10:10:f1:f6:ce:1f:e7:
73:08:4f:ca:02:f8:73:66:06:36:48:5b:46:7d:ac:be:bd:c4:
......
d5:46:0a:7b

Data域即为证书的实际内容,与Data域平级的Signature Algorithm实际就是苹果的CA(Certificate Authority)的公钥,Data域包含我的苹果账号信息,其中最为重要的是我的公钥(Subject Public Key Info),这个公钥与我本机的私钥是对应的。当我们双击安装完证书后,KeyChain会自动将这对密钥关联起来,所以在KeyChain中可以看到类似的效果:

后续在程序上真机的过程中,会使用这个私钥,对代码进行签名,而公钥会附带在mobileprovision文件中,打包进app。那么mobileprovision又从哪里来?有什么作用呢?

mobileprovision

首先来看一张图:

在Apple Developer Center通过之前生成的Certificate来生成mobileprovision配置文件,它将授权和沙盒联系了起来,可以用于让应用在你的开发设备上可以被运行和调试,也可以用于内部测试 (ad-hoc) 或者企业级应用的发布。配置文件并不是一个 plist 文件,它是一个根据密码讯息语法 (Cryptographic Message Syntax) 加密的文件(下文中会简称 CMS)。security 也可以解码这个 CMS 格式,那么我们就用 security 来看看一个 mobileprovision 文件内部是什么样子:

1
$ security cms -D -i example.mobileprovision

你会得到一个 XML 格式的 plist 文件内容输出,DeveloperCertificates 这项,这一项是一个列表,包含了可以为使用这个配置文件的应用签名的所有证书。如果你用了一个不在这个列表中的证书进行签名,无论这个证书是否有效,这个应用都无法运行。ProvisionedDevices,在这一项里包含了所有可以用于测试的设备列表。因为配置文件需要被苹果签名,所以每次你添加了新的设备进去就要重新下载新的配置文件。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>DeveloperCertificates</key>
<array>
<data>MIIF0DCCBLi......Oo7Clog==</data>
</array>
<key>ProvisionsAllDevices</key>
<true/>
<key>TeamIdentifier</key>
<array>
<string>C789GLWV85</string>
</array>
<key>Version</key>
<integer>1</integer>
</dict>
</plist>

证书(及其对应的私钥)和配置文件(mobileprovision)是签名和打包的两个必要文件,如果要重新签名一个App,就需要在这两个上面动手脚了。

首先来了解一个已经签名了的App包含的内容,$ codesign -vv -d Example.app 会列出一些有关 Example.app 的签名信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
Executable=/Users/pandora/Desktop/shell/gcdsample/Payload/GCDSample.app/GCDSample
Identifier=com.baidu.GCDSample
Format=app bundle with Mach-O universal (armv7 arm64)
CodeDirectory v=20200 size=851 flags=0x0(none) hashes=19+5 location=embedded
Signature size=4700
Authority=iPhone Developer: 张三 (AFCH46B9XZ)
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
Signed Time=Jul 18, 2016, 17:23:01
Info.plist entries=26
TeamIdentifier=RYRPKMVKDL
Sealed Resources version=2 rules=12 files=7
Internal requirements count=1 size=180

Authority=iPhone Developer: 张三 (AFCH46B9XZ)就是我的证书,iPhone Developer是开发使用,如果是发布证书的话则显示为 iPhone Distribution,Identifier是我在 Xcode 中设置的 bundle identifier。TeamIdentifier用于标识我的工作组(系统会用这个来判断应用是否是由同一个开发者发布),可以通过私钥共享的方式进行团队开发,首先,将存储在keychain中的证书所对应的私钥导出为p12文件,其他团队成员将此文件导入自己电脑,然后从member center下载对应的mobileprovision文件,就可以进行真机开发以及打包发布了。

值得一提的是在新发布的Xcode8中新增了支持自动管理证书和自定义管理证书,自动管理证书将会根据你所选择的开发者账号自动为你处理配置文件和签名证书的设置,并且只在必要时进行提醒。这为开发者节省了不少时间和经历。

上面提到证书(及其对应的私钥)和配置文件(mobileprovision)是签名和打包的两个必要文件,那么,mobileprovision在哪里呢?

App在签名的过程中会在程序包中新建一个叫做_CodeSignatue/CodeResources 的文件,这个文件中存储了被签名的程序包中所有文件的签名。可以查看plist 格式文件,它不光包含了文件和签名的列表,还包含了一系列规则,这些规则决定了哪些资源文件应当被设置签名。在 CodeResources文件中会有4个不同区域,其中的rules和files是为老版本准备的,而 files2和rules2是为新的第二版的代码签名准备的。最主要的区别是在新版本中你无法再将某些资源文件排除在代码签名之外,在过去(OS X 10.9.5 之前)你是可以的,只要在被设置签名的程序包中添加一个名为 ResourceRules.plist的文件,这个文件会规定哪些资源文件在检查代码签名是否完好时应该被忽略。但是在新版本的代码签名中,这种做法不再有效。在新版本的代码签名规定中,所有的代码文件和资源文件都必须设置签名,不再可以有例外,一个程序包中的可执行程序包,例如扩展 (extension),是一个独立的需要设置签名的个体,在检查签名是否完整时应当被单独对待。

所以,比如微信这种多target的App,在做重签名的时候,不仅是主工程,还包括watch app及其他extension,都需要重新签名才可以。

以开源中国为例,先从Appstore下载ipa文件,首先执行$ unzip oschina.ipa,解压ipa包,进入Payload文件夹内,找到iosapp.app包,$ codesign -vv -d oschina.app 会列出一些有关 Example.app 的签名信息,通过security命令产看keychain中已经安装的证书文件$ security find-identity -p codesigning,显示结果如下:

1
2
3
4
5
6
7
8
Policy: Code Signing
Matching identities
1) C552814957BC5691121564774AC86E036B9E2AEE "iPhone Developer: abc (WGDSKET7K5)" (CSSMERR_TP_CERT_EXPIRED)
2) 630648B3BF32E6D349EDE08C4517CAFA9B12FD6B "iPhone Developer: def (UX59QF88DD)" (CSSMERR_TP_CERT_EXPIRED)

Valid identities only
1) 32F000F9C845C6626E8E18919E58C386065C5D16 "iPhone Distribution: abc Co.,Ltd."
2) 38A279804C22853C3F2575FD06BF26E225C08569 "iPhone Distribution: def Co., Ltd."

Matching identities下显示所有已经安装的证书,Valid identities only代表当前可用的证书。

真正的开始

由于Appstore的的应用都是经过DRM加密的,如果想重签名App,需要将从Appstore下载的ipa文件解密,否则就算签名成功,安装成功,app还是会闪退。通过逆向Appstore应用(一)中的方法,可以拿到解密后开源中国iosapp.decrypted文件,替换Payload目录下ios.app内的名为iosapp的二进制文件,此时就可以得到解密后的iosapp.app文件了。在Payload目录下执行:

1
$ codesign -s "iPhone Distribution: abc" iosapp.app

提示:iosapp.app: is already signed,说明此app文件已经被签名过了,需要加上-f参数:

1
$ codesign -f -s "iPhone Distribution: abc" iosapp.app

显示:iosapp.app: replacing existing signature,签名成功,执行codesign -vv -d查看签名信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  Payload codesign -vv -d iosapp.app
Executable=/Users/pandora/Desktop/shell/oschina/Payload/iosapp.app/iosapp
Identifier=net.oschina.iosapp
Format=app bundle with Mach-O universal (armv7 arm64)
CodeDirectory v=20200 size=64082 flags=0x0(none) hashes=1997+3 location=embedded
Signature size=4758
Authority=iPhone Distribution: abc.
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
Signed Time=Sep 6, 2016, 18:08:09
Info.plist entries=36
TeamIdentifier=C789GLWV85
Sealed Resources version=2 rules=12 files=473
Internal requirements count=1 size=212

然后,压缩已经签名的Payload文件夹为zip格式,然后修改zip为ipa格式即可得到解密后的ipa文件了。使用同步推安装提示ApplicationVerificationFail,提示认证失败,这是因为配置文件(.mobileprovision)认证失败,决定了某一个应用是否能够在某一个特定的设备上运行,接下来需要给替换开源中国的mobileprovision文件。找到证书”iPhone Distribution: abc”对应的配置文件A.mobileprovision,修改名称为:embedded.mobileprovision,copy至iosapp.app包内,重新压缩转换为ipa文件,安装时依然提示:ApplicationVerificationFail。

这是因为缺少了entitlements文件,它决定了哪些系统资源在什么情况下允许被一个应用使用。简单的说它就是一个沙盒的配置列表,上面列出了哪些行为被允许,哪些会被拒绝,Xcode 会将这个文件作为 --entitlements 参数的内容传给codesign。在 Xcode 的 Capabilities 选项卡下选择一些选项之后, Xcode 会自动生成一个 entitlements文件,然后在需要的时候往里面添加条目。比如将应用添加进了一个 App Group (比如说为了与extensions 共享数据,com.apple.security.application-groups), 或者开启了推送功能 (aps-environment),如果有将它连接到调试器的需求,这就需要将 get-task-allow 设为true等等。

可以通过如下命令查看entitlements文件:

1
codesign -d --entitlements - /Users/pandora/Desktop/shell/oschina/开源中国\ 3.7.1/Payload/iosapp.app

得到的信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>WSUBE85MHP.net.oschina.iosapp</string>
</array>

<key>com.apple.developer.team-identifier</key>
<string>WSUBE85MHP</string>

<key>application-identifier</key>
<string>WSUBE85MHP.net.oschina.iosapp</string>

</dict>
</plist>

所以,重新签名一个app时,除了替换Certificate证书与mobileprovision配置文件外,还需要生成对应的entitlements.plist文件,分别执行以下两个命令:

1
2
$ security cms -D -i "extracted/Payload/$APPLICATION/embedded.mobileprovision" > t_entitlements_full.plist
$ /usr/libexec/PlistBuddy -x -c 'Print:Entitlements' t_entitlements_full.plist > t_entitlements.plist

t_entitlements.plist内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>C789GLWV85.com.baidu.abc</string>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.developer.associated-domains</key>
<string>*</string>
<key>com.apple.developer.team-identifier</key>
<string>C789GLWV85</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.baidu.abc</string>
</array>
<key>get-task-allow</key>
<false/>
<key>keychain-access-groups</key>
<array>
<string>C789GLWV85.*</string>
</array>
</dict>
</plist>

得到t_entitlements.plist后,把它作为参数传递给codesign,重新签名app文件:

1
codesign -f -s "iPhone Distribution: abc" /Users/pandora/Desktop/shell/oschina/Payload/iosapp.app/ --entitlements t_entitlements.plist

最后,压缩Payload文件夹,转化为ipa格式的文件,终于安装成功!本次重签名未更改bundleID,安装的时候会覆盖之前从Appstore下载的应用。如果希望两个或多个App共存的话,需要先替换App包中Info.plist的Bundle identifier的值:

1
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.baidu.abc" ./Payload/iosapp.app/Info.plist

然后再重新执行:

1
codesign -f -s "iPhone Distribution: abc" /Users/pandora/Desktop/shell/oschina/Payload/iosapp.app/ --entitlements t_entitlements.plist

就得到了更改bundle id后的App包,将其压缩转换为ipa文件,使用同步堆安装至手机,就可以实现多个相同的App共存了。总结,共需6步:

1
2
3
4
5
6
7
8
9
10
11
#1,解密二进制文件
#2,替换embedded.mobileprovision
#3,修改Bundle ID
$ /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.baidu.bac" ./Payload/iosapp.app/Info.plist
#4,生成mobileprovision证书对应的entitlements文件
$ security cms -D -i "./Payload/iosapp.app/embedded.mobileprovision" > t_entitlements_full.plist
$ /usr/libexec/PlistBuddy -x -c 'Print:Entitlements' t_entitlements_full.plist > t_entitlements.plist
#5,将Certificate和entitlements作为参数,传递给codesign签名
$ codesign -f -s "iPhone Distribution: abc" /Users/pandora/Desktop/shell/demo/oschina/Payload/iosapp.app/ --entitlements t_entitlements.plist
# 成功提示:replacing existing signature
#6,压缩签名后的Payload文件夹,转换zip为ipa格式,安装即可。

参考:

ObjC中国

iOS重签名探索

Resigning 3rd party apps

漫谈iOS程序的证书和签名机制

iOS 冰与火之歌番外篇 - App Hook 答疑以及 iOS 9 砸壳

逆向Appstore应用(一)

写在前面

最近在做一些iOS启动速度优化和App瘦身相关的工作,有时候需要横向对比一下竞品的数据,免不了要对一些商店的App在许可范围内做一些手脚,下面以开源中国的iPhone客户端为例给大家简单介绍下操作步骤。

当前环境:

  • 一台越狱iPhone,系统为iOS8.3
  • mac OS X EI 10.11.5
  • iTunes 12.4

通过iTunes获取ipa

点击下载,然后到我的Apps中找到刚刚下载的开源中国.ipa文件,将后缀.ipa修改为.zip,直接解压即可。找到Payload文件夹下的iosapp包文件,查看包内容,就是开发者上传Appstore的全部内容了。里面包含有png、xib等资源文件,以及本地化语言包,签名包以及二进制文件。

使用工具解密二进制文件

配图来自ifanr

App Store上的应用都使用了DRM(Digital Rights Management)数字版权加密保护技术,想更多了解DMG历史请戳这里。首先得破解加密的可执行文件,可以通过编译dumpdecrypted来解密,原理是让app预先加载一个解密的dumpdecrypted.dylib,然后在程序运行后,将代码动态解密,最后在内存中dump出来整个程序。(如果没有越狱设备,可以通过第三方市场下载未加密的ipa文件,跳过此步骤)。

找到刚才解压好的开源中国二进制,名为iosapp,运行:

1
2
3
4
5
6
7
8
otool -l iosapp_本地路径 | grep crypt
# 结果为:
cryptoff 16384
cryptsize 5619712
cryptid 1
cryptoff 16384
cryptsize 6144000
cryptid 1

cryptid 1代表加密,cryptid 0代表未加密。两个分别对应着armv7和arm64,也就是它们都有加密。接下来要开始编译dumpdecrypted.dylib文件了,首先clone dumpdecrypted到桌面,cd到dump decrypted目录,执行以下命令,即可生成dumpdecrypted.dylib文件了。

1
2
 make
`xcrun --sdk iphoneos --find gcc` -Os -Wimplicit -isysroot `xcrun --sdk iphoneos --show-sdk-path` -F`xcrun --sdk iphoneos --show-sdk-path`/System/Library/Frameworks -F`xcrun --sdk iphoneos --show-sdk-path`/System/Library/PrivateFrameworks -arch armv7 -arch armv7s -arch arm64 -c -o dumpdecrypted.o dumpdecrypted.c

编译后的目录如下:

如何使用dumpdecrypted.dylib文件来破解spa包中的二进制文件呢?

首先需要在已经越狱的iPhone中下载几个插件:

  • OpenSSH,用来建立与iPhone的ssh链接,传递文件用;
  • adv-cmds,在iPhone上使用ps命令查看进程,方便找到当前运行的App在iPhone中的文件目录;
  • iFile,在iPhone上建立一个web server,方便与mac传递数据;

通过Cydia安装好插件后,就要开始解密iosapp这个文件了。在iPhone设置中查找当前链接网络的ip地址,比如是:192.168.0.100,打开mac命令行,建立ssh链接:

1
ssh root@192.168.0.100

此时提示输入密码,默认为:alpine,无需修改。连接成功后,cd 到 /var/mobile/Containers/Bundle/Application/ 目录下,可能看到所有安装的App文件夹,在iPhone中打开开源中国App,保持前台运行。然后在mac终端执行以下命令:

1
ps -e | grep var

结果如下:

1
2
3
4
5
6
7
8
9
 143 ??         0:04.00 /usr/libexec/pkd -d/var/db/PlugInKit-Annotations
398 ?? 0:01.08 /private/var/db/stash/_.T1qNfY/Applications/MobileSafari.app/webbookmarksd
529 ?? 0:04.78 /private/var/db/stash/_.T1qNfY/Applications/Stocks.app/PlugIns/StocksWidget.appex/StocksWidget
530 ?? 0:01.77 /private/var/db/stash/_.T1qNfY/Applications/MobileCal.app/PlugIns/CalendarWidget.appex/CalendarWidget
3490 ?? 0:14.24 /var/mobile/Containers/Bundle/Application/7738D5DE-B38D-4BF1-9551-BF671978045F/CocoaPods.app/CocoaPods
3622 ?? 0:08.91 /var/mobile/Containers/Bundle/Application/B590E6C8-D5FA-4697-AF4B-EF717ADBCB90/BaiduBoxApp.app/BaiduBoxApp
4328 ?? 0:06.65 /var/mobile/Containers/Bundle/Application/86E38E63-5FF3-4EF5-AB07-D13C4CB70168/iosapp.app/iosapp
4350 ?? 0:01.47 /var/mobile/Containers/Bundle/Application/FBA32574-0453-4BB6-9F46-A87757F22325/WeChat.app/WeChat
4394 ttys000 0:00.01 grep var

由此可见开源中国的App运行在/var/mobile/Containers/Bundle/Application/86E38E63-5FF3-4EF5-AB07-D13C4CB70168/iosapp.app/目录下,每个app目录下都有一个Info.plist。我们可以通过这个Info.plist得到app的Bundle ID:

1
cat /var/mobile/Containers/Bundle/Application/86E38E63-5FF3-4EF5-AB07-D13C4CB70168/iosapp.app/Info.plist

得到开源中的的Bundle ID为net.oschina.iosapp,由于App的运行会受到沙盒的限制,因此dump出来的app只能保存在data目录下,这里我们可以通过Bundle ID和一个private API得到data目录的位置:

1
2
3
4
5
6
7
8
9
10
11
12
Class cls = NSClassFromString(@"LSApplicationWorkspace");
id s = [(id)cls performSelector:NSSelectorFromString(@"defaultWorkspace")];
NSArray *arr = [s performSelector:NSSelectorFromString(@"allInstalledApplications")];
Class Proxy = NSClassFromString(@"LSApplicationProxy");

for (id proxy in arr) {
id bundleID = [proxy valueForKey:@"applicationIdentifier"];
if (bundleID && [bundleID isEqualToString:@"net.oschina.iosapp"]) {
id dataUrl = [proxy valueForKey:@"dataContainerURL"];
NSLog(@"dataUrl : %@",dataUrl);
}
}

打印出来的data url为file:///private/var/mobile/Containers/Data/Application/6C425F0E-217C-4051-A131-735F3A816D89,接下来在mac终端,cd到dumpdecrypted.dylib所在目录,将dumpdecrypted.dylib拷贝到开源中国的Documents目录下:

1
scp dumpdecrypted.dylib root@172.24.64.228:/private/var/mobile/Containers/Data/Application/6C425F0E-217C-4051-A131-735F3A816D89/Documents/dumpdecrypted.dylib

传输成功显示:

1
dumpdecrypted.dylib   100%  193KB 192.9KB/s   00:00

然后进入iPhone中开源中国所在根目录的Documents下,执行DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib相关的命令,即将开始编译:

1
DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/Application/7202242C-DABA-4520-8D3D-81448728F587/iosapp.app/iosapp

成功后,会在Documents目录生成名为iosapp.decrypted的文件,至此就算是破解成功了,如下图所示:

如果显示dumpdecrypted.dylib: stat() failed with errno=1 即“Operation not permitted”,表示操作权限不够,可以将dumpdecrypted.dylib文件sip至手机的usr/lib目录下,然后再执行DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib命令编译,此时会在usr/lib目录下生成iosapp.decrypted文件。


砸壳后的app只能解密砸壳运行时的架构,可以看到在arm64架构下显示cryptid为0,已经成功解密。

解密后的文件如何使用?

在得到iosapp.decrypted后,真正的逆向App工作才刚刚开始。未完待续…

参考:

http://www.ifanr.com/473806

http://www.liuchendi.com/2015/12/23/iOS/24_dumpdecrypted/

一款Watch OS版微博

写在前面

手表买了将近快一年的时间了,每天常用的就是运动提醒了,尤其是间隔一小时站立及步数统计,对久坐人士有很好的督促作用。一直想着做一款手表App,最近终于有些时间就开始捣鼓了,大概前后花了两个周的空闲时间,基于Watch OS 1简单实现了一款手表版的微博,如有纰漏,还望指正。

App Groups

首先了解下App Groups的概念,这是Apple在iOS8中引入的技术。旨在位于同一group的app之间共享一份数据读写空间,打破了沙盒技术对应用间通信与交互的限制。比如通知中心的Today Extension作为app功能的补充而存在,Watch App也类似于此。在开始之前需要先创建App Group。可以在Xcode的Capabilities中添加,或者在Apple developer 中心来添加,本文采用后一种方式给大家介绍。

新生成的group id为”group.baidu.com.miniweibo”,用来关联Watch App与iPhone App,实现数据共享及进程通信。创建名为MiniWeibo的Project,对应生成MiniWeibo、MiniWeibo WatchKit 1 Extension和MiniWeibo WatchKit 1 App 三个target,然后在 Xcode 的 Capabilities 选项卡下分别打开主 target 与 WatchKit Extension target下的App Groups选项,刷新选中刚创建的group id即可。如下图所示:

选中后会自动添加名为MiniWeibo.entitlements与MiniWeibo WatchKit 1 Extension.entitlements的两个XML文件至工程文件中,当构建整个应用时,这两个entitlements文件也会提交给 codesign 作为应用所需要拥有哪些授权的参考,可以在 Xcode build setting 中的 code signing entitlements 中设置。entitlements文件内部格式如下:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.baidu.miniweibo</string>
</array>
</dict>
</plist>

<key>com.apple.security.application-groups</key>标识着此应用被添加至名为group.com.baidu.miniweibo的group中,为了与扩展 (extensions) 共享数据。另外还可以添加get-task-allow用来限定只能在用于开发的证书签名下运行,以及设置asp-environment的推送环境为development或者distribution。授权信息会被包含在应用的签名信息中,可以执行$ codesign -d --entitlements - Example.app,返回一个XML文件,查看签名信息中具体包含了什么授权信息,方便工程配置的查看与调试。

数据共享与调试

App Groups共享数据可以创建group id为标记的NSUserDefaults,来写入和读取可plist化的数据对象:

1
2
3
4
5
6
- (void)saveTextByNSUserDefaults
{
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"];
[shared setObject:_textField.text forKey:@"wangzz"];
[shared synchronize];
}

或者通过NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法,创建NSFileManager的单例,来管理App Group数据。File可以是普通的二进制文件,或者是sqlite文件。MiniWeibo采用的是基于sqlite的数据存储方式,来实现App间的通信:

1
2
NSString *dataBasePath = [[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.baidu.miniweibo"] URLByAppendingPathComponent:@"miniweibo.db"] path];
_queue = [FMDatabaseQueue databaseQueueWithPath:dataBasePath];

sqlite内部的数据处理是采用第三方开源库FMDB来实现,详细了解请参考:https://github.com/ccgus/fmdb

大致实现流程可以描述为:每个可交互的 WKInterfaceObject 子类都对应了一个 action,来调用Watch Kit中openParentApplication方法,唤起iOS target中application:handleWatchKitExtensionRequestreply:代理方法,请求网络数据,需要持久化的通过sqlite存储共享,临时数据通过reply回调,传递给Watch Extension来刷新Watch App界面。

开发的过程中,需要在iPhone App 与 Watch App之间频繁进程切换,通过Xcode -> Debug -> Attach to Process -> [process name],绑定iOS App 与 Watch Extension进行调试。

工程架构

Watch App是基于iOS App而存在的,WatchKit Extension主要负责逻辑部分,将随iOS App的主target被一同安装到iPhone中,通过UIApplicationDelegate及WKInterfaceController来调度手表上负责界面显示的WatchKit App。

图片来自 http://tech.glowing.com/cn/make_apple_watch_app/

1,主要类

如同WKInterfaceController一样,WKInterfaceButton、WKInterfaceLabel、WKInterfaceImage等并非是UIView的子类,而是继承自NSObject,Watch App 中实际展现和渲染在屏幕上的 view 对于代码来说是非直接可见的,我们只能在 Extension target 中通过对应的代理对象对属性进行设置,由 WatchKit 传递给手表中的 Watch App 并进行界面刷新。

2,Glance 和 Notification

GlanceController意为速览,算是Watch App的快捷显示及操作界面,如音乐App的播放控制,天气App的数据展示等。有些Glance界面是固定的,无需每次显示的时候刷新,只需在initWithContext中初始化即可;有些是需要动态展示,每次滑出Glance的时候都需要刷新界面,则需要调用呈现时的 -willActivate 方法,类似UIViewController中的viewWillAppear,相对的,-didDeactivate方法可以理解为viewDidDisappear,即界面即将消失。

当我们加入了一个新的通知界面之后,Xcode就会为我们生成这两个interface,分为Static Notification Interface Controller 和 Dynamic Notification Interface Controller两种。动态的Interface是可选的。当一个的通知到来之后,系统会优先去呈现动态的通知界面,当动态界面不可用或者低电量时才会去呈现静态的。动态通知使得我们可以提供一个更加丰富的体验给用户,如同其他Interface Controller一样,可以通过Outlet及IBAction来关联和定义,增强可操作性。通知流程如下图:

1
2
3
4
5
6
7
8
- (void)didReceiveRemoteNotification:(NSDictionary *)remoteNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler {
// This method is called when a remote notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
//
// After populating your dynamic notification interface call the completion block.
completionHandler(WKUserNotificationInterfaceTypeCustom);
}

​ completionHandler(WKUserNotificationInterfaceTypeCustom)中参数WKUserNotificationInterfaceTypeCustom决定了优先呈现Dynamic界面,如果是WKUserNotificationInterfaceTypeDefault则会显示Static通知界面。在调用completionHandler之前系统会短暂地显示Short-Look Interface,它不能滚动和定制,系统会显示App 的名字以及App 的图标Icon,以及一条自定义通知信息,同时在准备数据,构建Long-Look界面,然后系统会迅速的切换到The Long-Look Interface中,包含三个区域:顶部Sash、中间Content area及底部按钮区域,点击Sash、Content area都会直接进入Watch App,点击底部按钮区域可以触发Watch Kit与Watch Extension及iOS App交互。

准备自定义的NotificationInterface后,就可以开始测试了,模拟器可以直接选择Notification target编译即可。真机测试,可以使用Mac 端 apns小工具SmartPush,进行开发和生产环境的调试。至此MiniWeibo工程的开发暂告一段落,附张截图吧:

开发中的几个Tips:

  • 图片缓存

    使用addCachedImageWithData:name:来添加NSData缓存图片数据,相比较addCachedImage:name:方法避免了使用PND编码的过程,节省存储空间。每个Watch App大概有5mb的缓存分配,如果存储控件不足时,需要调用 removeCachedImageWithName:或者 removeAllCachedImages方法来清除数据。否则WatchKit将从最老的数据开始自动删除,为新数据腾出空间。

  • WKInterfaceLabel高度自适应与滚动

    不同与UIKit,很多视图控件无法通过手写的方式来管理,需要在Interface.storyboard中设置参数。比如WKInterfaceLabel需要多行显示和滚动时,需要设置Label->Lines为0,Size->Width为Relative to Container,以及Size->Height为Size To Fit Content,意为宽度与父视图一样,高度为内容自适应,即可实现多行显示与滚动了。

  • WKInterfaceGroup的使用

    WKInterfaceGroup继承自NSObject,可以理解为视图块,集中管理某个区域的视图显示,比如table cell。可以指定background colour、alpha及Radius等UIView的属性设置,在InterfaceController中使用频率很高。

关于Watch OS 2

Watch OS 2引入了ClockKit与WatchConnectivity框架,不仅增强了表盘自定义控件的灵活性,数据共享操作改进很大,其中extension 是直接存在于手表中的,在Watch OS 1中通过app group管理的方式已经失效。另外HealthKit也得到增强,健康监护及运动统计类的App会大有施展空间。

参考

1,https://onevcat.com/2014/08/notification-today-widget/OneV’s Den

2,http://foggry.com/blog/2014/06/23/wwdc2014zhi-app-extensionsxue-xi-bi-ji/王中周的技术博客

3,http://objccn.io/issue-17-2/objc中国

4,Apple Watch 文档

关于GCD的几个用例

GCD作为日常开发中的使用已经非常普遍,基于C的API为应用在多核硬件上高效运行提供了有力支持。本文分场景写了几个测试Demo,方便大家理解与应用。

1,dispatch_get_global_queue与dispatch_get_main_queue交互

很多应用场景需要后台读写大量数据,通过dispatch_get_global_queue函数可以获取全局队列来并发执行后台任务,并再结束后更新UI,保证应用的流程,避免主线程阻塞。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)test1
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < 10; i++) {
dispatch_group_async(group, queue, ^{
NSLog(@"___%i %@", i, [NSThread currentThread]);
});
}
dispatch_group_notify(group, queue, ^{
NSLog(@"currentThread1 %@",[NSThread currentThread]);
NSLog(@"执行完毕");

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"currentThread2 %@",[NSThread currentThread]);
NSLog(@"main_queue 执行完毕");
});
});
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test1:
2016-03-10 17:21:37.293 GCDSample[3892:2238505] ___0 <NSThread: 0x12e53e570>{number = 2, name = (null)}
2016-03-10 17:21:37.293 GCDSample[3892:2238506] ___2 <NSThread: 0x12e67f1e0>{number = 4, name = (null)}
2016-03-10 17:21:37.293 GCDSample[3892:2238504] ___3 <NSThread: 0x12e689760>{number = 3, name = (null)}
2016-03-10 17:21:37.293 GCDSample[3892:2238507] ___1 <NSThread: 0x12e683590>{number = 5, name = (null)}
2016-03-10 17:21:37.294 GCDSample[3892:2238505] ___4 <NSThread: 0x12e53e570>{number = 2, name = (null)}
2016-03-10 17:21:37.295 GCDSample[3892:2238506] ___5 <NSThread: 0x12e67f1e0>{number = 4, name = (null)}
2016-03-10 17:21:37.295 GCDSample[3892:2238504] ___6 <NSThread: 0x12e689760>{number = 3, name = (null)}
2016-03-10 17:21:37.295 GCDSample[3892:2238507] ___7 <NSThread: 0x12e683590>{number = 5, name = (null)}
2016-03-10 17:21:37.295 GCDSample[3892:2238505] ___8 <NSThread: 0x12e53e570>{number = 2, name = (null)}
2016-03-10 17:21:37.298 GCDSample[3892:2238506] ___9 <NSThread: 0x12e67f1e0>{number = 4, name = (null)}
2016-03-10 17:21:37.299 GCDSample[3892:2238506] currentThread1 <NSThread: 0x12e67f1e0>{number = 4, name = (null)}
2016-03-10 17:21:37.299 GCDSample[3892:2238506] 执行完毕
2016-03-10 17:21:37.302 GCDSample[3892:2238481] currentThread2 <NSThread: 0x12e50c390>{number = 1, name = main}
2016-03-10 17:21:37.303 GCDSample[3892:2238481] main_queue 执行完毕

2,异步线程中串行执行任务

test1中dispatch_group_async以并发的方式开启异步线程,不能保证执行顺序,如果想在并发线程中串行执行任务该如何做呢?只需要创建一个串行队列,加入group任务即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)test1
{
NSLog(@"code begin");
NSLog(@"currentThread0 %@",[NSThread currentThread]);
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t serialQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);

for (int i = 0; i < 10; i++) {
dispatch_group_async(group, serialQueue, ^{
[NSThread sleepForTimeInterval:.1f];
NSLog(@"___%i %@", i, [NSThread currentThread]);
});
}

dispatch_group_notify(group, serialQueue, ^{
NSLog(@"currentThread1 %@",[NSThread currentThread]);
NSLog(@"执行完毕");
});

NSLog(@"code end");
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test2:
2016-03-10 17:07:10.366 GCDSample[3879:2235420] code begin
2016-03-10 17:07:10.367 GCDSample[3879:2235420] currentThread0 <NSThread: 0x14c50c350>{number = 1, name = main}
2016-03-10 17:07:10.367 GCDSample[3879:2235420] code end
2016-03-10 17:07:10.472 GCDSample[3879:2235438] ___0 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:10.577 GCDSample[3879:2235438] ___1 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:10.683 GCDSample[3879:2235438] ___2 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:10.784 GCDSample[3879:2235438] ___3 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:10.890 GCDSample[3879:2235438] ___4 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:10.995 GCDSample[3879:2235438] ___5 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:11.101 GCDSample[3879:2235438] ___6 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:11.206 GCDSample[3879:2235438] ___7 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:11.309 GCDSample[3879:2235438] ___8 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:11.411 GCDSample[3879:2235438] ___9 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:11.411 GCDSample[3879:2235438] currentThread1 <NSThread: 0x14c692ed0>{number = 2, name = (null)}
2016-03-10 17:07:11.411 GCDSample[3879:2235438] 执行完毕

3,dispatch_barrier_async,分割多任务线程

如果有10个并发任务,想要分成两组,比如指定前五个任务执行完之后,优先执行第六个任务,再执行剩下的操作,可以通过dispatch_barrier_async来分割开,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)test3
{
dispatch_queue_t concurrentQueue = ({
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
queue;
});
for (int i = 0; i < 10; i++) {
if (i != 5) {
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:arc4random_uniform(3)];
NSLog(@"dispatch_async %i", i);
});
}else{
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"dispatch_barrier_async");
});
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
test3
2016-03-10 17:47:32.397 GCDSample[3949:2246479] dispatch_async 1
2016-03-10 17:47:32.400 GCDSample[3949:2246481] dispatch_async 3
2016-03-10 17:47:33.403 GCDSample[3949:2246480] dispatch_async 2
2016-03-10 17:47:34.400 GCDSample[3949:2246477] dispatch_async 0
2016-03-10 17:47:34.405 GCDSample[3949:2246479] dispatch_async 4
2016-03-10 17:47:34.406 GCDSample[3949:2246477] dispatch_barrier_async
2016-03-10 17:47:34.406 GCDSample[3949:2246479] dispatch_async 6
2016-03-10 17:47:35.411 GCDSample[3949:2246479] dispatch_async 7
2016-03-10 17:47:35.412 GCDSample[3949:2246480] dispatch_async 9
2016-03-10 17:47:35.411 GCDSample[3949:2246477] dispatch_async 8

4,串行队列死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)test4
{
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}

- (void)test4_1 {
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2 %@",[NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3 %@",[NSThread currentThread]);
});
NSLog(@"4 %@",[NSThread currentThread]);
});
NSLog(@"5");
}

方法test4会造成死锁,因为main queue需要等待dispatch_sync函数中block返回才能继续执行,而通过dispatch_sync放入main queue的block按照FIFO的原则(先入先出)现在得不到执行,就造成了相互等待的局面,产生死锁。将同步的串行队列放到另外一个异步线程就能够解决(如方法test4_1所示)。所以在使用dispatch_sync的时候需要很谨慎,需要先执行[NSThread isMainThread]判断下当前任务是否在mian queue中调用,比如有一段代码在后台执行,而它需要从界面控制层获取一个值。那么你可以使用dispatch_sync简单办到。执行结果:

1
2
3
4
5
6
7
8
9
test4
2016-03-10 17:23:30.020 GCDSample[3896:2239119] 1

test4_1
2016-03-11 17:30:11.900 GCDSample[1197:730552] 1
2016-03-11 17:30:11.901 GCDSample[1197:730552] 5
2016-03-11 17:30:11.903 GCDSample[1197:730564] 2 <NSThread: 0x170263080>{number = 2, name = (null)}
2016-03-11 17:30:11.933 GCDSample[1197:730552] 3 <NSThread: 0x174075400>{number = 1, name = main}
2016-03-11 17:30:11.933 GCDSample[1197:730564] 4 <NSThread: 0x170263080>{number = 2, name = (null)}

5,dispatch_semaphore_t 控制并发线程数

有时在执行多任务时需要避免抢占资源以及性能过多消耗的情况,需要在特定时间内控制同时执行的任务数量,在NSOperationQueue可以通过maxConcurrentOperationCount来控制,在GCD中可以指定semaphore来控制了。先介绍3个函数,dispatch_semaphore_create创建一个semaphore;dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1;dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1。看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (void)test7
{
_semaphore = dispatch_semaphore_create(2);
[self task7_1];
[self task7_2];
[self task7_3];
[self task7_4];
}

- (void)task7_1
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"7_1_begin");
sleep(4);
NSLog(@"7_1_end");
dispatch_semaphore_signal(_semaphore);
});
}

- (void)task7_2
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"7_2_begin");
sleep(4);
NSLog(@"7_2_end");
dispatch_semaphore_signal(_semaphore);
});
}

- (void)task7_3
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"7_3_begin");
sleep(4);
NSLog(@"7_3_end");
dispatch_semaphore_signal(_semaphore);
});
}

- (void)task7_4
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"7_4_begin");
sleep(4);
NSLog(@"7_4_end");
dispatch_semaphore_signal(_semaphore);
});
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
semaphore 为 1

2016-03-11 16:33:11.957 GCDSample[22394:2943718] 7_1_begin
2016-03-11 16:33:15.959 GCDSample[22394:2943718] 7_1_end
2016-03-11 16:33:15.960 GCDSample[22394:2943717] 7_2_begin
2016-03-11 16:33:19.964 GCDSample[22394:2943717] 7_2_end
2016-03-11 16:33:19.965 GCDSample[22394:2943721] 7_3_begin
2016-03-11 16:33:23.970 GCDSample[22394:2943721] 7_3_end
2016-03-11 16:33:23.970 GCDSample[22394:2943727] 7_4_begin
2016-03-11 16:33:27.975 GCDSample[22394:2943727] 7_4_end

------- ------- ------- ------- ------- ------- ------- ----

semaphore 为 2

2016-03-11 16:33:47.396 GCDSample[22406:2944975] 7_2_begin
2016-03-11 16:33:47.396 GCDSample[22406:2944974] 7_1_begin
2016-03-11 16:33:51.398 GCDSample[22406:2944975] 7_2_end
2016-03-11 16:33:51.398 GCDSample[22406:2944974] 7_1_end
2016-03-11 16:33:51.398 GCDSample[22406:2944976] 7_3_begin
2016-03-11 16:33:51.398 GCDSample[22406:2944983] 7_4_begin
2016-03-11 16:33:55.401 GCDSample[22406:2944976] 7_3_end
2016-03-11 16:33:55.401 GCDSample[22406:2944983] 7_4_end

可以看到,并发执行任务的数量取决于_semaphore = dispatch_semaphore_create(2);传入的数字,当为1时,效果如同执行串行队列。

6,dispatch_set_target_queue 设置队列优先级

如果有串行队列A和并行队列B,队列A中加入任务1,队列B中加入任务2、任务3,如果确保1、2、3顺序执行呢?可以通过dispatch_set_target_queue设置队列的优先级,将队列AB指派到队列C上,任务123将会在串行队列C中顺序执行。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)test8
{
dispatch_queue_t serialQueue = dispatch_queue_create("com.starming.gcddemo.serialqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t firstQueue = dispatch_queue_create("com.starming.gcddemo.firstqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t secondQueue = dispatch_queue_create("com.starming.gcddemo.secondqueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_set_target_queue(firstQueue, serialQueue);
dispatch_set_target_queue(secondQueue, serialQueue);

dispatch_async(firstQueue, ^{
NSLog(@"1 %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.f];
});
dispatch_async(secondQueue, ^{
NSLog(@"2 %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.f];
});
dispatch_async(secondQueue, ^{
NSLog(@"3 %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1.f];
});
}

执行结果:

1
2
3
2016-03-11 17:31:41.515 GCDSample[1202:730942] 1 <NSThread: 0x170078340>{number = 2, name = (null)}
2016-03-11 17:31:44.518 GCDSample[1202:730942] 2 <NSThread: 0x170078340>{number = 2, name = (null)}
2016-03-11 17:31:46.520 GCDSample[1202:730942] 3 <NSThread: 0x170078340>{number = 2, name = (null)}

未完待续,Have fun!

参考:
https://github.com/nixzhu/dev-blog/blob/master/2014-04-19-grand-central-dispatch-in-depth-part-1.md

FMDB使用摘要

最近项目中需要加入持久化数据缓存的需求,在比较了EGOCacheFMDB之后,决定采用基于SQLite3的FMDB来实现,结合AFNetworking封装了一套API缓存方案,在此简单介绍下具体逻辑与注意事项。

1,使用FMDatabaseQueue

FMDB的每一次数据读写操作,都可以理解为一个Operation,数据库是每个Operation对应的数据源,所以需要保证操作的线程安全,这就引入了FMDatabaseQueue的概念。每次Operation顺序加入队列中,保证了数据访问的有序进行,互补干扰。

+ (instancetype)databaseQueueWithPath:(NSString*)aPath

通过dispatch_queue_create创建并发队列,不同的dataPath通过FMDatabaseQueue来维护,多个queue之间并发执行任务,保证了数据读写的效率。

- (void)inDatabase:(void (^)(FMDatabase *db))block

每次Operation通过inDatabase执行,dispatch_sync方法来添加串行任务,保证了每个FMDatabaseQueue数据的安全与有序。在添加任务前,调用dispatch_get_specific获得FMDatabaseQueue关联的线程,检查是否是同一个线程,因为串行队列里面同步执行dispatch_sync会造成死锁。

2,存储json数据

在GET到API返回json数据时候,常常会遇到字典或者数组嵌套的情况,使用FMDB写入数据时,如果Key-Value一一对应会比较麻烦,所以,把整条记录转换为string对象来存储,在不降低性能的情况下对数据的写入与读取十分便捷。在此需要注意sql语句中的string要作为参数写入,以免引起syntax error,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 正确:
NSString *insertSQl=[NSString stringWithFormat:@"insert or replace into '%@' (ID,JSONString) values (?,?) ",tableName];
[_queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
if(![db executeUpdate:insertSQl,objId,jsonStr]){
BDLog(@"rollback Table %@",tableName);
*rollback = YES;
return ;
}
}];

// 错误:
NSString *insertSQl=[NSString stringWithFormat:@"insert or replace into '%@' (ID,JSONString) values (%@,%@) ",tableName,objId,jsonStr];
[_queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
if(![db executeUpdate:insertSQl]){
BDLog(@"rollback Table %@",tableName);
*rollback = YES;
return ;
}
}];

如果是大量的数据保存操作可以通过dispatch_group_t创建并发线程来执行,在并发线程中加入串行任务,避免对主线程造成阻塞,来提高代码效率。示例如下:

1
2
3
4
5
6
7
8
9
10
11
// 并发线程中串行执行
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t serialQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);

dispatch_group_async(group, serialQueue, ^{
// 先执行清理工作
});

dispatch_group_notify(group, serialQueue, ^{
// 保存数据
});

3,sqlite事务

与关系数据库进行交互的标准 SQLite 命令类似于 SQL。命令包括 CREATE、SELECT、INSERT、UPDATE、DELETE 和 DROP。这些命令基于它们的操作性质可分为以下几种:

  • DDL - 数据定义语言(CREATE、ALTER、DROP)
  • DML - 数据操作语言(INSERT、UPDATE、DELETE)
  • DQL - 数据查询语言(SELECT)

事务控制命令只与 DML 命令 INSERT、UPDATE 和 DELETE 一起使用。他们不能在创建表或删除表时使用,因为这些操作在数据库中是自动提交的。

SQLite中使用下面的命令来控制事务:BEGIN TRANSACTION:开始事务处理;COMMIT:保存更改,或者可以使用 END TRANSACTION 命令;ROLLBACK:回滚所做的更改。如果iOS的sqlite同时插入或者查询10000条数据,你该怎么办?减少数据库的开关操作,通过事务命令,把10000条语句封装成一个事务,只需一次COMMIT transaction即可完成。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 开始事务
-(int)beginService{
char *errmsg;
int rc = sqlite3_exec(database, "BEGIN transaction", NULL, NULL, &errmsg);
return rc;
}

// 提交事务
-(int)commitService{
char *errmsg;
int rc = sqlite3_exec(database, "COMMIT transaction", NULL, NULL, &errmsg);
return rc;
}

- (void)execSQL
{
[database open]
[self beginService];
for (i=0; i<100000; i++) {
//想要执行的sql语句
}
[self commitService]
[database close]
}

FMDB对事务也有很好的支持,调用- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block方法即可实现,如果数据写入或读取失败,可以指定rollback参数来设置是否需要回滚,撤销本次操作。

4,FMDB性能测试

设备环境iphone6s , xcode7.2 ,FMDB分别读写100、500、1000条数据,测试结果如下:

  • 100条数据:写入用时0.655秒,读取0.012秒
  • 500条数据,写入用时2.112秒,读取0.043秒
  • 1000条数据,写入用时6.2秒,读取0.036秒

可见FMDB的读取性能还是很不错的,大量写入数据的时候比较耗时,可以通过切换至后台线程执行,保证主线程的任务可以立即响应。

参考:

http://www.runoob.com/sqlite/sqlite-intro.html

Github上SSH key 的使用与管理

SSH 为 Secure Shell 的缩写,是相对FTP、POP和Telnet等明文传输数据来讲较为安全的一种协议。SSH传输的数据是经过压缩处理后的,传输速度快,从客户端来看,SSH提供两种级别的安全验证,第一种级别(基于口令的安全验证)第二种级别(基于密匙的安全验证)。Github、Gitlab及Bitbuckut等代码托管平台都支持基于密匙的SSH来进行远程代码管理,下面以Github为例具体说下ssh key的创建与使用。

1,SSH key的生成

abc@163.com 为Github的登录邮箱,通过以下命令即可创建一对公私钥 (公钥文件:~/.ssh/id_rsa.pub; 私钥文件:~/.ssh/id_rsa):

1
ssh-keygen -t rsa -C "abc@163.com"

然后会提示本地ssh key的保存路径,如果是单个创建,回车即可报错默认/Users/用户名/.ssh目录下。

1
2
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/pandora/.ssh/id_rsa):

接下来会提示是否需要帐号密码,可以为空,也可以任意指定(首次连接ssh是则会提示输入此密码)。

1
Enter passphrase (empty for no passphrase):

至此,ssh key创建完毕。接下来只需将生成的ssh key保存至github即可,查看ssh key命令:

1
cat ~/.ssh/id_rsa.pub

显示结果为:

1
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCohNI1KuNzVP7UlclbueAp/2Gxhbm0romfChDaqvF3dlMS0SS1HH1HQivG7G2J+hXwhV+V11x3LRKfyIkZy0iq6cccn4+Yan3zdWI12CfhzuHuVOQ7I2nLeDDF/CwqGrY/81r9HQpMNsPfnAHsoAT44M0QcTQORlapJYKIfz4LBT0ZXtGMnm8UeNR3t3RUL0RUZrBjgaeZIuihZjsxfpT3awOsLeTFJDld4Nv2ldw3sADQry0gT912r1IVBvpdmJ8SmQWDvjMggldhzHJoVq3ACM5jK+MSeVAUe11B3WlHDXaUIbHNyRhM+PyQ1FRgckVhz4NwJwPYSWJ5Zalm3GFl abc@163.com

bcopy命令将生成的公钥拷贝至剪切板:

1
pbcopy < ~/.ssh/id_rsa.pub

最后,打开github,找到设置页,在SSH keys中添加即可。

2,链接测试SSH key

运行ssh -T命令即可测试ssh key是否链接成功:

1
ssh -T git@github.com

如成功,则提示:Hi user_abc! You’ve successfully authenticated, but GitHub does not provide shell access. user_abc就是该邮箱在Github注册的用户名。

如果测试连接不成功,可使用ssh -vT git@github.com命令查看详细输出,便于跟踪问题,执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
OpenSSH_6.9p1, LibreSSL 2.1.8
debug1: Reading configuration data /Users/pandora/.ssh/config
debug1: /Users/pandora/.ssh/config line 2: Applying options for github.com
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 21: Applying options for *
debug1: Connecting to github.com [192.30.252.129] port 22.
debug1: Connection established.
debug1: identity file /Users/pandora/.ssh/id_rsa_dama2716588 type 1
debug1: key_load_public: No such file or directory
debug1: identity file /Users/pandora/.ssh/id_rsa_dama2716588-cert type -1
debug1: Enabling compatibility mode for protocol 2.0
debug1: Local version string SSH-2.0-OpenSSH_6.9
debug1: Remote protocol version 2.0, remote software version libssh-0.7.0
debug1: no match: libssh-0.7.0
debug1: Authenticating to github.com:22 as 'git'
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: server->client chacha20-poly1305@openssh.com <implicit> none
debug1: kex: client->server chacha20-poly1305@openssh.com <implicit> none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ssh-rsa SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8
debug1: Host 'github.com' is known and matches the RSA host key.
debug1: Found key in /Users/pandora/.ssh/known_hosts:1
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: Roaming not allowed by server
debug1: SSH2_MSG_SERVICE_REQUEST sent
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: publickey
debug1: Next authentication method: publickey
debug1: Offering RSA public key: /Users/pandora/.ssh/id_rsa_dama2716588
debug1: Server accepts key: pkalg ssh-rsa blen 279
debug1: Authentication succeeded (publickey).
Authenticated to github.com ([192.30.252.129]:22).
debug1: channel 0: new [client-session]
debug1: Entering interactive session.
debug1: Sending environment.
debug1: Sending env LC_CTYPE = UTF-8
debug1: client_input_channel_req: channel 0 rtype exit-status reply 0
Hi dama2716588! You've successfully authenticated, but GitHub does not provide shell access.
debug1: channel 0: free: client-session, nchannels 1
Transferred: sent 3244, received 1776 bytes, in 2.0 seconds
Bytes per second: sent 1650.3, received 903.5
debug1: Exit status 1

3,配置管理SSH key

当本地存储使用多个ssh key时,需要通过config文件(/Users/用户名/.ssh/config)来切换默认账户,ssh config文件常用配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Default github user(dama2716588@126.com)  默认配置,一般可以省略
Host github.com
Hostname github.com
User git
Identityfile ~/.ssh/id_rsa_dama2716588

# 2 user(dama2716588@163.com)
Host github.com
HostName github.com
User git
Identityfile ~/.ssh/id_rsa_pandorago

# 3 user(mayulong01@baidu.com)
gitlab.com 对应配置
Host gitlab.com
HostName gitlab.com
User mayulong01
Identityfile ~/.ssh/id_rsa_gitlab_mayulong01

Host: “personal.github.com”是一个”别名”,可以随意命名, 像github-PERSONAL这样的命名也可以;

HostName:比如我工作的git仓储地址是ssh://g@gitlab.baidu.com/abc.git, 那么我的HostName就要填”baidu.com”;

IdentityFile: 所使用的公钥文件;

参考链接:

https://help.github.com/articles/generating-ssh-keys/

聊一聊iOS后台任务

iOS开发的过程中常常会有按下home键盘,进入后台,但不希望当前任务立即停止的情况,比如保存数据,断开链接,继续下载文件等,接下来就简单聊下iOS的后台任务。

一,后台任务的分类

程序的5个状态和对应的AppDelegate的7个方法

  • Not Running, 未运行
  • Inactive, 非活动
  • Active, 活动
  • Background, 后台
  • Suspend, 挂起

对应的方法分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 进程启动但还没完成初始化,这个方法是iOS6之后才有的

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions

// 进程启动基本完成      

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

// 应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件

- (void)applicationWillResignActive:(UIApplication *)application   

// 应用程序入活动状态,这个刚好跟上面那个方法相反

- (void)applicationDidBecomeActive:(UIApplication *)application    

// 程序被推送到后台,如果要设置后台继续运行,则在这个函数里面设置即可

- (void)applicationDidEnterBackground:(UIApplication *)application     

// 程序从后台将要回到前台

- (void)applicationWillEnterForeground:(UIApplication *)application   

// 程序将要退出

- (void)applicationWillTerminate:(UIApplication *)application

在介绍iOS应用状态5种最基本的状态时,我们发现前台运行有两种状态,分别是Inactive和Active状态。大多数情况下,Inactive状态只是其它状态之间切换时短暂的停留状态,如前后台应用切换时,Inactive状态会在Active和Background之间短暂出现,比如App Switcher/回到原应用的操作等。

用户在按下home键后,app可做的事情有很多,比如听歌、打电话、下载电影、更新数据、定时任务等,可以大致分为两类,注册任务(耗时长)和非注册任务(耗时较短)。

二,非注册任务的运行

非注册任务一般耗时较短,多用来保存数据或延迟执行某一命令等,通过系统API即可实现。可以先看一段官方实例代码:

1
2
3
4
5
6
7
8
9
10
11
- (void)applicationDidEnterBackground:(UIApplication *)application
{
bgTask = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];

// Start the long-running task and return immediately.
}

beginBackgroundTaskWithName:expirationHandler:方法标识了一个后台任务的开始,并用过超时处理的回调来结束此任务。那么,超时时间具体是多少?可以通过UIApplication的只读属性backgroundTimeRemaining来获取当前后台任务执行的剩余时间,它不是具体的数字(我执行的时间大概160秒),而是iOS根据当前系统环境综合考量后估算出来的。然后执行expirationHandler回调完成一个后台任务的执行周期。

三,注册任务的流程

有些耗时较长的工作,则需要申请专门的权限来保证正常执行而不被挂起,只有少数几种类型被允许这个做。

  • Apps that play audible content to the user while in the background, such as a music player app
  • Apps that record audio content while in the background
  • Apps that keep users informed of their location at all times, such as a navigation app
  • Apps that support Voice over Internet Protocol (VoIP)
  • Apps that need to download and process new content regularly
  • Apps that receive regular updates from external accessories

申请使用以上场景的后台权限需要在Xcode->Capabilities->Background Mode中配置,如下图所示:

勾选所需的模式后,会自动在app的info.plist文件中添加Required background modes一项,包含了所勾选的后台运行模式。如下所示:

1
2
3
4
5
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>voip</string>
</array>

以Background Fetch为例,具体说下任务流程。

首先需要在Xcode Capabilities 中开启Background fetch选项,在didFinishLaunchingWithOptions中设置下获取的时间间隔:

1
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

如果不对最小后台获取间隔进行设定的话,系统将使用默值UIApplicationBackgroundFetchIntervalNever,也就是永远不进行后台获取。而最小的时间间隔则有系统根据电量、网络状态、用户使用习惯等综合考量后来设定一定的差值,执行fetch任务,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//File: YourAppDelegate.m
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
UINavigationController *navigationController = (UINavigationController*)self.window.rootViewController;

id fetchViewController = navigationController.topViewController;
if ([fetchViewController respondsToSelector:@selector(fetchDataResult:)]) {
[fetchViewController fetchDataResult:^(NSError *error, NSArray *results){
if (!error) {
if (results.count != 0) {
//Update UI with results.
//Tell system all done.
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
} else {
completionHandler(UIBackgroundFetchResultFailed);
}
}];
} else {
completionHandler(UIBackgroundFetchResultFailed);
}
}

与fetch类型的是,后台任务同样可以满足远程通知唤醒app执行某一进程,结束后以本地通知的方式提醒用户,以静默、智能的方式带给用户使用上的绝佳体验,相似的场景还有后台电影、音乐的下载等,在此不多做介绍。

四,后台任务的注意事项

1,关于OpenGL ES

有些基于位置的app需要后台定时更新用户的当前位置,导致未知崩溃。原因就是Location update类型的后台任务在更新位置时,需要重新绘制MKMapView,调用了OpenGL ES,而OpenGL ES必须在程序Inactive以前关闭,不然会crash。如官方文档描述:To summarize, your app needs to call the glFinish function to ensure that all previously submitted commands are drained from the command buffer and are executed by OpenGL ES. After it moves into the background, you must avoid all use of OpenGL ES until it moves back into the foreground.

可以在更新位置之前做一下app状态的检测:

1
2
3
if( (appState != UIApplicationStateBackground) && (appState != UIApplicationStateInactive)) {
// update location
}
2,关于CAAnimation动画


OpenGL提供了Core Animation的基础,它是底层的C接口,直接和iPhone,iPad的硬件通信,极少地抽象出来的方法。当app进入background模式时,所有基于Core Animation 的动画将自动停止,这是因为动画的渲染需要调用Open GL或者Core Graphics来实现UIKit层的变动,然后在applicationWillEnterForeground的时候重新启动即可。

参考:

http://onevcat.com/2013/08/ios7-background-multitask/

https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html

iOS面试总结

写在前面:iOS开发者水平良莠不齐,个人比较看中知识的全面性、编码规范性及学习能力,最近在面试iOS开发有些心得想和大家分享,整理如下:

一,基础篇

1,关键字

strong 该属性值对应 _strong 关键字,即该属性所声明的变量将成为对象的持有者;

weak 该属性对应 weak 关键字,与 weak 定义的变量一致,该属性所声明的变量将没有对象的所有权,并且当对象被破弃之后,对象将被自动赋值nil,delegate 和 Outlet 应该用 weak 属性来声明;

copy 与 strong 的区别是声明变量是拷贝对象的持有者;

assign 一般Scalar Varible用该属性声明,比如,int, BOOL;

static 类全局变量,只是在编译时候进行初始化,对于static变量,无论是定义在方法体里面 还是在方法体外面其作用域都一样;

extern extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个变量虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”,比如 extern NSString *aaa;

const

  • const把对象转换为一个常量,不可修改,比如const int bufSize = 512
  • const 对象默认为当前文件的局部变量,在全局作用域声明的const变量是定义该对象的文件的局部变量,不能被其它文件访问,可以加extern解决;

synchronised 通过对一段代码的使用进行加锁。其他试图执行该段代码的线程都会被阻塞,直到加锁线程退出执行该段被保护的代码段,也就是说@synchronized()代码块中的最后一条语句已经被执行完毕的时候;

2,应用程序的状态

主要考察对app当前状态及生命周期的了解程度。具体如下:

  • application:willFinishLaunchingWithOptions: - 这个方法是你在启动时的第一次机会来执行代码
  • application:didFinishLaunchingWithOptions: - 这个方法允许你在显示app给用户之前执行最后的初始化操作
  • applicationDidBecomeActive: - app已经切换到active状态后需要执行的操作
  • applicationWillResignActive: - app将要从前台切换到后台时需要执行的操作
  • applicationDidEnterBackground: - app已经进入后台后需要执行的操作
  • applicationWillEnterForeground: - app将要从后台切换到前台需要执行的操作,但app还不是active状态
  • applicationWillTerminate: - app将要结束时需要执行的操作

3,viewController的生命周期

单个

  • initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)
  • loadView:加载view
  • viewDidLoad:view加载完毕
  • viewWillAppear:控制器的view将要显示
  • viewWillLayoutSubviews:控制器的view将要布局子控件
  • viewDidLayoutSubviews:控制器的view布局子控件完成
    这期间系统可能会多次调用viewWillLayoutSubviews、viewDidLayoutSubviews 俩个方法
  • viewDidAppear:控制器的view完全显示
  • viewWillDisappear:控制器的view即将消失的时候
    这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法
  • viewDidDisappear:控制器的view完全消失的时候

多个跳转

  • 当我们点击push的时候首先会加载下一个界面然后才会调用界面的消失方法
  • initWithCoder:(NSCoder *)aDecoder:ViewController2 (如果用xib创建的情况下)
  • loadView:ViewController2
  • viewDidLoad:ViewController2
  • viewWillDisappear:ViewController1 将要消失
  • viewWillAppear:ViewController2 将要出现
  • viewWillLayoutSubviews ViewController2
  • viewDidLayoutSubviews ViewController2
  • viewWillLayoutSubviews:ViewController1
  • viewDidLayoutSubviews:ViewController1
  • viewDidDisappear:ViewController1 完全消失
  • viewDidAppear:ViewController2 完全出现

4,OC设计模式

MVC、 delegate、 通知、 KVO、 KVC、 单例、 工厂模式等。

需要分别描述下各自的使用场景,比如通知适合一对多?iOS 代理为啥要用weak修饰? iOS系统单例有哪些?

5,block和weak修饰符的区别

  • __block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型;
  • __weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型;
  • block对象可以在block中被重新赋值,weak不可以;

6,Objective-C中类别和类扩展的区别

Class extension常常被误解为一个匿名的category。它们的语法的确很相似。虽然都可以用来为一个现有的类添加方法和属性,但它们的目的和行为却是不同的,category和extensions的不同在于后者可以添加属性;另外类扩展添加的方法是必须要实现的;可以运行时给category通过objc_setAssociatedObjectobjc_getAssociatedObject添加和读取属性。

7,copy的使用

用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

  • 因为父类指针可以指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
  • 如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

8,LLVM 与 Clang

Clang 是一个 C++ 编写、基于 LLVM、发布于 LLVM BSD 许可证下的。C/C++/Objective C/Objective C++ 编译器,其目标(之一)就是超越 GCC。

Apple 使用 LLVM 在不支持全部 OpenGL 特性的 GPU (Intel 低端显卡) 上生成代码 (JIT),令程序仍然能够正常运行。之后 LLVM 与 GCC 的集成过程引发了一些不快,GCC 系统庞大而笨重,而 Apple 大量使用的 Objective-C 在 GCC 中优先级很低。此外 GCC 作为一个纯粹的编译系统,与 IDE 配合很差。加之许可证方面的要求,Apple 无法使用修改版的 GCC 而闭源。于是 Apple 决定从零开始写 C family 的前端,也就是基于 LLVM 的 Clang 了。

Clang 的特性:

  • 快,通过编译 OS X 上几乎包含了所有 C 头文件的 carbon.h 的测试,包括预处理 (Preprocess),语法 (lex),解析 (parse),语义分析 (Semantic Analysis),抽象语法树生成 (Abstract Syntax Tree) 的时间,Clang 是 Apple GCC 4.0 的 2.5x 快。(2007-7-25)
  • 内存占用小:Clang 内存占用是源码的 130%,Apple GCC 则超过 10x。
  • GCC 兼容性。
  • 设计清晰简单,容易理解,易于扩展增强。与代码基础古老的 GCC 相比,学习曲线平缓。
  • 基于库的模块化设计,易于 IDE 集成及其他用途的重用。

9,BAD_ACCESS如何调试

BAD_ACCESS的出现是因为访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。

  • 重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object;
  • 设置 Scheme Zombie 模式;
  • 设置全局断点快速定位问题代码所在行;
  • Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer;

二,实战篇

10,以下代码运行结果如何?

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}

结果:死锁

原因:

  • dispatch_sync在等待block语句执行完成,而block语句需要在主线程里执行,所以dispatch_sync如果在主线程调用就会造成死锁;
  • dispatch_sync是同步的,本身就会阻塞当前线程,也即主线程。而又往主线程里塞进去一个block,所以就会发生死锁;
  • MainThread等待dispatch_sync,dispatch_sync等待block,block等待 mainquen, mainquen等待MainThread,而MainThread等待dispatch_sync。这样就形成了一个死循环;

11,HitTest方法

场景:

View A位于上方,View B位于下方。View A上有Button 2 ,View B上有Button 1,如何穿透View A ,点击让Button 2响应?

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

  • 我们都知道,一个屏幕事件由响应链一步步传下去。这个函数返回的view就是可以让你决定在这个point的事件,你用来接收事件的view。当然,如果这个point不在你的view的范围,返回nil;
  • 如果hitTest返回的view不为空,则会把hitTest返回的view作为第一响应者
  • 如果hitTest返回的view为空,调用次序是从subview top到bottom,包括view本身,知道找到响应者为止。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
View A:
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
return nil;
}
else
{
return hitView;
}
}
1
2
3
4
5
6
7
8
9
10
11
View B:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 当touch point是在self.buttonFirst上,则hitTest返回self.buttonFirst
CGPoint btnPointInA = [self.buttonFirst convertPoint:point fromView:self];
if ([self.buttonFirst pointInside:btnPointInA withEvent:event]) {
return self.buttonFirst;
}
// 否则,返回默认处理
return [super hitTest:point withEvent:event];
}

12,GCD同步

如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

1
2
3
4
5
6
7
8
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});

13,NSOperation的特点

依赖:顾名思义,第一个方法用于添加依赖,第二个方法则用于移除依赖。需要特别注意的是,用addDependency:方法添加的依赖关系是单向的,比如[A addDependency:B];,表示 A 依赖 B,B 并不依赖 A ;

暂停:如果我们想要暂停和恢复执行 operation queue 中的 operation ,可以通过调用 operation queue 的 setSuspended: 方法来实现这个目的。不过需要注意的是,暂停执行 operation queue 并不能使正在执行的 operation 暂停执行,而只是简单地暂停调度新的 operation 。另外,我们并不能单独地暂停执行一个 operation ,除非直接 cancel 掉;

优先级: 我们只能够在执行一个 operation 或将其添加到 operation queue 前,通过 operation 的 setThreadPriority: 方法来修改它的线程优先级。当 operation 开始执行时,NSOperation 类中默认的 start 方法会使用我们指定的值来修改当前线程的优先级。另外,我们指定的这个线程优先级只会影响 main 方法执行时所在线程的优先级。所有其它的代码,包括 operation 的 completion block 所在的线程会一直以默认的线程优先级执行。因此,当我们自定义一个并发的 operation 类时,我们也需要在 start 方法中根据指定的值自行修改线程的优先级。

14, UITextView代理方法的使用

1
2
3
4
5
6
7
8
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView;
- (BOOL)textViewShouldEndEditing:(UITextView *)textView;
- (void)textViewDidBeginEditing:(UITextView *)textView;
- (void)textViewDidEndEditing:(UITextView *)textView;

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
- (void)textViewDidChange:(UITextView *)textView;
- (void)textViewDidChangeSelection:(UITextView *)textView;

15,如何把Model转换为字典

通过runtime的方式:

  • 首先,可以通过class_copyPropertyListprotocol_copyPropertyList方法来获取类的属性;

  • 比如获取某个类(obj)的属性列表:

1
2
unsigned int propsCount;
objc_property_t *props = class_copyPropertyList([obj class], &propsCount);
  • 通过property_getName方法就可以得到某个类属性的名字了
1
2
3
4
5
6
unsigned int propsCount;
objc_property_t *props = class_copyPropertyList([obj class], &propsCount);
for(int i = 0;i < propsCount; i++){
objc_property_t prop = props[i];
NSString *propName = [NSString stringWithUTF8String:property_getName(prop)];
}
  • 得到类属性的名称后,就可以知道该属性对应的类型了,如果是Object-C class,直接判断数据类型即可,比如NSString、NSArray、NSDictionary等。如果该属性的值对应的是派生类,则需要回到上一步重新解析,直到遍历完为止

16,常用的SVN/Git操作

  • SVN 分支与tag

    SVN官方推荐在一个版本库的根目录下先建立trunk、branches、tags这三个文件夹,其中trunk是开发主干,存放日常开发的内容;branches存放各分支的内容,比如为不同客户定制的不同版本;tags存放某个版本状态的标签,比如验收测试版、1.0.3版等。tags中的内容是存放不再修改的,tags通常只给管理员开放写权限。

  • SVN 回滚

    1). 改动没有被提交:直接svn revert something就行了;当something为目录时,需要加上参数-R(Recursive,递归),否则只会将something这个目录的改动。

    2). 改动已经被提交:可以使用svn diff -r HEAD:2500 [something],此处的something可以是文件、目录或整个项目。如果需要回滚到版本号2500:

1
svn merge -r HEAD:2500 something

17,APNS 、IAP、itms-services协议等

  • 询问关于推送、应用内付费以及企业帐号发布等知识;
  • 对AppFlyer、Adhoc、iTunes connect等了解使用情况。

18,算法题

1). 四个人夜间要过一座桥,每人走路速度不一样,过桥需要时间分别是1,2,5,10分钟。现在只有一只手电筒在过桥时必须带,同时只能两人过,如何安排能够让四人最快速度过桥?

2). 25匹马赛跑,每次只能跑5匹,最快能赛几次找出跑得最快的3匹马?

19,编码规范

不规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef enum {
UserSex_Man,
UserSex_Woman,
}UserSex;

@interface testA : NSObject

@property(nonatomic, strong) NSString *name;
@property (assign, nonatomic) int age;
@property (nonatomic, assign) UserSex sex;

-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;


- (void)doLogIn;

@end

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef NS_ENUM(NSInteger, TSTUserSexType) {
TSTUserSexTypeMan,
TSTUserSexTypeWoman,
};

@interface testA : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic ,assign) int age;
@property (nonatomic, assign) TSTUserSexType sex;

- (instancetype)initUserModelWithUserName:(NSString*)name withUserAge:(int)age;

- (void)doLoginWithSuccess:(void(^)(id response))success
failure:(void(^)(NSError *error))failure;

@end

三、高级篇

20,Autorelease对象什么时候释放?

对于每一个Runloop, 系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个象CallStack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。那什么是一个Runloop呢? 一个UI事件,Timer call, delegate call, 都会是一个新的Runloop。

Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。

21,深入理解runloop

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

问题一:NSURLConnection或NSStream指定RunLoop Mode的原因?
问题二:为何NSTimer在界面滚动时无响应?

参考回答一:
如果是在主线程,那么在滚动ScrollView或者TableView时,主线程的Run Loop会运行在UITrackingRunLoopMode模式,那么NSURLConnection或者NSStream的回调就无法运行,设置为NSRunLoopCommonModes,都可以保证NSURLConnection或者NSStream的回调可以被调用。
参考回答二:
当用户触摸界面时,主线程的run loop不再对timer事件进行处理。解决办法如下:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

参考:http://blog.ibireme.com/2015/05/18/runloop/

22,runtime的理解与使用

场景一:运行时给category添加属性,比如objc_getAssociatedObjectobjc_setAssociatedObject
场景二:动态获取类属性名称,比如class_copyPropertyList
场景三:消息转发:
第一步:动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveInstanceMethod: 或者 +resolveClassMethod:,前者处理实例方法调用,后者处理类方法调用。我们可以它们里面用 class_addMethod() 加入异常处理的方法,不过前提是我们以及实现了处理方法。
第二步:备用接收者
如果在第一步还是无法处理消息,则 Runtime 会继续调以下方法:

1
- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非 nil 的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是 self 自身,否则就会出现无限循环。当然,如果我们没有指定相应的对象来处理 aSelector,则应该调用父类的实现来返回结果。
第三步:完整转发
如果第二步:备用接收者还是未能处理好消息,那么接下来只有启用完整的消息转发机制了,这时候会调用以下方法:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的 NSInvocation 对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation 中,包括:selector、目标(target)和参数。我们可以在 -forwardInvocation: 方法中选择将消息转发给其它对象。完整实例如下:
ViewController示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (void)viewDidLoad {
[super viewDidLoad];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"

[self performSelector:@selector(unknownMethod)];
[self performSelector:@selector(unknownMethod2)];
[self performSelector:@selector(unknownMethod3)];

}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"unknownMethod"]) {
class_addMethod(self.class, @selector(unknownMethod), (IMP) dealWithExceptionForUnknownMethod, "v@:");
}
return [super resolveInstanceMethod:sel];
}

// Deal with unknownMethod.
void dealWithExceptionForUnknownMethod(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd); // Print: <ViewController: 0x7ff96be33e60>, 0x1078259fc
}

// Deal with unknownMethod2.
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *selectorString = NSStringFromSelector(aSelector);
if ([selectorString isEqualToString:@"unknownMethod2"]) {
return [[RuntimeMethodHelper alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

// Deal with unknownMethod3.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([RuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[[RuntimeMethodHelper alloc] init]];
}
}

RuntimeMethodHelper示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*RuntimeMethodHelper.h*/
@interface RuntimeMethodHelper : NSObject
- (void)unknownMethod2;
- (void)unknownMethod3;
@end

/*RuntimeMethodHelper.m*/
@implementation RuntimeMethodHelper
- (void)unknownMethod2 {
NSLog(@"%@, %p", self, _cmd); // Print: <RuntimeMethodHelper: 0x7fb61042f410>, 0x10170d99a
}

- (void)unknownMethod3 {
NSLog(@"%@, %p", self, _cmd); // Print: <RuntimeMethodHelper: 0x7f814b498ee0>, 0x102d79929
}
@end

场景四:动态创建类和对象,例如:

1
2
3
4
5
6
// 创建类实例
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置创建类实例
id objc_constructInstance ( Class cls, void *bytes );
// 销毁类实例
void * objc_destructInstance ( id obj );

场景五:IOS中如何Hook消息?
class_replaceMethod使用该函数可以在运行时动态替换某个类的函数实现,截获系统类的某个实例函数。

场景六:Method Swizzling
例如,我们想跟踪在程序中每一个view controller展示给用户的次数,在每个view controller的viewDidAppear中添加跟踪代码,但是这太过麻烦。这种情况下,我们就可以使用Method Swizzling。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import <objc/runtime.h> 

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

@end

关于load

  • load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用;
  • load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性;
  • initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用;
  • Swizzling应该总是在dispatch_once中执行,确保代码只被执行一次。

23,lib库的编写与使用

  • 如何保证lib库中category文件的正常读取?
  • 如何保证lib对armv7s的支持?
    Build Setting -> Architectures, 添加 $(ARCHS_STANDARD)和 armv7s
  • 如何合并不同平台的lib库?
  • iOS 第三方库冲突的如何处理?可以对lib库内的文件修改重修打包吗 ?
    实例,比如libHChatSDK.a中包含了JSONKit库,现有工程中同样包含了JSONKit库,这样在当前工程中加入libHChatSDK.a时会引起duplicate symbols for architecture armv7的编译错误,那么我们可以重新编辑libHChatSDK.a,删除JSONKit.o然后再打包合并即可,具体命令如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 1,新建armv7s目录
    mkdir ~/desktop/armv7s
    # 2,分离出armv7s平台
    lipo libHChatSDK.a -thin armv7s -output libHChatSDK-armv7s.a
    # 3,解压 libHChatSDK.a
    cd armv7s && ar xv libHChatSDK-armv7s.a
    # 4,查看库中所包含的文件列表
    ar -t armv7/libHChatSDK-armv7s.a
    # 5,找到JSONKit.o并删除
    rm JSONKit.o
    # 6,重新打包libHChatSDK-armv7s.a
    ar rcs libHChatSDK-armv7s.a armv7s/*.o
    # 7,重复第3步,确认JSONKit已经被删除
    # 8,按照1-7分别生成armv7平台、arm64平台及模拟器使用的x86_64、i386平台
    # 9,最后用lipo -c 命令合并各个平台即可

常用的lib命令:

1
2
3
4
5
6
7
8
# 用 lipo info 查看静态库支持的平台
$ lipo libname.a -info
# 用 lipo remove 参数来删除平台
lipo libname.a -remove x86_64 -output libname1.a
# 用 lipo create 将两个不同平台的库合并到一起
$ lipo -create libname1.a libname2.a -output libname3.a
# 用 lipo thin 参数来分离平台
lipo libname.a -thin armv7 -output armv7/libname-armv7.a

24,ARM64与ARMv7

Arm处理器,因为其低功耗和小尺寸而闻名,几乎所有的手机处理器都基于arm,其在嵌入式系统中的应用非常广泛,它的性能在同等功耗产品中也很出色。

Armv6、armv7、armv7s、arm64都是arm处理器的指令集,所有指令集原则上都是向下兼容的,如iPhone4S的CPU默认指令集为armv7指令集,但它同时也兼容armv6指令集,只是使用armv6指令集时无法充分发挥其性能,即无法使用armv7指令集中的新特性,同理,iPhone5的处理器标配armv7s指令集,同时也支持armv7指令集,只是无法进行相关的性能优化,从而导致程序的执行效率没那么高。

需要注意的是iOS模拟器没有运行arm指令集,编译运行的是x86指令集,所以,只有在iOS设备上,才会执行设备对应的arm指令集。

iOS设备与ARM平台分布如下图:

参考:

http://www.jianshu.com/p/2e7ae4457083

http://www.iswifting.com/2015/07/26/71/

http://blog.leichunfeng.com/blog/2015/05/31/objective-c-autorelease-pool-implementation-principle/

几款mac小工具推荐

iTerm2

Mac下的命令行工具五花八门,选择一款便捷的扩展性强的工具对工作效率提升十分重要。在此推荐iTem2 + oh-my-zsh,Mac自带的shell工具Terminal是基于bash实现,而且zsh在兼容bash的基础上又提供了强大的扩展插件,二者方便切换,只需一条命令即可:exec bash(切换bash),exec zsh(切换zsh)。oh my zsh是一款开源的ZSH插件,可以方便的自定义主题,对SVN/GIT有很好的支持,有强大的开源开源插件,如autojumpsvn等提供支持。附图如下:

快捷键配置如下:

  • 设置Left option 为 +Esc,可以单词为单位左右移动光标:向后 Option + B(back) ; 向前 Option + F(front)
  • Option + Q 清除当前行
  • Command + K 清除当前面板
  • Option + P 上一个输入过的命令
  • Option + N 后一个输入过的命令
  • 向上箭头 :上一个执行过的命令
  • 向下剪头 :后一个执行过的命令
  • Control + A 光标最前
  • Control + E 光标最后

aTEXT

经常使用命令行的同学,有时不得不重复一些常用命令,比如代码的提交、更新,API的测试等,aText就是一款提供命令行或常用语句缩写的工具。

Hexo

hero是一款极为方便的基于nodejs的静态博客工具,只需几行命令即可搞定,具体请参考这里。hexo还支持一键发布到GitHub Pages, Heroku 或其他网站,另外官方推出诸多插件,比如hexo-admin后台管理可以方便的添加tag、分类等。

其他

比如常用的markdown编辑器Typara,效率工具番茄土豆,以及奇妙清单等对工作效率的提升也有很大帮助。更多请关注来自知乎的热门推荐

常用的SVN命令小结

1,基础命令

1
2
3
svn co repository_url // check out respoitory
svn ci -m "your comments" // commit files
svn up repository_url // update files

2,branch & tag

所谓打tag,要从SVN官方推荐的目录结构说起了。SVN官方推荐在一个版本库的根目录下先建立trunk、branches、tags这三个文件夹,其中trunk是开发主干,存放日常开发的内容;branches存放各分支的内容,比如为不同客户定制的不同版本;tags存放某个版本状态的标签,比如验收测试版、1.0.3版等。branhces和tags本质没有区别,都是通过svn copy方式建立的,差异在于通常branches中的内容是需要继续修改或开发的,tags中的内容是存放不再修改的,这一般通过权限设置来解决,tags通常只给管理员开放写权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 新建分支
svn copy master_repository_url branch_repository_url -m "your comments"

// 新建空白分支
svn mkdir branch_repository_url

// 删除分支
svn rm branch_repository_url -m "your comments"

// 新建tag
svn copy master_repository_url tag_repository_url -m "your comments"

// 删除tag
svn rm tag_repository_url -m "your comments"

// 查看branches
svn ls ^/branches --verbose

3,回滚

3-1),改动没有被提交

这种情况下,使用svn revert就能取消之前的修改。svn revert用法如下:当something为单个文件时,直接svn revert something就行了;当something为目录时,需要加上参数-R(Recursive,递归),否则只会将something这个目录的改动。

3-2),改动已经被提交

这种情况下,用svn merge命令来进行回滚。先运行svn up保证拿到最新的版本,然后svn log查看并找到要回滚的版本号,如果想要更详细的了解情况,可以使用svn diff -r HEAD:2500 [something],此处的something可以是文件、目录或整个项目。

如果需要回滚到版本号2500:

1
svn merge -r HEAD:2500 something

为了保险起见,再次确认回滚的结果:

1
svn diff -r HEAD:2500 [something]

发现无误,提交即可。

3-3),分支与主干的合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 分支合到主干 cd trunk
svn merge -r <revision where branch was cut>:<revision of trunk> svn://branch/path

# 分支当前版本为4847,想把4825到4847间的改动merge到主干
# cd trunk
svn merge -r 4825:4847 svn://branch/path
svn ci -m "merge branch changes r4835:4847 into trunk"

# 主干合到分支 cd branch
# 在r23创建了一个分支,trunk版本号更新到了25,想把23-25之间的改动merge到分支
svn merge -r 23:25 svn://trunk/path
svn ci -m "merge trunk changes r23:25 into my branch"

# cd trunk
# 查看当前Branch中已经有那些改动已经被合并到Trunk中
svn mergeinfo svn://branch/path

# cd trunk
# 查看Branch中那些改动还未合并
svn merginfo svn://branch/path --show-revs eligible

3-4),merge分支B到分支A

1
2
3
4
5
step1: Checkout URL A
# cd branch A
step2: merge URL B to your working copy of A
svn merge -r 10:HEAD http://branch-b .
step3: Commit A

4,冲突提示

1
2
3
4
5
6
(p) postpone          暂时推后处理,我可能要和那个和我冲突的家伙商量一番
(df) diff-full 把所有的修改列出来,比比看
(e) edit 直接编辑冲突的文件
(mc) mine-conflict 如果你很有自信可以只用你的修改,把别人的修改干掉
(tc) theirs-conflict 底气不足,还是用别人修改的吧
(s) show all options 显示其他可用的命令

5,svn符号

1
2
3
4
5
6
U:表示从服务器收到文件更新了
G:表示本地文件以及服务器文件都已更新,而且成功的合并了
其他的如下:
A:表示有文件或者目录添加到工作目录
R:表示文件或者目录被替换了.
C:表示文件的本地修改和服务器修改发生冲突

6,patch

有时同事A做的修改需要同事B去Review,同事C去提交。使用patch工具可以很好的决代码传递。
6-1).生成patch:
同事 A 运行如下命令生成 patch:

1
svn diff > aaa.patch

6-2).应用patch:
同事 B 运行如下命令应用 patch:

1
patch –p0 < aaa.patch

6-3).去除patch,恢复旧版本
当他 review 完代码,想删除该 patch 时, 可运行:

1
patch -RE -p0 < aaa.patch

-p0,是“当前路径”
-p1,是“上一级路径”

参考:http://stereointeractive.com/blog/2009/02/17/svn-merge-trunk-changes-to-your-branch/