swift学习

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
53
54
55
56
//
// SwiftTips.swift
// SwiftSum
//
// Created by yangyuan on 2016/10/5.
// Copyright © 2016年 huan. All rights reserved.
//
import UIKit
// MARK: - 让人眼前一亮的初始化方式
//参考链接http://swift.gg/2016/09/23/swift-configuring-a-constant-using-shorthand-argument-names/
class Initss {
/*
声明常量后,在一个紧接着的闭包中进行初始化,而不是之后在 viewDidLoad 或其他类似的方法中进行设置,这在 Swift 中是很常见的写法(也确实是一种不错的写法!)。
但是觉得在闭包中多命名一个 UIView 很难看,上面代码中有 purpleView 和 view 两个 UIView 实例,那么 view 是不是应该命名成 purpleView
*/
let purpleView: UIView = {
// 在此初始化 view
// 直接叫 "view" 真的好吗?
let view = UIView()
view.backgroundColor = .purple
return view
}()
// MARK: - 用位置参数(positional references)来初始化 Swift 常量
let yellowView: UIView = {
$0.backgroundColor = .yellow
return $0
// 确保下一行的括号内要传入 UIView()
}(UIView())
// MARK: - 有个叫 Then 的库更赞,能够写出可读性更好的代码:
let lable = UILabel().then {
$0.textColor = .black
$0.text = "Hello, World!"
}
}
public protocol Then {}
extension Then where Self: Any {
public func then(_ block: (inout Self) -> Void) -> Self {
var copy = self
block(&copy)
return copy
}
}
extension Then where Self: AnyObject {
public func then(_ block: (Self) -> Void) -> Self {
block(self)
return self
}
}
extension NSObject: Then{}

iOS-Wiondows认证

三行代码让你通过UIWebview中遇到的NTLM验证

Windows认证分为NTLM认证 和Kerberos v5身份认证,这里只列出NTLM认证的情况。在移动开发中细分为两种情况(iOS为例)

访问的API是带NTLM认证+From身份认证

其中NTLM认证只是作为一种加强方式(只是微软环境才有的),或者受限于服务端的其他环境需要。那么在移动端该层认证有点多余,并且会影响效率。因为每次访问该接口都是先返回401质询后,提取出scheme,若为NTLM再设置其认证信息,如设置其domain信息等。

如访问xx移动登录接口,就一个典型的带了windows认证后又要传Form参数的情况

http://xxx.xxx.xx:8600/MobileService/MobileHandler.ashx?action=Login

Form参数列表这里就不一一列出
如何获取需要NTLM api的数据?若使用ASIHttp的,只需要设置 request的username和password即可。AFHttpNetwork同理,只是设置credentials的时候复杂些。但是要用原生的NSURLConnection 或者NSURLSession 请参见下文中访问带NTLM验证的网页。从网络抓包结果来看:访问一次接口要先失败一次第二次传身份信息才成功,某种程度上导致效率较低。

访问的资源是html

即html需要先过了NTLM验证才能访问,如访问路由器的页面192.168.1.1,会默认弹出一个认证框出来。如果是我们自己开发或者集成带有windows认证的页面,就无法自动弹窗出来,因为弹窗属于窗口级别、在我们开发的程序中不能越级访问系统资源,需要我们手动实现起认证功能。这里用iOS原生的sdk方法说明。

Step 1,
1
2
3
4
5
NSURLRequest *req =[NSURLRequest requestWithURL:reqUrl];
NSURLConnection *connect =[NSURLConnection connectionWithRequest:req delegate:self];
[connect start];

构建一个NSURLRequest请求对象,只需要一个NSURL地址即可,这里reqUrl可以写为

[NSURL UrlWithString:@”http://需要过windows认证的页面地址”]

Step 2,

以NSURLRequest的实例对象构建NSURLConnection实例对象,并设置Connection的委托方法,可以理解为回调方法,当请求发送到服务端后,服务端返回数据后就会进入对应的回调方法,返回该连接是否需要认证、或者其他然后再进行处理。

进入NSURLConnection的回调方法后,判断其认证方式

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
//处理身份认证(void)connection:(NSURLConnection *)connectiondidReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if ([challenge previousFailureCount] == 0)
{
//若是windows认证方式,iOS的sdk会自动进入该回调方法,
//在没有尝试认证失败的情况下,设置Credential的参数
        //USERNAME
        //PASSWORD
            [[challenge sender]useCredential:[NSURLCredential credentialWithUser:@"USERNAME"password:@"PASSWORD"persistence:NSURLCredentialPersistencePermanent]forAuthenticationChallenge:challenge];
            NSLog(@"...1");
    }else
{
若没有认证,则取消AuthenticaionChllenge的操作
        [[challenge sender]cancelAuthenticationChallenge:challenge];
    }
}
//处理访问的网页,在改方法里面再用webview载入request请求后,就可以实现了访问带有windows认证的网页。
-(void)connection:(NSURLConnection *)connectiondidReceiveResponse:(NSURLResponse *)response;
{
    NSLog(@"received response viansurlconnection");
   
    /** THIS IS WHERE YOU SET MAKE THE NEWREQUEST TO UIWebView, which will use the new saved auth info **/
#pragma mark -真实访问的url地址
    NSURLRequest *urlRequest = [NSURLRequestrequestWithURL:[NSURL URLWithString:@"http://192.168.90.130/_layouts/ReportServer/RSViewerPage.aspx?rv%3aRelativeReportUrl=%2fReportLib%2fReports%2f%25E8%25BF%2590%25E8%2590%25A5-%25E5%25AE%25A2%25E8%25BF%2590%25E9%2587%258F.rdl"]];
    [_webView loadRequest:urlRequest];
}

以上的代码适用于iOS7或者iOS8,但是在iOS9以上,苹果废弃了NSURLConnection,那么我们需要做的就是将NSURLConnection的方式迁移到NSURLSession的回调方法即可。

show me the code

构建NSURLSession请求,设置委托回调
1
2
3
4
5
6
NSMutableURLRequest *webReq = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com:8000/todolist.aspx"]];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *conn =[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [conn dataTaskWithRequest:webReq];
[task resume];
在回调鉴权中填入相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler {
NSString *authMethod = [[challenge protectionSpace] authenticationMethod];
//下面的鉴权没有判断是哪种鉴权方法,精细点可以只针对哪种认证做处理
if ([challenge previousFailureCount] == 0) {
_authed = YES;
/* SET YOUR credentials, i'm just hard coding them in, tweak as necessary */
NSURLCredential *cred = [NSURLCredential credentialWithUser:@"USERNAME"
password:@"PASSWORD"
persistence:NSURLCredentialPersistenceNone];
[[challenge sender] useCredential:cred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
NSLog(@"Finished Challenge");
} else {
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
webview载入request
1
2
3
4
5
6
7
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://xxxx:8000/mERP/MobileWorkflow/todolist.aspx?usercode=USERNAME&password=PWD"]];
[_myWebView loadRequest:urlRequest];
}

写在最后

以上设置解决api访问级别的应该没有问题,但是测试在iOS9.3+webview载入request依然会有问题。可以构建一个NSURLComponets来解决这个问题。鉴权那套都不需要了

1
2
3
4
NSURLComponents *componet = [NSURLComponents componentsWithString:@"http://xxxx.xxx.com:8000/mERP/MobileWorkflow/todolist.aspx?usercode=USERNAME&password=PASSWORD"];
componet.user = @"USERNAME";
componet.password = @"PASSWORD";
[_myWebView loadRequest:[NSURLRequest requestWithURL:componet.URL]];

就是这么任性

总结:

不管是利用三方框架还是sdk的自带方法,用了windows认证的情况都是存在两次连接的情况的。至于在pc中是否也是两次连接我这里没有做详细研究。

补充:

在Android开发中实现了访问NTLM认证网页或者带windows认证接口的问题,现在在xx地铁中安卓端的代码就是采用的我在文中列出的解决办法。

至于访问地铁报表页面或者访问公文文档库页面,显示自动匹配的问题我们是通过Client的UA字符串的办法来解决的,即让安卓客户端欺骗Sharepoint服务器它是iOS设备,并且这里UA字符串是我测试了iOS6,iOS7的各个硬件设备提取出来的。

Mozilla/5.0 (iPhone; CPUiPhone OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko)Version/6.0 Mobile/10B329 Safari/8536.25

iOS后台长活及逆向验证

之前我有问过同事(RocketChild)火箭闹钟的长后台,点击home键后,app一直长活,她提示他们是反编译了”怪物闹钟”得到的启发,然而在没有去反编译怪物闹钟的时候我猜测是播放静音文件,抓包看过没有voip的长连接。接下来我反编译过后得出的结论印证了我的猜测。

##App常见的后台长活处理

  • Voip voip实现长连接
    XMPPFramework 有个属性就是开启后台长活的
  • 后台下载(Server push notification iOS7+)
  • 定位服务(显著位置更新)
  • 播放静音文件 (相比之前实现成本较低,通过合理设置audio的声音通道不会污染其他声音源)

播放静音文件 实现iOS应用在进入到后台之后,依旧可以执行任务,并不受时间的限制

swift实现播放静音文件

播放静音文件的demo

oc实现播放静音文件

思路:

  1. 使用UIApplication对象的beginBackgroundTaskWithExpirationHandler申请后台执行任务,该任务只有大概3分钟的运行时间
  2. 应用申请到后台执行任务后,使用NSTimer开启一个定时任务,主要负责监控应用剩余的后台可执行时间,当可用的时间少于一个值时,播放一段默声音乐,然后调用UIApplication对象的endBackgroundTask方法将之前申请的后台执行任务结束掉,最后再重新申请一个后台执行任务,这样就可以实现后台不限时执行任务了
  3. 应用在后台播放音乐,需要开启Background Modes,然后勾选Audio and AirPlay即可

注:应用在后台运行的过程中重新申请后台执行任务是无效的,通过在网上查找资料,播放音乐可以让应用进入到一个假前台的状态,此时重新申请后台执行任务是有效的,如此循环n次,就可以获得大约3n的后台执行时间,从而达到后台无限时执行任务

Show me the code(oc)

1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSError *error = nil;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
    
    [NSTimer scheduledTimerWithTimeInterval:60 target:self selector:@selector(tik) userInfo:nil repeats:YES];
    // Override point for customization after application launch.
    return YES;
}

在tik方法中判断下后台保留的时间是多少秒,这里为了稳妥,设置为61s。

1
2
3
4
5
6
7
8
9
10
- (void)tik{
    
    if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 61.0)
    {
        [self longTimeTask];
        [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
    }
    
}

如何避免上架过程中被reject掉,闹钟类App中有播放声音文件的需求,可以在App的plist主动声明。

那么如果我们一开始没有猜中开头和结尾常规的方法就是沿用套路。

套路

  • UI层分析

    在日常开发中我们可以用Xcode自带的工具进行视图层次的摸索,这里省略。那么如何去分析别人家的app,我们需要借助的工具Reveal。
    Reveal如何在越狱设备上调试

  • 网络层分析

    青花瓷大法:Charles用来抓网络传输数据包,如丝般顺滑。基础功能这里也不用介绍了,值得注意的地方这里列举两个
    1.如果要抓取https的包,我们需要配置SSL设置
    2.上面那步骤完成后,有小伙伴说在真机上面还是不能抓取https的数据,要么你的Charles版本有点老,在iOS9中是有开启ATS功能的,普通的App为了向下兼容都是默认关闭了,在旧版本的Charles中对开启ATS功能的app确实无法抓包,在目前的版本中已经突破了这个限制。
    Charles抓包的原理
    本质还是”中间人”攻击。需要抓取https的包你的设备上还需要信任由Charles下发的证书。


逆向套路

让目标程序破茧而出 -- dumpdecrypted
1.运行时分析 -- cycript
2.追踪神器 -- logify
3.反汇编工具 -- hopper
4.断点调试工具 -- lldb+debugserver
5.注入工具 -- insert_dylib + install_name_tool

  • 砸壳

    apple市场里面的应用默认就加了一道屏障,需要自备砸壳大法。
    砸壳参考
    这样岂不是太繁琐了,有没有舒爽的姿势,有的!
    我会告诉你用pp助手下载越狱市场里面的app都是砸过壳的吗?

  • 砸壳后我们能干点什么(class-dump & 找线索)

    class-dump 使用相信大家都比较熟悉,这里也略过。拿到.h文件后我们可以看下类中的方法以及成员变量;拿到的.h 文件为后面配置 theos的一个插件logify来自动生成Tweak.xm
    keyword:mute.mp3 silent.wav 等
    分析app的plist文件也可以获得它有哪些声明的文件

  • Cycript 运行时分析(这里参考微信读书团队分享中的例子作为案列)

    砸完壳之后,我们再 dump出头文件,但是微信的类太多了,头文件有几百个,如此多的头文件,让人眼花缭乱,所以要找到突破口,我们得缩小范围,我喜欢用的思路是从 ui入手,先找到微信对话界面的 controller,然后再追踪 controller中对应的消息处理函数。
    这样第二个工具 cycript 隆重出场了 ,它也是个手机端 的工具,用于查看 app运行时数据,大伙儿可以通过 cydia安装,安装完之后,ssh到越狱手机的终端。
    先找到微信的进程id:ps aux | grep WeChat再执行:cycript -p 微信的pid

微信app从UI树分析

Cycript打印UI树

1 找到当前的进程id

1
ps aux | grep Appname

2 cycript -p Target app pid

1
UIApp.keyWindow.recursiveDescription().toString()

打印的是当前的ui树,随便找一个节点(树的中间,为什么要在中间,大家可以思考下 ),copy它的内存地址,例如 0x14da3f000

1
[#内存地址 nextResponder]

直到找到一个ViewController,然后再去.h里面 找到对应的类头文件。

  • Hopper

    然而真的要看到.h里面的方法实现,我们还是得借助IDA类似的工具来进行分析,简单些的就使用Hopper进行iOS包中的二进制文件分析,在工具里面点击if(b)f(x)可以查看,某个方法的伪代码实现,这里虽然是伪代码,但是其实可以看见许多关键信息了,结合我们自己的经验基本可以得出想要模仿的 app的关键信息点了(如果对方写法不按照规范来,不按照常理出牌,我们也没法!)如视频播放某个app什么格式都能播放,我们主要分析其解码用了什么框架,是否用了三方的东西,或者用了哪个三方的库,基本可以构面了,然后再模仿下就好了。

怪物闹钟

火箭闹钟

基于播放静音文件的线索我在Hopper里面搜索下关键字,省去了我去用lldb+debugserver的步骤。其实这里只是一个抛砖引玉的过程,我觉得我们在做功能的同时如果能从反方向的角度来思考下问题。

####写在最后关于用Theos编写、改写tweak的时候要注意的地方有:
1,Tweak的control 里面的name 必须要和plist名字对应

改写一个tweak

2,Tweak.plist的
你想要hook的app的签名和tweak中的签名要匹配,否则后面执行make package install安装后没有效果

3,Tweak.wx 加入了%new新方法你需要在你导入的类.h文件中添加你新加方法的声明

Tweak新增加方法需要在引入头文件中声明

4,Makefile的书写,疑惑的地方是我自己创建了个theos的替身,执行的时候报错,我下载了个微信红包的Tweak.xm替换自己的theos替身就好了,应该是文件夹权限的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
THEOS_DEVICE_IP = 172.16.0.115
THEOS_DEVICE_PORT = 22
ARCHS = armv7 arm64
TARGET = iphone:latest:9.0
include theos/makefiles/common.mk
TWEAK_NAME = GodTest
GodTest_FILES = Tweak.xm
GodTest_FRAMEWORKS = UIKit Foundation
include $(THEOS_MAKE_PATH)/tweak.mk
after-install::
install.exec "killall -9 GodTest"

Makefile注意事项
测试的Tweak工程和Xcode工程打包

参考链接:
http://dev.qq.com/topic/5791da152168f2690e72daa4

Discover相关笔记

#Discover

  • 获取选择的城市

1
2
3
4
5
6
7
8
9
10
theCityArray = [[NSMutableArray alloc] initWithCapacity:10];
[ServiceRouter requestHotCities:0.
                          longitude:0.
                              block:^(RequestResultType type,NSArray* cityArray){
                                  if (REQUEST_SUCCEEDED(type)) {
                                      [temArray addObjectsFromArray:cityArray];
                                  }
                                  
                              }];
  • 在地图的Delegate回调中禁用LocationManager中事件更新

    否则会造成updatelocation方法一直回调,虽然在后面的call api之前被滤掉了,但是mapview的delegate方法一直回调,徒增耗电量。
  • 搜索功能

    相对独立,只是搜索地址,点击后跳转到搜索的地点上。
  • 时光机 复用之前的逻辑

    点击按钮后弹出时光机的选项
  • 点击定位当前

    locationmanager中的方法的触发,获得位置回调后调用方法停止

  • ##手指移动地图

    平移、 对角线移动

    产品给出的是屏幕移动3/4后才开始请求

移动可能会造成中心的点的变化,通过取对角线的中点和上次中点的变化,可以判断是否发生了变化。如果只对中心点进行放大缩小,中心点的位置就没有变化,这个时候再结合地图中的span属性进行判断就能判断是缩放还是平移了。这里总体上面还是迁移原来老的逻辑。

  • 缩放

    放大缩小每次都是上次的一个比值,1/1.2 1*1.2,rate = 1.2
    无论何种移动,都是在regionDidChanged回调方法中去处理

  • Workflow
    1,Discover 首次加载的时候调用定位locationmanager的starlocate方法,获取到位置后停止。viewDidload之前设置了一个默认的地址及北京,然后在开始进行location current的地址回调方法。

    首次定位无论如何都去发送网络请求(之前的代码逻辑中有个bool值用于判断,是否是首次请求,重构过程中可以考虑是否还用这个标识)。

2,缩放每次都进行网络请求
3,移动过程中之前的判断是经度、或者纬度发生变化是0.001的差值时候才发生网络请求。

Charles如何抓取https数据包的

Swiftweekly

  • ####Span的解释

    大概意思就是span表示的是regoin的范围。它有两个字段一个是latitudeDelta,表示纬度范围,南纬和北纬加一起应该有180度,所以它的范围应该是大于0度,小于等于180度;另一个是longitudeDelta,表示经度范围,东经和西经加一起应该有360度,所以它的范围应该是大于0度,小于360度。看完了上面的解释其实还不是很理解,需要用代码来验证一下上面的解释。那我就举一个例子,让地图正好显示中国地图全部。我们先来看看中国地图的经纬度范围,百度搜索“中国经纬度范围”,得出如下结果。

参考链接

*对象引用关联比如给AlertView添加一个东西

通过runtime来添加类变量的方式并不是Category的功劳,objc_setAssociatedObject 和 objc_getAssociatedObject在任何类型的类里都可以把一个变量绑定个一个对象,实际上起到的是一种绑定对象传值的作用,但这并不是说通过Category是能添加变量的,严格意义上来讲单纯的通过Category是不能的,只能说runtime和Category两者结合着还实现的。runtime并不属于Category的功能范畴

1
2
3
4
5
6
@interface Teacher : NSObject
{
NSUInteger age;
}
@end

光有个年龄还不能满足对teacher的描述,我想加个profession实例来存teacher的专业。直观的想法是子类化Teacher,其实也可以用类别。
你需要了解一下 runtime 编程知识,关注一下 objc_setAssociatedObject 和 objc_getAssociatedObject 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// Teacher+Profession.m
//
#import "Teacher+Profession.h"
#import <objc/runtime.h>
const char *ProfessionType = "NSString *";
@implementation Teacher (Profession)
-(void)setProf:(NSString*)prof
{
objc_setAssociatedObject(self, ProfessionType, prof, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)prof
{
NSString *pro = objc_getAssociatedObject(self, ProfessionType);
return pro;
}

现在就可以通过setProf: 和 prof 来存取 teacher 的 profession 值了。

/
消息转发要点
/
如图所示

从上面我们可以发现,在发消息的时候,如果 selector 有对应的 IMP ,则直接执行,如果没有,oc 给我们提供了几个可供补救的机会,依次有

  • resolveInstanceMethod 、
  • forwardingTargetForSelector、
  • forwardInvocation。
    Aspects 之所以选择在 forwardInvocation 这里处理是因为,这几个阶段特性都不太一样:resolvedInstanceMethod 适合给类/对象动态添加一个相应的实现,
    forwardingTargetForSelector 适合将消息转发给其他对象处理,
    相对而言,forwardInvocation 是里面最灵活,最能符合需求的。
    因此 Aspects 的方案就是,对于待 hook 的 selector,将其指向 objc_msgForward / _objc_msgForward_stret ,同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hook 住 forwardInvocation 函数,使他指向自己的实现。按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward / _objc_msgForward_stret ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于 forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation 函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。

gcd学习

GCD

Queue

GCD有三种queue.

  • main queue: 主线程队列。是一个串行队列。一般用来更新UI。
  • global queue: 全局队列,是一个并行队列。使用方法相信大家都知道。
1
2
3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self dosomething];
});

上面的意思就是开启一个异步线程,在全局队列中执行。

  • custom queue:自定义队列。有两种自定义队列。
1
2
dispatch_queue_t serial_queue = dispatch_queue_create("com.reviewcode.www", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrent_queue = dispatch_queue_create("com.zangqilong.www", DISPATCH_QUEUE_CONCURRENT);

serial_queue即是我自定义的一个串行队列。上面提到的主线程队列也是一个串行队列。
concurrent_queue是我自定义的一个并行队列。上面提到的global queue就是一个并行队列。
现在我们来讨论2个问题。

  1. dispatch_async里使用串行队列和并行队列的效果。
  2. dispatch_sync里使用串行队列和并行队列的效果。
    串行队列和并行队列的创建如下。
1
2
serial_queue = dispatch_queue_create("com.reviewcode.www", DISPATCH_QUEUE_SERIAL);
concurrent_queue = dispatch_queue_create("com.zangqilong.www", DISPATCH_QUEUE_CONCURRENT);

讨论的问题

在dispatch_async使用串行队列

看代码。

1
2
3
4
5
6
7
8
9
10
11
12
- (void)testSerialQueueWithAsync
{
for (int index = 0; index < 10; index++) {
dispatch_async(serial_queue, ^{
NSLog(@"index = %d", index);
NSLog(@"current thread is %@", [NSThread currentThread]);
});
}
NSLog(@"Running on main Thread");
}

然后在viewDidLoad()方法里打印。打印结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
index = 0
Running on main Thread
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 1
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 2
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 3
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 4
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 5
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 6
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 7
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 8
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}
index = 9
current thread is <NSThread: 0x7fad5be0f020>{number = 2, name = (null)}

打印结果的几个特征。

  1. 在dispatch_async使用的所有Thread均为同一个Thread。因为他们的指针地址完全相同。
  2. 输出结果是按顺序输出,符合我们对串行队列的期待。即FIFO。先进先出原则。
  3. Running on main Thread这句话并没有在最后执行,而是会出现在随机的位置,这也符合我们对dispatch_async的期待,因为他会开辟一个新的线程执行,不会阻塞主线程。
    ok,让我们测试下一个。

在dispatch_async中使用并行队列

看代码。

1
2
3
4
5
6
7
8
9
10
11
- (void)testConcurrentQueueWithAsync {
for (int index =0; index<10; index++) {
dispatch_async(concurrent_queue, ^{
NSLog(@"index = %d", index);
NSLog(@"current thread is %@", [NSThread currentThread]);
});
}
NSLog(@"Running on main Thread");
}

打印结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
index = 1
index = 5
Running on main Thread
index = 2
index = 6
index = 7
index = 3
index = 0
index = 8
index = 4
index = 9
current thread is <NSThread: 0x7fded2a17e20>{number = 2, name = (null)}
current thread is <NSThread: 0x7fded1508e40>{number = 3, name = (null)}
current thread is <NSThread: 0x7fded290d6e0>{number = 5, name = (null)}
current thread is <NSThread: 0x7fded2a17e60>{number = 4, name = (null)}
current thread is <NSThread: 0x7fded1510470>{number = 8, name = (null)}
current thread is <NSThread: 0x7fded17050f0>{number = 6, name = (null)}
current thread is <NSThread: 0x7fded1704b30>{number = 10, name = (null)}
current thread is <NSThread: 0x7fded2806fe0>{number = 7, name = (null)}
current thread is <NSThread: 0x7fded2900e10>{number = 9, name = (null)}
current thread is <NSThread: 0x7fded2b01060>{number = 11, name = (null)}

打印结果的特征如下:

  1. 输出的结果是乱序的,说明我们的输出语句是并发的,由多个线程共同执行的。
  2. Running on main Thread这句话依然没有被阻塞,直接输出了。
  3. 每次打印语句的Thread均不相同。

仔细比对两次打印结果的异同点。提出问题。

  1. 串行队列如何保证在异步线程中遵守先进先出原则(从Demo里看,也就是顺序打印我们的结果)?
    很简单,它会保证每次dispatch_async开辟线程执行串行队列中的任务时,总是使用同一个异步线程。这也是为什么我们的第一次打印结果中,NSThread总是同一个。
  2. 在dispatch_async中放入并行队列并执行的时候,为什么执行顺序总是乱序的?
    因为在并行对列中,每执行一次任务的时候,dispatch_async总会为我们开辟一个新的线程(当然,开辟线程的总量是有限制的,你可以试试循环一万次并打印Thread信息)来执行任务,所以不同线程开始结束的时间都不一样,导致了结果是乱序的。

在dispatch_sync中使用串行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)testSerialQueueWithSync
{
for (int index =0; index<10; index++) {
dispatch_sync(serial_queue, ^{
NSLog(@"index = %d", index);
NSLog(@"current thread is %@", [NSThread currentThread]);
});
}
NSLog(@"Running on main Thread");
}

打印结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
index = 0
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 1
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 2
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 3
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 4
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 5
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 6
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 7
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 8
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
index = 9
current thread is <NSThread: 0x7ff84b507ac0>{number = 1, name = main}
Running on main Thread
  1. dispatch_sync并没有开辟一个新的线程,直接在当前线程中执行代码(即main线程)。所以会阻塞当前线程。
  2. Running on main Thread在最后输出。

也就是说,当使用dispatch_sync执行串行队列的任务时,不会开辟新的线程,会直接使用当前线程执行队列中的任务。

在dispatch_sync中使用并行队列

代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testConcurrentQueueWithSync {
for (int index = 0; index<10; index++) {
dispatch_sync(concurrent_queue, ^{
NSLog(@"index = %d", index);
NSLog(@"current thread is %@", [NSThread currentThread]);
});
}
NSLog(@"Running on main Thread");
}

打印结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
index = 0
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 1
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 2
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 3
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 4
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 5
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 6
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 7
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 8
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
index = 9
current thread is <NSThread: 0x7fc1c0e048e0>{number = 1, name = main}
Running on main Thread

结果很奇怪,和在串行队列执行的效果一模一样。按我们的思考,并行队列里执行任务不应该是多个线程同时跑么?其实是由于dispatch_sync并不会开辟新的线程执行任务,所以导致了执行并行队列任务的线程总会是一个线程,自然,结果是一样的。

dispatch_barrier

讲完了GCD最基本的用法。我们来看看一个不太常用的GCD。dispatch_barrier
这个barrier我感觉使用霸道总裁来形容比较合适。这里借用raywenderlich上介绍barrier的一张图。

看的有点懵逼?
不要紧。我来解释一下。
首先,在一个并行队列中,有多个线程在执行多个任务,在这个并行队列中,有一个dispatch_barrier任务。这样会有一个什么效果呢?
就是,所有在这个dispatch_barrier之后的任务总会等待barrier之前的所有任务结束之后,才会执行。那么细心的同学可能会发现这里有个问题,既然所有在barrier之后的任务都会等待在barrier之前的任务结束之后执行,那么barrier本身执行是否会阻塞当前线程?
所以,dispatch_barrier也是有两个方法的。dispatch_barrier_syncdispatch_barrier_async.

dispatch_barrier_sync

还是看代码理解的更快一点。代码如下。

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
- (void)testBarrierSyncWithConCurrentQueue {
for (int index = 0; index<10; index++) {
dispatch_async(concurrent_queue, ^{
NSLog(@"index = %d", index);
});
}
for (int j = 0; j<10000; j++) {
dispatch_barrier_sync(concurrent_queue, ^{
if (j == 10000-1) {
NSLog(@"barrier Finished");
NSLog(@"current thread is %@", [NSThread currentThread]);
}
});
}
NSLog(@"Running on Main Thread");
for (int index =10; index<20; index++) {
dispatch_async(concurrent_queue, ^{
NSLog(@"index = %d", index);
});
}
}

那么运行之后我们的输出结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
index = 2
index = 7
index = 0
index = 1
index = 3
index = 4
index = 5
index = 8
index = 6
index = 9
barrier Finished
current thread is <NSThread: 0x7fa81bd08050>{number = 1, name = main}
Running on Main Thread
index = 10
index = 11
index = 12
index = 13
index = 14
index = 15
index = 16
index = 17
index = 18
index = 19

ok,总结一下。

  1. dispatch_barrier_sync确实是会在队列中充当一个栅栏的作用,凡是在他之后进入队列的任务,总会在dispatch_barrier_sync之前的所有任务执行完毕之后才执行。
  2. 见名知意,dispatch_barrier_sync是会在主线程执行队列中的任务的,所以,Running on Main Thread这句话会被阻塞,从而在barrier之后执行。

dispatch_barrier_async

那么我们再看看dispatch_barrier_async执行的效果。
代码如下。

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
- (void)testBarrierAsyncWithConCurrentQueue {
for (int index = 0; index<10; index++) {
dispatch_async(concurrent_queue, ^{
MyLog(@"index = %d", index);
});
}
for (int j = 0; j<10000000; j++) {
dispatch_barrier_async(concurrent_queue, ^{
if (j == 10000000-1) {
MyLog(@"barrier Finished");
MyLog(@"current thread is %@", [NSThread currentThread]);
}
});
}
MyLog(@"Running on Main Thread");
for (int index =10; index<20; index++) {
dispatch_async(concurrent_queue, ^{
MyLog(@"index = %d", index);
});
}
}

其实结果不难猜到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
index = 1
index = 0
index = 4
index = 2
index = 3
index = 8
index = 9
index = 7
index = 5
index = 6
Running on Main Thread
barrier Finished
current thread is <NSThread: 0x7f9b0b6023c0>{number = 2, name = (null)}
index = 11
index = 13
index = 14
index = 10
index = 15
index = 17
index = 16
index = 12
index = 18
index = 19

dispatch_barrier_async会开辟一条新的线程执行其中的任务,所以不会阻塞当前线程。其他的功能和dispatch_barrier_sync相同。

几个小问题

  1. 为什么我们只举了barrier和并行队列的例子,而没有举barrier和串行队列的例子?
    因为,barrier和串行队列配合是完全没有意义的。barrier的目的是什么?目的是为了在某种情况下,同一个队列中一些并发任务必须在另一些并发任务之后执行,所以需要一个类似于拦截的功能,迫使后执行的任务必须等待。那么,串行队列中的所有任务本身就是按照顺序执行的,那么又有什么必要使用拦截的功能呢?

  2. 在global queue中使用barrier没有意义,为什么?
    barrier实现的基本条件是,要写在同一队列中。举个例子,你现在创建了两个并行队列,你在其中一个队列中插入了一个barrier任务,那么你不可能期待他可以在第二个队列中生效,对吧。同样的,每一次使用global queue,系统分配给你的可能是不同的并行队列,你在其中插入一个barrier任务,又有什么意义呢?

该做点什么了

思考一个问题,NSMutableDictionary是否是线程安全的?

做个测试好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)testMutableDictionaryThreadSafe {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(concurrent_queue, ^{
for (int index = 0; index<1000 ; index++) {
dict[@(index)] = @(index);
}
dispatch_semaphore_signal(sema);
});
dispatch_async(concurrent_queue, ^{
for (int index = 0; index<1000 ; index++) {
dict[@(index)] = @(0);
}
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"dict is %@", dict);
}

运行结果是,直接崩了。。
因为NSMutableDictionary不是线程安全的,任意一个线程在往字典里写入数据的时候是不允许有其他线程访问的,不管是读或者写都不可以。
所以,现在的任务就是,我们需要使用gcd来实现一个线程安全的NSMutableDictionary。

第一个方案,serial queue + dispatch_async

首先,串行队列能保证每一个读或者写操作都是按顺序执行的,并且会在同一个线程执行任务,dispatch_async又能保证读写操作均能在异步线程执行,所以不会卡当前线程。所以表面上看是没问题的。
关键的问题是,太慢了,因为你每次只会有一个线程读或者写。如果同时有一百个读的请求,那么你的数据必须要按照顺序,一个一个的读出来。所以这个方案行不通。

第二个方案,concurrent queue + dispatch_async

用这个方案意味着,我们可以多线程同时读取字典里的数据。但是我们得确保一个条件。我们读取字典数据的时候,必须保证没有别的线程在写。
所以,确定读取线程安全的条件变成了,如何迫使写的这个操作在同一时刻,只有一个线程在写,并且,没有其他线程读或者写。那么,当然可以用dispatch_barrier_async来实现我们的需求了。
为什么dispatch_barrier_async可以实现我们的需求?
想一下dispatch_barrier_async的几个特性。
同一个队列中,只要插入了一个barrier,那么在他之后的所有任务都必须等他完成了才可以继续。所以,只要我们保证所有写操作都在barrier里完成,那么,我们不可能在一个concurrent queue里同时有多个线程在往字典里写数据。因为假如有多个写的操作,每一个写操作总会等待前一个写操作完成之后才执行。
所以代码如下。

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
typedef void (^ThreadSafeDictionaryBlock)(ThreadSafeDictionary *dict, NSString *key, id object);
@interface ThreadSafeDictionary ()
{
dispatch_queue_t concurrentQueue;
}
@end
@implementation ThreadSafeDictionary
- (id)init
{
if (self = [super init]) {
concurrentQueue = dispatch_queue_create("www.reviewcode.cn", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)objectForKey:(id)aKey block:(ThreadSafeDictionaryBlock)block
{
id key = [aKey copy];
__weak __typeof__(self) weakSelf = self;
dispatch_async(concurrentQueue, ^{
ThreadSafeDictionary *strongSelf = weakSelf;
if (!strongSelf)
return;
id object = [self objectForKey:key];
block(self, key, object);
});
}
- (void)setObject:(id)object forKey:(NSString *)key block:(ThreadSafeDictionaryBlock)block
{
if (!key || !object)
return;
NSString *akey = [key copy];
__weak ThreadSafeDictionary *weakSelf = self;
dispatch_barrier_async(concurrentQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;;
if (!strongSelf)
return;
[self setObject:object forKey:akey];
if (block) {
block(strongSelf, akey, object);
}
});
}
@end

这里可能会有人存疑,外部用__weak关键在声明weakSelf这个没什么问题,防止retain cycle。但里面为什么又用__strong声明了一个strongSelf?

这是因为,你享受了weak的好处,同时也要承担风险,由于不持有这个weakSelf,所以你无法保证在代码运行过程中,self不会被释放。

那可能又有人问了,你在外部用weak声明的原因是防止retain cycle,结果又在里面声明了一个strong,那不相当于做无用功了么。但问题是,在block内部声明的strongSelf是局部变量,他的生命周期只是在block内而已,当block执行完它自动就被销毁了。所以不会造成retain cycle。

还有一个问题,如果在第一句__strong typeof(weakSelf)strongSelf = weakSelf;的时候weakSelf已经被销毁了怎么办?

你没看我下面有个if(!strongSelf)么?不就是应对这种情况的。

dispatch_semaphore

信号量。
信号量的用法相当简单。
一共有三个方法。

1
2
3
dispatch_semaphore_create => 创建一个信号量
dispatch_semaphore_signal => 发送一个信号
dispatch_semaphore_wait => 等待信号

dispatch_semaphore的使用场景是处理并发控制。
如果感觉不是很明白的话,想想NSOperationQueue的一个属性,maxConcurrentOperationCount.这个属性的意思就是设定NSOperationQueue里的NSOperation同时运行的最大数量。

我们的信号量也可以实现同样的功能。
首先,创建一个信号量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
创建方法里会传入一个long型的参数,这个东西你可以想象是一个库存。有了库存才可以出货。
dispatch_semaphore_wait,就是每运行一次,会先清一个库存,如果库存为0,那么根据传入的等待时间,决定等待增加库存的时间,如果设置为DISPATCH_TIME_FOREVER,那么意思就是永久等待增加库存,否则就永远不往下面走。
dispatch_semaphore_signal,就是每运行一次,增加一个库存。
那么下面的代码运行起来会是怎样的结果呢?

1
2
3
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"等待semaphore");

结果就是,等待semaphore这句话永远不会输出。原因有两个。

  1. 你初始化信号量的时候,并没有库存,也就是你传入的值是0.
  2. 你传入等待增加库存的时间是DISPATCH_TIME_FOREVER,也就是说,除非有地方运行了dispatch_semaphore_signal增加了库存,否则我永远等待下去。
    基于上述的两个原因,导致了程序不往下走了。

所以,其实这样的特性可以帮助我们完成一个功能,测试异步网络请求是否成功。
比如,在以前写Unit Test测试网络请求的时候,我们常常这么写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)downloadImageURLWithString:(NSString *)URLString
{
// 1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURL *url = [NSURL URLWithString:URLString];
__unused Photo *photo = [[Photo alloc]
initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *error) {
if (error) {
XCTFail(@"%@ failed. %@", URLString, error);
}
// 2
dispatch_semaphore_signal(semaphore);
}];
// 3
dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, 5);
if (dispatch_semaphore_wait(semaphore, timeoutTime)) {
XCTFail(@"%@ timed out", URLString);
}
}

解释一下。

  1. 我们创建了一个信号量,并且传入的参数为0,可以想象现在是0库存.
  2. 开启了一个异步线程下载网络数据,在回调的block中判断error是否为nil。如果为nil,直接使用XCTFail,报错。如果成功了。那么增加一个库存,告诉信号量,好了,现在有库存了。你可以继续运货了。
  3. 设置一个超时时间,5秒钟,这个是干嘛的呢?这个是告诉信号量,我等待库存增加的时间只有5秒钟,如果超过了五秒钟,我就报错。怎么判断是否超过5秒钟呢?dispatch_semaphore_wait(semaphore, timeoutTime) ,如果超过了5秒钟,那么这个方法会返回一个非0的长整型。

这样,我们就可以给一个网络接口写单元测试了。

上文我们提到过,信号量的主要功能还是控制并发量,可以实现类似于NSOperationQueue的功能。那么我现在尝试实现一个。

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
@implementation CustomOperationQueue
- (id)initWithConcurrentCount:(int)count
{
self = [super init];
if (self)
{
if (count < 1) count = 5;
semaphore = dispatch_semaphore_create(count);
queue = Dispatch_queue_create("com.zangqilong.www", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (id)init
{
return [self initWithConcurrentCount:5];
}
- (void)addTask:(CallBackBlock)block
{
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 0), ^{
block();
dispatch_semaphore_signal(semaphore);
});
});
}
@end

我们把注意力放在- (void)addTask:(CallBackBlock)block这个方法里。

  1. 我们创建了一个初始库存是5的一个信号量
  2. 在addTask方法里,由于我们的初始库存是5,所以第一次添加了一个任务之后,dispatch_semaphore_wait会直接放行,并减少一个库存,所以现在库存是4,然后,每当我们完成一个任务,也就意味着,我们可以把库存还回去了。所以就会调用dispatch_semaphore_signal去增加一个库存。
  3. 那么,如果我们的每一个任务耗时都相当长,所以我们是一直消耗库存但是没有还回库存,所以当添加到第6个任务的时候,这个时候由于库存已经为0,所以wait方法会一直等待,不执行第六个任务,直到有前面的任务完成,库存大于0,这时候才会执行第六个任务。

这样,我们就完成了并发量的控制。

dispatch_group

直接看代码

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
dispatch_group_t serviceGroup = dispatch_group_create();
// 开始第一个请求
// 进入组
dispatch_group_enter(serviceGroup);
[self.configService startWithCompletion:^(ConfigResponse *results, NSError* error){
configError = error;
// 离开组
dispatch_group_leave(serviceGroup);
}];
// 开始第二个请求
// 先进入组
dispatch_group_enter(serviceGroup);
[self.preferenceService startWithCompletion:^(PreferenceResponse *results, NSError* error){
// 离开组
preferenceError = error;
dispatch_group_leave(serviceGroup);
}];
// 当小组里的任务都清空以后,通知主线程完成了所有任务
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
// Assess any errors
NSError *overallError = nil;
if (configError || preferenceError)
{
// Either make a new error or assign one of them to the overall error
overallError = configError ?: preferenceError;
}
// Now call the final completion block
completion(overallError);
});

这个dispatch_group最常见的功能就是,一个页面有多个异步网络请求,如何监测所有任务都完成了。
这个方法相信大家都会用。
那么需要注意的是。这里的网络请求回调只有一个block,所以无论请求出错还是成功了,只需要写一次dispatch_group_leave,但是如果你的回调block有两个,分成功的回调和错误的回调,那么你必须在两个block里都写disptach_group_leave,因为你不知道你的请求到底会走成功的block还是失败的block,如果少写了,一旦某个请求失败了,那么你的notify方法就永远也不会执行了。
p.s.如有侵权请告诉我

Node.js webapi

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
RESTful API
GET /products
var express = require('express')
var app = express();
var products = [
{ name: 'apple juice', description: 'good', price: 12.12 },
{ name: 'banana juice', description: 'just so sos', price: 4.50 }
]
app.get('/products', function(req, res) {
res.json(products);
});
var server = app.listen(3000, function() {
console.log('listening on port %d', server.address().port);
})
GET /products/:id => 200
app.get('/products/:id', function(req, res) {
res.json(products[req.params.id]);
})
GET /products/:id => 404
app.get('/products/:id', function(req, res) {
if (products.length <= req.params.id || req.params.id < 0) {
res.statusCode = 404;
return res.send('Error 404: No products found')
}
res.json(products[req.params.id]);
})
POST /products => 201
因为我们在接受POST请求的时候,需要解析POST的body,所以,我们就要使用middleware来做这件事情,在早期版本中提供了bodyParser这样的方式。但是这种方式会创建大量的临时文件。所以,我们应该直接使用json或者urlencoded这样的middleware直接解析
在package.json中添加依赖:
"body-parser": "1.4.3"
var express = require('express'),
bodyParser = require('body-parser')
var app = express()
.use(bodyParser.json());
app.post('/products', function(req, res) {
var newProduct = {
name: req.param('name'),
description: req.param('description'),
price: req.param('price')
}
products.push(newProduct);
res.statusCode = 201;
res.location('/products/' + products.length);
res.json(true);
});

.Net推送证书制作

.net 推送证书的制作

使用OpenSSL (使用环境:Mac Os X 10.9 命令行工具)
将aps_developer_identity.cer转换成 aps_developer_identity.pem格式。//红色部门及从开发证书下载下来对应两个环境:(dev或者distribution(push))
1
openssl x509 -in aps_developer_identity.cer -inform DER -out aps_developer_identity.pem -outform PEM
将p12格式的私钥转换成pem,需要设置4次密码,密码都设置为:dynastech。
1
openssl pkcs12 -nocerts -out PushChat_Noenc.pem -in PushChat.p12 //这个是在mac os x的钥匙串中导出的推送证书的.p12,默认是需要输入私钥密码的,可以输入空的密码
用certificate和the key 创建PKCS#12格式的文件。
1
2
openssl pkcs12 -export -in aps_developer_identity.pem -inkey PushChat_Noenc.pem -certfile PushChat.certSigningRequest -name "aps_developer_identity" -out aps_developer_identity.p12
//将三个颜色的文件合成 xxxxxx.p12后,就可以提供给.net程序调用了,然后在.net程序中需要设置xxxxx.p12默认复制到输出。

这样我们就得到了在.net应用程序中使用的证书文件:aps_developer_identity.p12。

PHP证书的制作参见 iOS远程推送通知.pdf

XX环境中使用到的有关文件

(为了方便,我改名为PushChat.cer 以及 PushChat.p12,名字可以随便改)

具体三方推送平台的引入(百度推送、极光推送),只需要按照要求制作证书上传到对应的平台,即可使用,目前是苹果端使用的原生的推送机制,Android使用的极光推送。

JSPatch

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* Created by chenyun on 15/7/7.
*/
require('UIColor,UIViewController,NSNumber,UIView')
/**
* 进行 tableveiw 数据源的替换
*/
defineClass('TestTableViewController',
{
tableView_cellForRowAtIndexPath: function(tableView, indexPath)
{
var cell = tableView.dequeueReusableCellWithIdentifier("CELL");
if(!cell){
cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0,"CELL");
}
// var num = self.dataSource().objectAtIndex(indexPath.row())
// cell.textLabel().setText(num + "js")
var jsArray = self.dataSource().toJS()
cell.textLabel().setText(jsArray[indexPath.row()] + "JS")
return cell;
}
}
)
/**
* 事件替换
*/
defineClass('TestViewController',
{
tableView_didSelectRowAtIndexPath: function(tableView, indexPath)
{
if(indexPath.section() == 1 && indexPath.row() == 0){
var testVC = self.storyboard().instantiateViewControllerWithIdentifier("testVC");
self.navigationController().pushViewController_animated(testVC,1)
}
if(indexPath.section() == 1 && indexPath.row() == 1){
var tableVC = JSTableViewController.alloc().init()
self.navigationController().pushViewController_animated(tableVC,YES);
}
}
}
)
/**
* 声明一个类
*/
defineClass('JSTableViewController:UITableViewController',{
dataSource:function()
{
var data = self.getProp('data')
if(data)return data;
var data = [];
for(var i = 0 ; i < 12;i++)
{
data.push("from js " + i)
}
self.setProp_forKey(data,'data')
return data;
},
numberOfSectionsInTableView: function(tableView) {
return 1;
},
tableView_numberOfRowsInSection: function(tableView, section) {
return self.dataSource().count();
},
tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell")
if (!cell) {
cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0, "cell")
}
cell.textLabel().setText(self.dataSource().objectAtIndex(indexPath.row()))
return cell
},
tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
return 60
},
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert",self.dataSource().objectAtIndex(indexPath.row()), self, "OK", null);
alertView.show()
}
})
/**
* 重写 btn 点击事件
*/
defineClass('SomeTestViewController',
{
/**
* 系统方法
* @param animated
*/
viewWillAppear:function(animated){
self.super().viewWillAppear(1)
self.setTitle("JSPatch Methods")
},
/**
* 执行一段动画
* @param sender
*/
doAnimation:function(sender){
var red = Math.floor(Math.random() * ( 255 +0.1))/255;
var green = Math.floor(Math.random() * ( 255 +0.1))/255;
var blue = Math.floor(Math.random() * ( 255 +0.1))/255;
var color = UIColor.colorWithRed_green_blue_alpha(red,green,blue,1)
UIView.animateWithDuration_animations_completion(1.0,block("",function(){
self.view().setBackgroundColor(color)})
,block("BOOL",function(finished){}));
},
/**
* 背景色改变
* @param sender
*/
changeBackgroundColor:function(sender){
var red = Math.floor(Math.random() * ( 255 +0.1))/255;
var green = Math.floor(Math.random() * ( 255 +0.1))/255;
var blue = Math.floor(Math.random() * ( 255 +0.1))/255;
var color = UIColor.colorWithRed_green_blue_alpha(red,green,blue,1)
self.view().setBackgroundColor(color)
},
/**
* 增加控件
* @param sender
*/
addView:function(sender){
var xx = Math.floor(Math.random() * (320 + 1))
var yy = Math.floor(Math.random() * (640 + 1))
var aView = require('UIView').alloc().initWithFrame({x:xx,y:yy,width:50,height:50})
aView.setBackgroundColor(UIColor.redColor())
self.view().addSubview(aView)
},
alert:function(sender){
var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert","test", self, "OK", null);
alertView.show()
},
alertView_willDismissWithButtonIndex: function(alertView, idx) {
console.log('click btn ')
}
})

iOS策略模式的简单应用

在iOS开发中,使用官方框架,官方sdk中,可以接触到不少设计模式,可能平时没有注意,实际上已经用到了不少设计模式

下面举一个例子:

策略模式:至于什么是策略模式,请自己百度吧,我也说不清楚,但是知道怎么用,下面结合代码详细说明

比方我有一个NSMutableArray,里面每个元素都是一个NSDictionary,其中NSDictionary有不少“键--值”对,我想以“键1对应的值1”为标准,对NSMutableArray进行排序。
NSMutableArray

--- NSDictionary1

------“name”:"zhangsan"

------“age”:“30”

--- NSDictionary2

------“name”:"lisi"

------“age”:“28“

--- NSDictionary3

------“name”:"lisi"

------“age”:“48“

下面我需要针对”age“字段进行排序

那么策略模式在这里就是这么展示的:你丢给NSMutableArray对象一个排序的方法(一个策略),那么他就拿这个方法对内部的元素进行排序,你丢给他不同的方法(也就是不同的策略<实际的每个策略,不简单是一个参数,而是做一件事情的完整过程>),他就给你不同的结果。

NSArray中存放的是NSDictionary,可以使用策略的方法对NSDictionary进行定制,增加比较的方法。然后调用NSArray的sortUsingSelector方法对数组进行排序,这里使用NSDictionay中的时间对象的时间排序。具体操作如下:

XXX.h文件

1
2
3
4
5
6
@interface NSMutableDictionary(myCompare)
-(NSComparisonResult)myCompareMethodWithDict: (NSMutableDictionary*)theOtherDict;
@end

XXX.m文件

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
@implementation NSMutableDictionary(myCompare)
- (NSComparisonResult)myCompareMethodWithDict:(NSMutableDictionary*)anotherDict
{
NSMutableDictionary *firstDict = self;
int iSelfAge =[ [firstDict objectForKey: @"age"]intValue];
int iOtherAge = [[anotherDict objectForKey: @"age"]intValue];
//return [firstDate compare: secondDate];
// //NSOrderedAscending = -1, NSOrderedSame, NSOrderedDescending}
if(iSelgAge<iOtherAge)return NSOrderedAscending;
else if (iSelgAge==iOtherAge)return NSOrderedSame;
else return NSOrderedDescending;
}
@end

2.使用myCompareMethodWithDict对NSArray进行排序,假设NSArray是从plist文件中读取的NSDictionary对象的数组。

1
2
3
4
5
    NSString* documentsDirectory = [paths objectAtIndex:0];
NSString *plistPath = [NSString stringWithFormat:@"%@/XXX.plist",documentsDirectory];
     NSMutableDictionary * cacheData = [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];
[cacheArray sortUsingSelector:@selector(myCompareMethodWithDict:)];//根据年龄降序排序
这样,cacheArray就是排序好的数组了。

最近做的一个通讯录应用中有个需求需要对用户展示的位置进行排序。返回的数据json解析后存在了sqlite的[数据库](http://lib.csdn.net/base/14)中了。

那么解决的方法有两种

1,sql排序 :没有什么新的东西,主要学习了在sqlite中用sql语句实现left join,union的相关操作,在Xcode中sql语句太长了显得不太好看,那么推荐navi猫中把业务处理好后再粘贴到Xcode中进行修改

2,策略模式:对mutalbeArray的元素(这里并不是你一个Dictionnary,而是一个自定义的Model)我们同样可以给这个mutalbeArray中的元素一个策略。

iOS 长后台

研究iOS长后台,卡壳太久了,记得是去年给某药业做一个移动外勤app,需要iOS保持后台在线。尝试了网上给出的各种方法,有的只能在插上电源的时候一直跑,但是掉了电,过不了几分钟就背杀掉了。

这里也不用iOS7的一些方法,在进入后台的时候直接申请一个后台任务。同时开启一个定时器去检测(作者给出的检测时间是1min),检测到后台任务的时间比较小的时候,转载的文章给出的值是61s。当小于这个值的时候,这个时候来播放一段无声的音乐文件。播放的同时,再来申请后台的任务,这样就不会被苹果干掉了。

但是有一个缺点,如果你的应用没有播放音乐的功能,只是为了长后台而加上,有可能是无法上架的。但是对于企业应用来说这已经足够你使用了。

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
[NSTimer scheduledTimerWithTimeInterval:60 target:self selector:@selector(tik) userInfo:nil repeats:YES];
// Override point for customization after application launch.
return YES;
}
1
2
3
4
5
6
7
8
9
- (void)tik{
if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 61.0)
{
[self longTimeTask];
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)longTimeTask
{
NSString *musicFilePath = [[NSBundle mainBundle] pathForResource:@"SlientAudio" ofType:@"wav"]; //创建音乐文件路径
NSURL *musicURL = [[NSURL alloc] initFileURLWithPath:musicFilePath];
if (_myBackMusic == nil)
{
AVAudioPlayer *thePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:musicURL error:nil];
//创建播放器
self.myBackMusic = thePlayer; //赋值给自己定义的类变量
}
[self.myBackMusic prepareToPlay];
//[self.myBackMusic setVolume:1]; //设置音量大小
// thePlayer.numberOfLoops = -1;//设置音乐播放次数 -1为一直循环
[self.myBackMusic play]; //播放
}

http://my.oschina.net/u/1386081/blog/277380


http://pan.baidu.com/s/1pJM9gSj