NSData的一些直接操作binary data的用法

##方法:

//將另一個NSData加到原來的NSData後面
-(void)appendData:(NSData*)otherData

//將bytes直接加入NSData,參數為1.bytes的起始位置(要加&),2.bytes的長度
-(void)appendBytes:(const void*)bytes length:(NSUInteger)length

//截取其中某一段NSData
-(NSData*)subdataWithRange:(NSRange)range

//從你的NSData中copy某段bytes出來,存到你新建的buffer(要加&)中(存數值資料,而非NSData)
-(void)getBytes:(void*)buffer range:(NSRange)range

//從你的NSData中從頭copy某段指定長度的bytes出來,存到你新建的buffer(要加&)中(存數值資料,而非NSData)
-(void)getBytes:(void*)buffer length:(NSUInteger)length

##範例

//直接取得binary檔案

NSURL *imgPath = [[NSBundle mainBundle] URLForResource:@"yourBinary" withExtension:@"bin"];
NSString *stringPath = [imgPath absoluteString];
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:stringPath]];

//要把NSData轉回NSNumber時(這裡是以short為範例)
short s;
[yourData getBytes:&s range:range];
NSNumber* stepCount = [NSNumber numberWithUnsignedShort:s];


// low level read method - read data while there is data and space available in the input buffer
- (void)_readData{
    uint8_t buf[EAD_INPUT_BUFFER_SIZE];
    while ([[_session inputStream] hasBytesAvailable]){
        NSInteger bytesRead = [[_session inputStream] read:buf maxLength:EAD_INPUT_BUFFER_SIZE];
        if (_readData == nil){
            _readData = [[NSMutableData alloc] init];
        }
        [_readData appendBytes:(void *)buf length:bytesRead];
        //NSLog(@"read %d bytes from input stream", bytesRead);
 }
    
    [[NSNotificationCenter defaultCenter] postNotificationName:EADSessionDataReceivedNotification object:self userInfo:nil];

    //清空_readData
    if( _readData ){
        [_readData setLength:0];
    }
}

// high level read method
// 這裡輸入的參數為你想要切的長度。

- (NSData *)readData:(NSUInteger)bytesToRead
{
    NSData *data = nil;
    if ([_readData length] >= bytesToRead)
    {
        NSRange range = NSMakeRange(0, bytesToRead);
        data = [_readData subdataWithRange:range];
        [_readData replaceBytesInRange:range withBytes:NULL length:0];
    }
    return data;
}

// get number of bytes read into local buffer

-(NSUInteger)readBytesAvailable{
    return [_readData length];

}

//自己寫的切封包

+ (NSArray*)splitDataIntoChunks:(NSData*)data size:(NSUInteger)bytesPerChunk{
    if(!data) return nil;
    NSMutableArray* array = [[NSMutableArray alloc]init];
    NSUInteger dataLength = [data length];
    NSUInteger chunkCount = 0;
    while (chunkCount < dataLength){
        NSRange range = NSMakeRange(chunkCount, bytesPerChunk);
        NSData* chunk = [data subdataWithRange:range];
        [array addObject:chunk];
        chunkCount += 2;
    }
    return array;
}
12/29/2016 10:37 上午 posted in  Foundation

iOS开发,如何利用Platinum库开发一个DLNA功能?

##一、DLNA简介
什么是DLNA?

DLNA的全称是DIGITAL LIVING NETWORK ALLIANCE(数字生活网络联盟), 其宗旨是Enjoy your music, photos and videos, anywhere anytime, DLNA(Digital Living Network Alliance) 由索尼、英特尔、微软等发起成立、旨在解决个人PC,消费电器,移动设备在内的无线网络和有线网络的互联互通,使得数字媒体和内容服务的无限制的共享和增长成为可能,目前成员公司已达280多家。
<引自百度百科>
DLNA包含若干协议和标准,最重要的也是我们最常用的就是UPnP技术。那么UPnP又是什么呢?

通用即插即用 (UPnP) 是一种用于 PC 机和智能设备(或仪器)的常见对等网络连接的体系结构,尤其是在家庭中。UPnP 以 Internet 标准和技术(例如 TCP/IP、HTTP 和 XML)为基础,使这样的设备彼此可自动连接和协同工作,从而使网络(尤其是家庭网络)对更多的人成为可能。
<引自百度百科>

那么,了解了这两个概念,还不够,我们还需要了解DLNA中另外几个重要概念。

  1. DMC:简单点说,就是一个控制点,那么这个控制点是什么,我个人的理解是,谁控制,谁就是控制点。
  2. DMS:这是一个媒体服务器。
  3. DMR:我理解为媒体渲染器。
    这三者的关系是,DMC通过获取DMS上的歌曲或者视频(也可以不是DMS上的,而仅仅只是一个链接),把它们传送到DMR上,由DMR进行播放。

好了,我们把几个最基本的概念了解了一下,下面我们正式开始开发DLNA功能。DEMO在最后。
再补充一句,platinum是集成开发,博主认为,各位还是应该先看看底层协议,了解DLNA具体是怎么运作的,然后再使用这个库进行开发,下面给一个博主写的DEMO并推荐四篇文章,让各位学习。

  1. DEMO
  2. 基于DLNA实现iOS,Android投屏:基本概念
  3. 基于DLNA实现iOS,Android投屏:SSDP发现设备
  4. 基于DLNA实现iOS,Android投屏:SOAP控制设备
  5. 基于DLNA实现iOS,Android投屏:订阅事件通知

##二、集成Platinum库

2.1 下载Platinum库

git clone --depth=1 https://github.com/plutinosoft/Platinum.git

2.2 编译Neptune库
由于Platinum库是基于C++编写的,所以我们要安装一个叫Neptune的跨平台库。不过我们不用自己去编译,打开你下载好的Platinum库,你会发现有一个Cartfile的文件,用cat Cartfiel就能输出里面的内容,你会看到里面有这个库,所以我们直接下载就好了。
下载的话我们需要用homebrew去下载一个carthage,然后再下载这个库。没有homebrew的以及不知道carthage是什么的同学请自行百度哦,这里就不多说了。

cd Platinum
brew install carthage
carthage update

以上三条命令完成后,你会在platinum/Carthage/build文件夹下看到两个文件夹,分别是iOS和Mac,里面各有一个Neptune.framework,这时就说明我们编译成功了。

2.3 编译platinum库
打开platinum/Build/Targets/universal-apple-macosx/Platinum.xcodeproj工程,选择Platinum-iOS的target,分别在模拟器和iOS设备上运行一次。如下:

运行完了之后,你可以看到如下的库:
编译好的库

然后右击选择show in Finder,找到它的位置,两个都要(前面编译了两次,把两次编译后的位置都找到),如下所示:

库的位置

iphonesimulator是模拟器的,iphoneos是真机的。
然后右击库,选择查看简介,复制它的路径,执行以下 的命令

//注意:-info 后面的参数请输入自己的
lipo -info /Users/gvs/Library/Developer/Xcode/DerivedData/Platinum-hkmepfuudtuavbfavwowgoojdisc/Build/Products/Release-iphonesimulator/Platinum.framework/Platinum 

lipo -info /Users/gvs/Library/Developer/Xcode/DerivedData/Platinum-hkmepfuudtuavbfavwowgoojdisc/Build/Products/Release-iphoneos/Platinum.framework/Platinum

如果是这样的,说明是正常的:
查看支持的架构

我们看到模拟器支持的是i386 x86_64架构的,真机是支持armv7 arm64架构的,所以我们需要把两个库链接在一起。执行以下命令:

//把路径换成你自己的
lipo -create /Users/gvs/Library/Developer/Xcode/DerivedData/Platinum-hkmepfuudtuavbfavwowgoojdisc/Build/Products/Release-iphoneos/Platinum.framework/Platinum /Users/gvs/Library/Developer/Xcode/DerivedData/Platinum-hkmepfuudtuavbfavwowgoojdisc/Build/Products/Release-iphonesimulator/Platinum.framework/Platinum -output /Users/gvs/Library/Developer/Xcode/DerivedData/Platinum-hkmepfuudtuavbfavwowgoojdisc/Build/Products/Release-iphoneos/Platinum.framework/Platinum
//查看有没有链接成功
lipo -info /Users/gvs/Library/Developer/Xcode/DerivedData/Platinum-hkmepfuudtuavbfavwowgoojdisc/Build/Products/Release-iphoneos/Platinum.framework/Platinum

完成后是这样子的:

可以看到,platinum库已经同时支持真机和模拟器的架构了。

##三、使用platinum库
1.把刚刚编译好的Platinum.framework和我们之前下载的Neptune.framework一起拖进工程里面,勾选copy if needed选项,然后在Embedded Binaries 里面添加这两个库。
2.在使用库里面的头文件的时候,应该用#import <Platinum/Platinum.h>这样子的方式使用。
3.包含头文件后编译一次,如果没有报错,说明我们导入成功了。

##四、开发DLNA功能

我们先看看这个库它本身是怎么使用的,打开Platinum/Build/Targets/universal-apple-macosx/Platinum.xcodeproj工程文件,target选择MicroMediaController,它是以控制台方式运行的。target对应的代码在Source/Apps/MicroMediaController下面,打开main.cpp,我们先注释一行代码,如下,这行代码是输出一些信息的,我们先可以不看。

然后运行。运行后如果你有支持DLNA的设备,控制台依旧会输出一些信息,这时不用管它。在控制台里面按一下回车,会出现如下信息,也就是命令,然后自己慢慢摸索就可以了。对着MicroMediaController下单代码,慢慢看,慢慢写。

下面提供一个小DEMO,完成了播放,暂停,上一首,下一首,调音量功能,有需要的可以下载下来看看。

PlatinumDemo

简书:iOS开发,如何利用Platinum库开发一个DLNA功能?

12/21/2016 10:01 上午 posted in  DLNA

Attributes和ParagraphStyle介绍

NSMutableParagraphStyle的部分属性:

typedef NS_ENUM(NSInteger, NSLineBreakMode) {/* What to do with long lines */  
    NSLineBreakByWordWrapping = 0,     /* Wrap at word boundaries, default */  
    NSLineBreakByCharWrapping,/* Wrap at character boundaries */  
    NSLineBreakByClipping,/* Simply clip */剪掉后面显示不了的部分  
    NSLineBreakByTruncatingHead,/* Truncate at head of line: "...wxyz" */头部分的内容以……方式省略  
    NSLineBreakByTruncatingTail,/* Truncate at tail of line: "abcd..." */结尾部分的内容以……方式省略  
    NSLineBreakByTruncatingMiddle/* Truncate middle of line:  "ab...yz" */中间部分的内容以……方式省略  
} NS_ENUM_AVAILABLE_IOS(6_0);  
  

typedef NS_ENUM(NSInteger, NSWritingDirection) {  
    NSWritingDirectionNatural       = -1,    // Determines direction using the Unicode Bidi Algorithm rules P2 and P3  
    NSWritingDirectionLeftToRight   =  0,    // Left to right writing direction 左到右的书写方向  
    NSWritingDirectionRightToLeft   =  1    // Right to left writing direction 右到左的书写方向  
} NS_ENUM_AVAILABLE_IOS(6_0);  
  

 //   NSParagraphStyleAttributeName 段落的风格(设置首行,行间距,对齐方式什么的)看自己需要什么属性,写什么  
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];  
    paragraphStyle.lineSpacing = 10;// 字体的行间距  
    paragraphStyle.firstLineHeadIndent = 20.0f;//首行缩进  
    paragraphStyle.alignment = NSTextAlignmentJustified;//(两端对齐的)文本对齐方式:(左,中,右,两端对齐,自然)  
    paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;//结尾部分的内容以……方式省略 ( "...wxyz" ,"abcd..." ,"ab...yz")  
    paragraphStyle.headIndent = 20;//整体缩进(首行除外)  
    paragraphStyle.tailIndent = 20;//  
    paragraphStyle.minimumLineHeight = 10;//最低行高  
    paragraphStyle.maximumLineHeight = 20;//最大行高  
    paragraphStyle.paragraphSpacing = 15;//段与段之间的间距  
    paragraphStyle.paragraphSpacingBefore = 22.0f;//段首行空白空间/* Distance between the bottom of the previous paragraph (or the end of its paragraphSpacing, if any) and the top of this paragraph. */  
    paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight;//从左到右的书写方向(一共??三种)  
    paragraphStyle.lineHeightMultiple = 15;/* Natural line height is multiplied by this factor (if positive) before being constrained by minimum and maximum line height. */  
    paragraphStyle.hyphenationFactor = 1;//连字属性 在iOS,唯一支持的值分别为0和1  
  
/* 
     NSFontAttributeName 字体大小 
     NSParagraphStyleAttributeName 段落的风格(设置首行,行间距,对齐方式什么的) 
     NSKernAttributeName 字间距 
     */  
    NSDictionary *attributes = @{  
                                 NSFontAttributeName:[UIFont systemFontOfSize:15],  
                                 NSParagraphStyleAttributeName:paragraphStyle,  
                                 NSKernAttributeName:@(10),  
                                                                
                                 };  
    textView.attributedText = [[NSAttributedString alloc] initWithString:textView.text attributes:attributes];

NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
paragraph.alignment = NSTextAlignmentJustified;//设置对齐方式
paragraph.lineBreakMode = NSLineBreakByWordWrapping;

NSAttributedString的初始化方法有
-initWithString:用String初始化,并没有Attributed信息。
-initWithAttributedString:用AttributedString去初始化。
-initWithString:Attributed:用string及attribute的dictionary来初始化。

下面这段代码可以查看ios中可用的字体,具体那些字体长什么样,可以查看字体册工具。

NSArray *familyArray = [UIFont familyNames];
for (id family in familyArray) {
printf(“%s\n”,[family cStringUsingEncoding:NSUTF8StringEncoding]);
NSArray *fontArray = [UIFont fontNamesForFamilyName:family];
for (id font in fontArray) {
printf(”   %s\n”,[font cStringUsingEncoding:NSUTF8StringEncoding]);
}
}

NSMutableParagraphStyleNSParagraphStyle包括一下属性

  1. alignment //对齐方式
  2. firstLineHeadIndent //首行缩进
  3. headIndent //缩进
  4. tailIndent //尾部缩进
  5. lineBreakMode //断行方式
  6. maximumLineHeight //最大行高
  7. minimumLineHeight //最低行高
  8. lineSpacing //行距
  9. paragraphSpacing //段距
  10. paragraphSpacingBefore //段首空间
  11. baseWritingDirection //句子方向
  12. lineHeightMultiple //可变行高,乘因数。
  13. hyphenationFactor //连字符属性
//   NSParagraphStyleAttributeName 段落的风格(设置首行,行间距,对齐方式什么的)看自己需要什么属性,写什么  
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];  
    paragraphStyle.lineSpacing = 10;// 字体的行间距  
    paragraphStyle.firstLineHeadIndent = 20.0f;//首行缩进  
    paragraphStyle.alignment = NSTextAlignmentJustified;//(两端对齐的)文本对齐方式:(左,中,右,两端对齐,自然)  
    paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;//结尾部分的内容以……方式省略 ( "...wxyz" ,"abcd..." ,"ab...yz")  
    paragraphStyle.headIndent = 20;//整体缩进(首行除外)  
    paragraphStyle.tailIndent = 20;//  
    paragraphStyle.minimumLineHeight = 10;//最低行高  
    paragraphStyle.maximumLineHeight = 20;//最大行高  
    paragraphStyle.paragraphSpacing = 15;//段与段之间的间距  
    paragraphStyle.paragraphSpacingBefore = 22.0f;//段首行空白空间/* Distance between the bottom of the previous paragraph (or the end of its paragraphSpacing, if any) and the top of this paragraph. */  
    paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight;//从左到右的书写方向(一共➡️三种)  
    paragraphStyle.lineHeightMultiple = 15;/* Natural line height is multiplied by this factor (if positive) before being constrained by minimum and maximum line height. */  
    paragraphStyle.hyphenationFactor = 1;//连字属性 在iOS,唯一支持的值分别为0和1  

具体AttributtedString属性的键值对如下:

 NSFontAttributeName                设置字体属性,默认值:字体:Helvetica(Neue) 字号:12
 NSForegroundColorAttributeNam      设置字体颜色,取值为 UIColor对象,默认值为黑色
 NSBackgroundColorAttributeName     设置字体所在区域背景颜色,取值为 UIColor对象,默认值为nil, 透明色
 NSLigatureAttributeName            设置连体属性,取值为NSNumber 对象(整数),0 表示没有连体字符,1 表示使用默认的连体字符
 NSKernAttributeName                设定字符间距,取值为 NSNumber 对象(整数),正值间距加宽,负值间距变窄
 NSStrikethroughStyleAttributeName  设置删除线,取值为 NSNumber 对象(整数)
 NSStrikethroughColorAttributeName  设置删除线颜色,取值为 UIColor 对象,默认值为黑色
 NSUnderlineStyleAttributeName      设置下划线,取值为 NSNumber 对象(整数),枚举常量 NSUnderlineStyle中的值,与删除线类似
 NSUnderlineColorAttributeName      设置下划线颜色,取值为 UIColor 对象,默认值为黑色
 NSStrokeWidthAttributeName         设置笔画宽度,取值为 NSNumber 对象(整数),负值填充效果,正值中空效果
 NSStrokeColorAttributeName         填充部分颜色,不是字体颜色,取值为 UIColor 对象
 NSShadowAttributeName              设置阴影属性,取值为 NSShadow 对象
 NSTextEffectAttributeName          设置文本特殊效果,取值为 NSString 对象,目前只有图版印刷效果可用:
 NSBaselineOffsetAttributeName      设置基线偏移值,取值为 NSNumber (float),正值上偏,负值下偏
 NSObliquenessAttributeName         设置字形倾斜度,取值为 NSNumber (float),正值右倾,负值左倾
 NSExpansionAttributeName           设置文本横向拉伸属性,取值为 NSNumber (float),正值横向拉伸文本,负值横向压缩文本
 NSWritingDirectionAttributeName    设置文字书写方向,从左向右书写或者从右向左书写
 NSVerticalGlyphFormAttributeName   设置文字排版方向,取值为 NSNumber 对象(整数),0 表示横排文本,1 表示竖排文本
 NSLinkAttributeName                设置链接属性,点击后调用浏览器打开指定URL地址
 NSAttachmentAttributeName          设置文本附件,取值为NSTextAttachment对象,常用于文字图片混排
 NSParagraphStyleAttributeName      设置文本段落排版格式,取值为 NSParagraphStyle 对象

enum {
NSUnderlineStyleNone = 0×00,
NSUnderlineStyleSingle = 0×01,
};设置删除线。

11/21/2016 14:45 下午 posted in  TextKit

NSOperation

##一、简介

###1、使用步骤

配合使用NSOperation和NSOperationQueue也能实现多线程编程

先将操作封装到一个NSOperation对象中
然后将NSOperation对象添加到NSOperationQueue中
系统会自动将NSOperationQueue中的NSOperation取出来
将取出的NSOperation封装的操作放到一条新线程中执行
###2、具体实现方法:

第一步:封装操作

封装操作的时候可以使用NSOperation的子类实现,因为NSOperation是抽象类,所以不能直接使用。

三种方式:

NSInvocationOperation
NSBlockOperation
自定义子类继承NSOperation,实现内部相应的方法
下面分别讲解三种方式:

1、NSInvocationOperation
创建NSInvocationOperation对象

- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;

调用start方法开始执行操作

- (void)start;
//一旦执行操作,就会调用target的sel方法

注意:
默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作
只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作

2、创建NSBlockOperation对象

+ (id)blockOperationWithBlock:(void (^)(void))block;

通过addExecutionBlock:方法添加更多的操作

- (void)addExecutionBlock:(void (^)(void))block;

注意:只要NSBlockOperation封装的操作数 > 1,就会异步执行操作

3、自定义NSOperation
自定义NSOperation的步骤很简单
重写- (void)main方法,在里面实现想执行的任务
重写- (void)main方法的注意点:
自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池)
经常通过- (BOOL)isCancelled方法检测操作是否被取消,对取消做出响应
由于取消同样会继续执行当前正在执行的任务, 所以耗时操作需要手动判断是否已经取消

if (self.isCancelled) return;

第二步:添加任务奥队列中:
// 一般情况下, 在做企业开发时候,都会定义一个全局的自定义队列, 便于使用

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSOperationQueue的作用

NSOperation可以调用start方法来执行任务,但默认是同步执行的
如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
添加操作到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

设置最大并发数

- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;

##3、队列的取消、暂停、恢复

- (void)cancelAllOperations;
// 取消队列中所有的任务的执行
// 取消和暂停一样, 是取消后面的任务, 不能取消当前正在执行的任务
// 注意: 取消是不可以恢复的

提示:也可以调用NSOperation的- (void)cancel方法取消单个操作
暂停和恢复队列

- (void)setSuspended:(BOOL)b;
// YES代表暂停队列,NO代表恢复队列
- (BOOL)isSuspended;
// 只要设置队列的suspended为YES, 那么就会暂停队列中其它任务的执行
// 也就是说不会再继续执行没有执行到得任务
// 只要设置队列的suspended为NO, 那么就会恢复队列中其它任务的执行
// 注意: 设置为暂停之后, 不会立即暂停
// 会继续执行当前正在执行的任务, 直到当前任务执行完毕, 就不会执行下一个任务了
// 也就是说, 暂停其实是暂停下一个任务, 而不能暂停当前任务
// 注意: 暂停是可以恢复的

##4、依赖

NSOperation之间可以设置依赖来保证执行顺序
比如一定要让操作A执行完后,才能执行操作B,可以这么写

[operationB addDependency:operationA]; // 操作B依赖于操作A

可以在不同queue的NSOperation之间创建依赖关系

##5、任务操作的监听

可以监听一个操作的执行完毕

- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;</code>

// 4.监听op4什么时候执行完毕
op4.completionBlock = ^{
    NSLog(@"op4中所有的操作都执行完毕了");
};

##具体使用介绍:
实例一:
NSBlockOperation简单使用,并添加任务

 //1. 封装任务
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        // 主线程
        NSLog(@"1---%@", [NSThread currentThread]);
    }];
    // 2.追加其它任务
    // 注意: 在没有队列的情况下, 如果给BlockOperation追加其它任务, 那么其它任务会在子线程中执行
    [op1 addExecutionBlock:^{
        NSLog(@"2---%@", [NSThread currentThread]);
    }];
    [op1 addExecutionBlock:^{
        NSLog(@"3---%@", [NSThread currentThread]);
    }];
    // 3.启动任务
    [op1 start];

实例二:

NSInvocationOperation的简单使用

// 1.封装任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
// 2.要想执行任务必须调用start
[op1 start];

实例三:

创建各种操作并添加到队列里面

// 1.创建队列
    /*
     GCD中有哪些队列:
     并发: 自己创建, 全局
     串行: 自己创建, 主队列
 
     NSOperationQueue:
     主队列: mainQueue
     自己创建: 会在子线程中执行
     */
 
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 2.创建任务
    // 只要是自己创建的队列, 就会在子线程中执行
    // 而且默认就是并发执行
    //第一种
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    //第二种
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2 == %@", [NSThread currentThread]);
    }];
 
    // 注意: 如果是使用block来封装任务, 那么有一种更简便的方法
    // 只要利用队列调用addOperationWithBlock:方法, 系统内部会自动封装成一个NSBlockOperation然后再添加到队列中
    //第三种
    [queue addOperationWithBlock:^{
        NSLog(@"3 == %@", [NSThread currentThread]);
    }];
 
 
    // 3.添加任务到队列中
    // 只要将任务添加到队列中, 队列会自动调用start
    [queue addOperation:op1];
    [queue addOperation:op2];

实例四:

最大执行数量

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 自己创建的队列默认是并发,如果设置maxConcurrentOperationCount = 1,就是串行
// 注意: 不能设置为0, 如果设置为0就不行执行任务\
// 默认情况下maxConcurrentOperationCount = -1,是并行
// 在开发中并发数最多尽量不要超过5~6条
queue.maxConcurrentOperationCount = 0;

实例四:

线程间通讯

// 1.创建一个队列
    // 一般情况下, 在做企业开发时候, 都会定义一个全局的自定义队列, 便于使用
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
 
    // 2.添加一个操作下载第一张图片
    __block UIImage *image1 = nil;
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url  = [NSURL URLWithString:@"http://imgcache.mysodao.com/img2/M04/8C/74/CgAPDk9dyjvS1AanAAJPpRypnFA573_700x0x1.JPG"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        image1 = [UIImage imageWithData:data];
    }];
 
    // 3.添加一个操作下载第二张图片
    __block UIImage *image2 = nil;
     NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url  = [NSURL URLWithString:@"http://imgcache.mysodao.com/img1/M02/EE/B5/CgAPDE-kEtqjE8CWAAg9m-Zz4qo025-22365300.JPG"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        image2 = [UIImage imageWithData:data];
    }];
    // 4.添加一个操作合成图片
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        [image1 drawInRect:CGRectMake(0, 0, 100, 200)];
        [image2 drawInRect:CGRectMake(100, 0, 100, 200)];
        UIImage *res = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
 
        // 5.回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.imageView.image = res;
        }];
    }];
 
    // 6.添加依赖
 
    [op3 addDependency:op1];
    [op3 addDependency:op2];
 
    // 7.添加操作到队列中
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
11/18/2016 13:17 下午 posted in  Thread

GCD使用总结

GCD是iOS的一种底层多线程机制,今天总结一下GCD的常用API和概念,希望对大家的学习起到帮助作用。

##GCD队列的概念

在多线程开发当中,程序员只要将想做的事情定义好,并追加到DispatchQueue(派发队列)当中就好了。
派发队列分为两种,一种是串行队列(SerialDispatchQueue),一种是并行队列(ConcurrentDispatchQueue)。

一个任务就是一个block,比如,将任务添加到队列中的代码是:

dispatch_async(queue, block);

当给queue添加多个任务时,如果queue是串行队列,则它们按顺序一个个执行,同时处理的任务只有一个。
当queue是并行队列时,不论第一个任务是否结束,都会立刻开始执行后面的任务,也就是可以同时执行多个任务。
但是并行执行的任务数量取决于XNU内核,是不可控的。比如,如果同时执行10个任务,那么10个任务并不是开启10个线程,线程会根据任务执行情况复用,由系统控制。

##获取队列

系统提供了两个队列,一个是MainDispatchQueue,一个是GlobalDispatchQueue
前者会将任务插入主线程的RunLoop当中去执行,所以显然是个串行队列,我们可以使用它来更新UI。
后者则是一个全局的并行队列,有高、默认、低和后台4个优先级。

它们的获取方式如下:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRORITY_DEFAULT, 0)

###执行异步任务

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);     dispatch_async(queue, ^{ 
	//...
});

这个代码片段直接在子线程里执行了一个任务块。使用GCD方式任务是立即开始执行的
它不像操作队列那样可以手动启动,同样,缺点也是它的不可控性。

###令任务只执行一次

+ (id)shareInstance {      
+ static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         _shareInstance = [[self alloc] init];
     }); 
 }

这种只执行一次且线程安全的方式经常出现在单例构造器当中。

##任务组

有时候,我们希望多个任务同时(在多个线程里)执行,再他们都完成之后,再执行其他的任务,
于是可以建立一个分组,让多个任务形成一个组,下面的代码在组中多个任务都执行完毕之后再执行后续的任务:

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, ^{ NSLog(@"1"); });  
dispatch_group_async(group, queue, ^{ NSLog(@"2"); }); 
dispatch_group_async(group, queue, ^{ NSLog(@"3"); }); 
dispatch_group_async(group, queue, ^{ NSLog(@"4"); }); 
dispatch_group_async(group, queue, ^{ NSLog(@"5"); }); 

dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 
	NSLog(@"done"); 
});

###延迟执行任务

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 
	//... 
});

这段代码将会在10秒后将任务插入RunLoop当中。

dispatch_asycndispatch_sync

先前已经有过一个使用dispatch_async执行异步任务的一个例子,下面来看一段代码:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);     
dispatch_async(queue, ^{ 
	NSLog(@"1");  
});

NSLog(@"2");

这段代码首先获取了全局队列,也就是说,dispatch_async当中的任务被丢到了另一个线程里去执行,async在这里的含义是,当当前线程给子线程分配了block当中的任务之后,当前线程会立即执行,并不会发生阻塞,也就是异步的。那么,输出结果不是12就是21,因为我们没法把控两个线程RunLoop里到底是怎么执行的。

类似的,还有一个“同步”方法dispatch_sync,代码如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_sync(queue, ^{
NSLog(@"1");
}); 
NSLog(@"2");

这就意味着,当主线程将任务分给子线程后,主线程会等待子线程执行完毕,再继续执行自身的内容,那么结果显然就是12了。

需要注意的一点是,这里用的是全局队列,那如果把dispatch_sync的队列换成主线程队列会怎么样呢:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
	NSLog(@"1");
});

这段代码会发生死锁,因为:

  1. 主线程通过dispatch_sync把block交给主队列后,会等待block里的任务结束再往下走自身的任务,
  2. 而队列是先进先出的,block里的任务也在等待主队列当中排在它之前的任务都执行完了再走自己。

这种循环等待就形成了死锁。所以在主线程当中使用dispatch_sync将任务加到主队列是不可取的。

##创建队列

我们可以使用系统提供的函数获取主串行队列和全局并行队列,当然也可以自己手动创建串行和并行队列,代码为:

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);

在MRC下,手动创建的队列是需要释放的

dispatch_release(myConcurrentDispatchQueue);

手动创建的队列和默认优先级全局队列优先级等同,如果需要修改队列的优先级,需要:

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t targetQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(myConcurrentDispatchQueue, targetQueue);

上面的代码修改队列的优先级为后台级别,即与默认的后台优先级的全局队列等同。

##串行、并行队列与读写安全性

在向串行队列(SerialDispatchQueue)当中加入多个block任务后,一次只能同时执行一个block,如果生成了n个串行队列,并且向每个队列当中都添加了任务,那么系统就会启动n个线程来同时执行这些任务。

对于串行队列,正确的使用时机,是在需要解决数据/文件竞争问题时使用它。比如,我们可以令多个任务同时访问一块数据,这样会出现冲突,也可以把每个操作都加入到一个串行队列当中,因为串行队列一次只能执行一个线程的任务,所以不会出现冲突。

但是考虑到串行队列会因为上下文切换而拖慢系统性能,所以我们还是很期望采用并行队列的,来看下面的示例代码:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_async(queue, ^{
	//数据读取
});
dispatch_async(queue, ^{
	//数据读取2 
}); 
dispatch_async(queue, ^{
	//数据写入
});
dispatch_async(queue, ^{ 
	//数据读取3
});
dispatch_async(queue, ^{
	//数据读取4
});

显然,这5个操作的执行顺序是我们无法预期的,我们希望在读取1和读取2执行结束后,再执行写入,写入完成后再执行读取3和读取4。

为了实现这个效果,这里可以使用GCD的另一个API:

dispatch_barrier_async(queue, ^{
	//数据写入
});

这样就保证的写入操作的并发安全性。

对于没有数据竞争的并行操作,则可以使用并行队列(CONCURRENT)来实现。

##JOIN行为

CGD利用dispatch_group_wait来实现多个操作的join行为,代码如下:

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, ^{
	sleep(0.5);
	NSLog(@"1"); 
});
dispatch_group_async(group, queue, ^{
	sleep(1.5);
	NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
	sleep(2.5);
	NSLog(@"3");
});
NSLog(@"aaaaa");

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC);
if (dispatch_group_wait(group, time) == 0) { 
 	NSLog(@"已经全部执行完毕");
}else {
	NSLog(@"没有执行完毕");

NSLog(@"bbbbb");

这里起了3个异步线程放在一个组里,之后通过dispatch_time_t创建了一个超时时间(2秒),程序之后行,立即输出了aaaaa,这是主线程输出的,当遇到dispatch_group_wait时,主线程会被挂起,等待2秒,在等待的过程当中,子线程分别输出了1和2,2秒时间达到后,主线程发现组里的任务并没有全部结束,然后输出了bbbbb。

在这里,如果超时时间设置得比较长(比如5秒),那么会在2.5秒时第三个任务结束后,立即输出bbbbb,也就是说,当组中的任务全部执行完毕时,主线程就不再被阻塞了。

如果希望永久等待下去,时间可以设置为DISPATCH_TIME_FOREVER。

##并行循环
类似于C#的PLINQ,OC也可以让循环并行执行,在GCD当中有一个dispatch_apply函数:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(20, queue, ^(size_t i) { 
	NSLog(@"%lu", i); 
});

这段代码让i并行循环了20次,如果内部处理的是一个数组,就可以实现对数组的并行循环了,它的内部是dispatch_sync的同步操作,所以在执行这个循环的过程当中,当前线程会被阻塞。

##暂停和恢复

使用dispatch_suspend(queue)可以暂停队列中任务的执行,使用dispatch_result(queue)可以继续执行被暂停的队列。

11/17/2016 18:55 下午 posted in  GCD

Runtime中的实用函数总结

##关联对象:

###设置一个关联对象

void objc_setAssociatedObject(id object, void *key ,id value, objc_AssociationPolicy policy)

###获取关联对象

void objc_getAssociatedObject(id object, void *key)

###删除该对象所有的关联对象

void objc_removeAssociatedObjects(id object)

##消息传递

###向某个对象/父类 发送消息

objc_msgSend(Super)

##方法调配

###交换两个方法的实现

void method_exchangeImplementation(Method m1, Method m2)

###得到该法的指针

Method class_getInstanceMethod(Class aClass, SEL aSelector)

##动态创建对象
###创建新的类

Class objc_allocateClassPair(Class superclass, const char *name,size_t extraBytes)

###给类增加新的方法

BOOL class_addMethod(Class cls, SEL name, IMP imp,const char *types)

###注册新的类

void objc_registerClassPair(Class cls)

###获得对象的isa指针所指向的对象

Class object_getClass(id obj)
11/17/2016 18:43 下午 posted in  Runtime

Objective-C中的Runtime实现

iOS的开发语言objective-c,它的真实面目是它不是真正的面向对象语言,而抽象理解为此而已。其实它就是C+,有个公式可以很好地诠释那就是

OC = C + Runtime; 接下来我们就好好讲讲在Runtime下的objc-class。准备资料,objc4-646/runtime。

##Class定义
###小小说明一下objc-api.h里的OBJC_ISA_AVAILABILITY:

/*介绍一下__attribute__((deprecated))的作用,__attribute是给函数、变量、类做属性说明的关键字,deprecated是弃用原先的进行兼容

若是__OBJC2__,原先的类,编译器发出警告*/

#if !defined(OBJC_ISA_AVAILABILITY)
#   if __OBJC2__
#       define OBJC_ISA_AVAILABILITY  __attribute__((deprecated))
#   else
#       define OBJC_ISA_AVAILABILITY  /* still available */
#   endif
#endif

typedef struct objc_method *Method;
typedef struct objc_ivar *Ivar;
typedef struct objc_category *Category;
typedef struct objc_property *objc_property_t;

###接下来就是runtime.h里的Class的定义:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针
     
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;//父类
    const char *name                                         OBJC2_UNAVAILABLE;//类名
    long version                                             OBJC2_UNAVAILABLE;//类版本
    long info                                                OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)
    long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//缓存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//协议
#endif
     
} OBJC2_UNAVAILABLE;

###class初始化
Runtime的行为之一就是initialize。在程序运行过程中,它会在你程序中每个类调用一次initialize。这个调用的时间发生在你的类接收到消息之前,但是在它的超类接收到initialize之后。

//在苹果的官方Runtime中有个objc-initialize.m文件,进行解读
typedef struct _objc_initializing_classes {
    int classesAllocated;//类是否分配存在
    Class *metaclasses;//类的父类,如果没有父类那就是自身
} _objc_initializing_classes;//初始化一个对象_objc_initializing_classes
 
//将所有的类存储在静态链表中,以待接下来的接收和发送消息
static _objc_initializing_classes *_fetchInitializingClassList(BOOL create)
{
    _objc_pthread_data *data;
    //list为类链表
    _objc_initializing_classes *list;
    Class *classes;
     
    data = _objc_fetch_pthread_data(create);
    if (data == nil) return nil;
     
    //链表增加类节点
    list = data->initializingClasses;
    if (list == nil) {
        if (!create) {
            return nil;
        } else {
            list = (_objc_initializing_classes *)
            _calloc_internal(1, sizeof(_objc_initializing_classes));
            data->initializingClasses = list;
        }
    }
    //将创建的classes接在metaclasses后
    classes = list->metaclasses;
    if (classes == nil) {
        // If _objc_initializing_classes exists, allocate metaclass array,
        // even if create == NO.
        // Allow 4 simultaneous class inits on this thread before realloc.
        list->classesAllocated = 4;
        classes = (Class *)
        _calloc_internal(list->classesAllocated, sizeof(Class));
        list->metaclasses = classes;
    }
    return list;
}

###runtime下Class的各项操作(重要几个)

  • add*(增加)
  1. static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, BOOL replace);//增加方法
  2. BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);//增加类方法
  3. BOOL class_addIvar(Class cls, const char *name, size_t size,uint8_t alignment, const char *type);//增加实例变量
  4. static BOOL class_addProperty(Class cls, const char *name,const objc_property_attribute_t *attrs, unsigned int count,BOOL replace);//增加属性
  • replace*(修改)
  1. IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); //修改方法
  2. void class_replaceProperty(Class cls, const char *name,const objc_property_attribute_t *attrs, unsigned int n);//修改属性
  • get*(获取)
  1. static Class getClass(const char *name);//获取类
  2. static ivar_t *getIvar(Class cls, const char *name);//获取类变量(static相当于“+“)
  3. Method class_getInstanceMethod(Class cls, SEL sel);//获取实例方法
  4. static Method class_getMethod(Class cls, SEL sel);;//获取类方法
  5. static Protocol *getProtocol(const char *name);//增加协议
  • set*(设置)
  1. objc_class::setInitialized();//set的initialized初始化
  2. static Class setSuperclass(Class cls, Class newSuper);//设置父类
  3. 其他还有类似于 void *objc_destructInstance(id obj);//摧毁实例对象等等

###Class的重要函数

  • get*(获取)
  1. object_getClass(id obj);
  2. IMP object_getMethodImplementation(id obj, SEL name);//获得实例方法实现
  3. Ivar object_getInstanceVariable(id obj, const char *name, void **value)//获取实例属性
  • set*(设置)
  1. Class object_setClass(id obj, Class cls);
  2. Ivar object_setInstanceVariable(id obj, const char *name, void *value);//设置实例属性
  3. void object_setIvar(id obj, Ivar ivar, id value);//设置实例变量
  • 其他
  1. static void class_resolveClassMethod(Class cls, SEL sel, id inst);//动态添加类方法,不必在乎方法是否存在
  2. static void class_resolveInstanceMethod(Class cls, SEL sel, id inst);//动态添加实现方法,不必在乎方法是否存在
  3. unsigned class_createInstancesFromZone(Class cls, size_t extraBytes, void *zone,id *results, unsigned num_requested);//创建实例存储空间
  • 消息转发
  1. void instrumentObjcMessageSends(BOOL flag);//flag传入YES,运行时发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。
11/17/2016 14:59 下午 posted in  Runtime

iOS中的Runtime详解

转载:简书:iOS开发之runtime详解

##简介
Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的
比如:

[receiver message];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)

以上你可能看不出它的价值,但是我们需要了解的是 Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。

Runtime 基本是用 C 和汇编写的,由此可见苹果为了动态系统的高效而做出的努力。苹果和 GNU 各自维护一个开源的 Runtime 版本,这两个版本之间都在努力保持一致。

点击这里下载苹果维护的开源代码。

##Runtime 的作用
Objc 在三种层面上与 Runtime 系统进行交互:

  1. 通过 Objective-C 源代码
  2. 通过 Foundation 框架的 NSObject 类定义的方法
  3. 通过对 Runtime 库函数的直接调用

###Objective-C 源代码
多数情况我们只需要编写 OC 代码即可,Runtime 系统自动在幕后搞定一切,还记得简介中如果我们调用方法,编译器会将 OC 代码转换成运行时代码,在运行时确定数据结构和函数。

###通过 Foundation 框架的 NSObject 类定义的方法
Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类时个例外,它是个抽象超类)

一些情况下,NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 -description 方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。

还有一些 NSObject 的方法可以从 Runtime 系统中获取信息,允许对象进行自我检查。例如:

  • -class方法返回对象的类;
  • -isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
  • -respondsToSelector: 检查对象能否响应指定的消息;
  • -conformsToProtocol:检查对象是否实现了指定协议类的方法;
  • -methodForSelector: 返回指定方法实现的地址。

###通过对 Runtime 库函数的直接调用

Runtime 系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入#import <objc/runtime.h>头文件即可。

许多函数可以让你使用纯 C 代码来实现 Objc 中同样的功能。除非是写一些 Objc 与其他语言的桥接或是底层的 debug 工作,你在写 Objc 代码时一般不会用到这些 C 语言函数。对于公共接口都有哪些,后面会讲到。我将会参考苹果官方的 API 文档Objective-C Runtime Reference

##Runtime 的术语的数据结构
要想全面了解 Runtime 机制,我们必须先了解 Runtime 的一些术语,他们都对应着数据结构。

###SEL
它是selector在 Objc 中的表示(Swift 中是 Selector 类)。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:

typedef struct objc_selector *SEL;

我们可以看出它是个映射到方法的 C 字符串,你可以通过 Objc 编译器器命令@selector() 或者 Runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器。

注意:不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。

###id

id 是一个参数类型,它是指向某个类的实例的指针。定义如下:

typedef struct objc_object *id;
struct objc_object { Class isa; };

以上定义,看到 objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对象所属的类。

注意:isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。

PS:KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型,详见:KVO章节

###Class

typedef struct objc_class *Class;

Class 其实是指向 objc_class 结构体的指针。objc_class 的数据结构如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

objc_class 可以看到,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。
其中 objc_ivar_listobjc_method_list 分别是成员变量列表和方法列表:

// 成员变量列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

由此可见,我们可以动态修改 *methodList 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。这里可以参考下美团技术团队的文章:深入理解 Objective-C: Category

objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar 则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法的信息则由 objc_method 结构体存储。

值得注意的时,objc_class 中也有一个 isa 指针,这说明 Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做 Meta Class(元类) 的东西,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。

我们所熟悉的类方法,就源自于 Meta Class。我们可以理解为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。

当你发出一个类似 [NSObject alloc](类方法) 的消息时,实际上,这个消息被发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(Root Meta Class)的实例。所有元类的 isa 指针最终都指向根元类。

所以当 [NSObject alloc] 这条消息发送给类对象的时候,运行时代码 objc_msgSend() 会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。

1330553-81f64a11ad20

上图实现是 super_class 指针,虚线时 isa 指针。而根元类的父类是 NSObject,isa指向了自己。而 NSObject 没有父类。

最后 objc_class 中还有一个 objc_cache ,缓存,它的作用很重要,后面会提到。

###Method
Method 代表类中某个方法的类型

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

objc_method 存储了方法名,方法类型和方法实现:

  • 方法名类型为 SEL
  • 方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型
  • method_imp 指向了方法的实现,本质是一个函数指针

###Ivar
Ivar 是表示成员变量的类型。

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}

其中 ivar_offset 是基地址偏移字节

###IMP

IMP在objc.h中的定义是:

typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。

如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache 中会提到。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。

而一个确定的方法也只有唯一的一组 id 和 SEL 参数。

###Cache

Cache 定义如下:

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。

Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。

###Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意:返回的是属性列表,列表中每个元素都是一个 objc_property_t 指针

##实例

#import <Foundation/Foundation.h>

@interface Person : NSObject

/** 姓名 */
@property (strong, nonatomic) NSString *name;

/** age */
@property (assign, nonatomic) int age;

/** weight */
@property (assign, nonatomic) double weight;

@end

以上是一个 Person 类,有3个属性。让我们用上述方法获取类的运行时属性。

unsigned int outCount = 0;

    objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

    NSLog(@"%d", outCount);

    for (NSInteger i = 0; i < outCount; i++) {
        NSString *name = @(property_getName(properties[i]));
        NSString *attributes = @(property_getAttributes(properties[i]));
        NSLog(@"%@--------%@", name, attributes);
    }

打印结果如下:

2014-11-10 11:27:28.473 test[2321:451525] 3
2014-11-10 11:27:28.473 test[2321:451525] name--------T@"NSString",&,N,V_name
2014-11-10 11:27:28.473 test[2321:451525] age--------Ti,N,V_age
2014-11-10 11:27:28.474 test[2321:451525] weight--------Td,N,V_weight

property_getName 用来查找属性的名称,返回 c 字符串。property_getAttributes 函数挖掘属性的真实名称和 @encode 类型,返回 c 字符串。

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

class_getProperty 和 protocol_getProperty 通过给出属性名在类和协议中获得属性的引用。

##消息

一些 Runtime 术语讲完了,接下来就要说到消息了。体会苹果官方文档中的 messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。

这里要清楚一点,objc_msgSend 方法看清来好像返回了数据,其实objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息发送的步骤(如下图):

1330553-87d03a7c0971c730

  1. 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
  2. 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
  3. 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
  4. 如果 cache 找不到就找类的方法列表中是否有对应的方法。
  5. 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
  6. 如果还找不到,就要开始进入动态方法解析了,后面会提到。

在消息的传递中,编译器会根据情况在 objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret 这四个方法中选择一个调用。如果消息是传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用名字带有 stret 的函数。

###方法中的隐藏参数
疑问:我们经常用到关键字 self ,但是 self 是如何获取当前方法的对象呢?

其实,这也是 Runtime 系统的作用,self 实在方法运行时被动态传入的。

objc_msgSend 找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,它还将传递两个隐藏参数:

  • 接受消息的对象(self 所指向的内容,当前方法的对象指针)
  • 方法选择器(_cmd 指向的内容,当前方法的 SEL 指针)
    因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们时在代码被编译时被插入方法实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。

这两个参数中, self更实用。它是在方法实现中访问消息接收者对象的实例变量的途径。

这时我们可能会想到另一个关键字 super ,实际上 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定的父类。 receiver 仍然是 self 本身,当我们想通过 [super class] 获取父类时,编译器其实是将指向 self 的 id 指针和 class 的 SEL 传递给了 objc_msgSendSuper 函数。只有在 NSObject 类中才能找到 class 方法,然后 class 方法底层被转换为 object_getClass(), 接着底层编译器将代码转换为 objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向 self 的 id 指针,与调用 [self class] 相同,所以我们得到的永远都是 self 的类型。因此你会发现:

// 这句话并不能获取父类的类型,只能获取当前类的类型名
NSLog(@"%@", NSStringFromClass([super class]));

###获取方法地址

NSObject 类中有一个实例方法:methodForSelector,你可以用它来获取某个方法选择器对应的 IMP ,举个例子:

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

当方法被当做函数调用时,两个隐藏参数也必须明确给出,上面的例子调用了1000次函数,你也可以尝试给 target 发送1000次 setFilled: 消息会花多久。

虽然可以更高效的调用方法,但是这种做法很少用,除非时需要持续大量重复调用某个方法的情况,才会选择使用以免消息发送泛滥。

注意:methodForSelector:方法是由 Runtime 系统提供的,而不是 Objc 自身的特性

##动态方法解析
你可以动态提供一个方法实现。如果我们使用关键字 @dynamic 在类的实现文件中修饰一个属性,表明我们会为这个属性动态提供存取方法,编译器不会再默认为我们生成这个属性的 setter 和 getter 方法了,需要我们自己提供。

@dynamic propertyName;

这时,我们可以通过分别重载 resolveInstanceMethod:resolveClassMethod: 方法添加实例方法实现和类方法实现。

当 Runtime 系统在 Cache 和类的方法列表(包括父类)中找不到要执行的方法时,Runtime 会调用 resolveInstanceMethod: 或 resolveClassMethod: 来给我们一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子为 resolveThisMethodDynamically 方法添加了实现内容,就是 dynamicMethodIMP 方法中的代码。其中 "v@:" 表示返回值和参数,这个符号表示的含义见:Type Encoding

注意:动态方法解析会在消息转发机制侵入前执行,动态方法解析器将会首先给予提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,就让 resolveInstanceMethod: 方法返回 NO。

##消息转发

1330553-400bc5bde4db1725

###重定向
消息转发机制执行前,Runtime 系统允许我们替换消息的接收者为其他对象。通过- (id)forwardingTargetForSelector:(SEL)aSelector方法。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果此方法返回 nil 或者 self,则会计入消息转发机制(forwardInvocation:),否则将向返回的对象重新发送消息。

###转发
当动态方法解析不做处理返回 NO 时,则会触发消息转发机制。这时 forwardInvocation: 方法会被执行,我们可以重写这个方法来自定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

唯一参数是个 NSInvocation 类型的对象,该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation: 方法来对不能处理的消息做一些处理。也可以将消息转发给其他对象处理,而不抛出错误。

注意:参数 anInvocation 是从哪来的?在 forwardInvocation: 消息发送前,Runtime 系统会向对象发送 methodSignatureForSelector: 消息,并取到返回的方法签名用于生成 NSInvocation 对象。所以重写 forwardInvocation: 的同时也要重写 methodSignatureForSelector: 方法,否则会抛异常。

当一个对象由于没有相应的方法实现而无法相应某消息时,运行时系统将通过 forwardInvocation: 消息通知该对象。每个对象都继承了 forwardInvocation: 方法。但是, NSObject 中的方法实现只是简单的调用了 doesNotRecognizeSelector:。通过实现自己的 forwardInvocation: 方法,我们可以将消息转发给其他对象。

forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。这一切都取决于方法的具体实现。

注意:forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。所以,如果我们向往一个对 象将一个消息转发给其他对象时,要确保这个对象不能有该消息的所对应的方法。否则,forwardInvocation:将不可 能被调用。

###转发和多继承
转发和继承相似,可用于为 Objc 编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好像它把另一个对象中的方法接过来或者“继承”过来一样。

1330553-c7ef6392ecc9ee9d

这使得在不同继承体系分支下的两个类可以实现“继承”对方的方法,在上图中 Warrior 和 Diplomat 没有继承关系,但是 Warrior 将 negotiate 消息转发给了 Diplomat 后,就好似 Diplomat 是 Warrior 的超类一样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

###转发与继承
虽然转发可以实现继承的功能,但是 NSObject 还是必须表面上很严谨,像 respondsToSelector: 和 isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。

如果上图中的 Warrior 对象被问到是否能响应 negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

回答当然是 NO, 尽管它能接受 negotiate 消息而不报错,因为它靠转发消息给 Diplomat 类响应消息。

如果你就是想要让别人以为 Warrior 继承到了 Diplomat 的 negotiate 方法,你得重新实现 respondsToSelector: 和 isKindOfClass: 来加入你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了 respondsToSelector: 和 isKindOfClass: 之外,instancesRespondToSelector: 中也应该写一份转发算法。如果使用了协议,conformsToProtocol: 同样也要加入到这一行列中。

如果一个对象想要转发它接受的任何远程消息,它得给出一个方法标签来返回准确的方法描述 methodSignatureForSelector:,这个方法会最终响应被转发的消息。从而生成一个确定的 NSInvocation 对象描述消息和消息参数。这个方法最终响应被转发的消息。它需要像下面这样实现:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

##健壮的实例变量(Non Fragile ivars)

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量了。当一个类被编译时,实例变量的内存布局就形成了,它表明访问类的实例变量的位置。实例变量一次根据自己所占空间而产生位移:
1330553-bcbc243a281ef8d9

上图左是 NSObject 类的实例变量布局。右边是我们写的类的布局。这样子有一个很大的缺陷,就是缺乏拓展性。哪天苹果更新了 NSObject 类的话,就会出现问题:

1330553-33263710847f6d86

我们自定义的类的区域和父类的区域重叠了。只有苹果将父类改为以前的布局才能拯救我们,但这样导致它们不能再拓展它们的框架了,因为成员变量布局被固定住了。在脆弱的实例变量(Fragile ivar)环境下,需要我们重新编译继承自 Apple 的类来恢复兼容。如果是健壮的实例变量的话,如下图:

在健壮的实例变量下,编译器生成的实例变量布局跟以前一样,但是当 Runtime 系统检测到与父类有部分重叠时它会调整你新添加的实例变量的位移,那样你再子类中新添加的成员变量就被保护起来了。

注意:在健壮的实例变量下,不要使用 siof(SomeClass),而是用 class_getInstanceSize([SomeClass class]) 代替;也不要使用 offsetof(SomeClass, SomeIvar),而要使用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 来代替。

##总结
我们让自己的类继承自 NSObject 不仅仅是因为基类有很多复杂的内存分配问题,更是因为这使得我们可以享受到 Runtime 系统带来的便利。

虽然平时我们很少会考虑一句简单的调用方法,发送消息底层所做的复杂的操作,但深入理解 Runtime 系统的细节使得我们可以利用消息机制写出功能更强大的代码。

11/17/2016 09:44 上午 posted in  Runtime

iOS中的唯一标识

##IDFA(广告标识符)-identifierForldentifier
依赖:AdSupport.framework
系统支持:iOS6及以上系统
获取方式:[ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString
定义: 由数字和字母组成的用来标识唯一设备的字符串。
特点

每个设备只有一个IDFA,不同APP在同一设备上获取IDFA的结果是一样的
设备重启不会产生新的IDFA
但IDFA存在重新生成的情况:
用户完全重置系统(设置程序 -> 通用 -> 还原 -> 还原位置与隐私)
用户明确还原广告(设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符)
注意:Appstore禁止不使用广告而采集IDFA的app上架。请参考:
如何防止应用因获取IDFA被AppStore拒绝

##IDFV-identifierForVendor
依赖:UIKit.framework
系统支持:iOS6及以上系统
获取方式:[UIDevice currentDevice].identifierForVendor.UUIDString
定义:由数字和字母组成的用来标识唯一设备的字符串。
特点: 根据vendor的值,如果vendor相同,则返回同一字符串;如果vendor不同,则返回不同的字符串。
vendor解释:英文解释为卖家,小贩。根据xcode文档解释,正常情况下,会根据App Store提供的数据进行判断。但是如果app不是通过app store进行安装的(如企业应用或开发调试阶段),那么会根据bundle ID判断。
判断准则

如:com.example.app1和com.example.app2,只有最后的后缀不同,所以会产生相同的vendor ID

详细参考:iOS唯一标示符引导
:https://possiblemobile.com/2013/04/unique-identifiers/

简书:iOS怎样获取设备唯一标识符

10/27/2016 14:22 下午 posted in  apple

iOS app启动的方式(launchOptions)

iOS app启动的方式有哪些:

  1. 自己启动(用户手动点击启动)
  2. urlscheme启动(关于urlScheme的详解点击打开链接)
  3. 本地通知启动 (自己写的本地通知启动,蓝牙模块的启动,地理围栏的启动)
  4. 远程通知启动 (后台服务器的推送通知)

在appdelegate.m文件中

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

系统给的一些关于launchoption的字典里面的内容

UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsURLKey                   NS_AVAILABLE_IOS(3_0); // userInfo contains NSURL with launch URL
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsSourceApplicationKey     NS_AVAILABLE_IOS(3_0); // userInfo contains NSString with launch app bundle ID
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsRemoteNotificationKey    NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED; // userInfo contains NSDictionary with payload
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsLocalNotificationKey     NS_AVAILABLE_IOS(4_0) __TVOS_PROHIBITED; // userInfo contains a UILocalNotification
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsAnnotationKey            NS_AVAILABLE_IOS(3_2); // userInfo contains object with annotation property list
UIKIT_EXTERN NSString *const UIApplicationProtectedDataWillBecomeUnavailable    NS_AVAILABLE_IOS(4_0);
UIKIT_EXTERN NSString *const UIApplicationProtectedDataDidBecomeAvailable       NS_AVAILABLE_IOS(4_0);
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsLocationKey              NS_AVAILABLE_IOS(4_0); // app was launched in response to a CoreLocation event.
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsNewsstandDownloadsKey    NS_AVAILABLE_IOS(5_0) __TVOS_PROHIBITED; // userInfo contains an NSArray of NKAssetDownload identifiers
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsBluetoothCentralsKey     NS_AVAILABLE_IOS(7_0); // userInfo contains an NSArray of CBCentralManager restore identifiers
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsBluetoothPeripheralsKey  NS_AVAILABLE_IOS(7_0); // userInfo contains an NSArray of CBPeripheralManager restore identifiers
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsShortcutItemKey          NS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED; // userInfo contains the UIApplicationShortcutItem used to launch the app.

// Key in options dict passed to application:[will | did]FinishLaunchingWithOptions and info for UIApplicationDidFinishLaunchingNotification
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsUserActivityDictionaryKey    NS_AVAILABLE_IOS(8_0); // Sub-Dictionary present in launch options when user activity is present
UIKIT_EXTERN NSString *const UIApplicationLaunchOptionsUserActivityTypeKey          NS_AVAILABLE_IOS(8_0); // Key in user activity dictionary for the activity type

UIKIT_EXTERN NSString *const UIApplicationOpenSettingsURLString NS_AVAILABLE_IOS(8_0);

// Keys for application:openURL:options:
UIKIT_EXTERN NSString *const UIApplicationOpenURLOptionsSourceApplicationKey NS_AVAILABLE_IOS(9_0);   // value is an NSString containing the bundle ID of the originating application
UIKIT_EXTERN NSString *const UIApplicationOpenURLOptionsAnnotationKey NS_AVAILABLE_IOS(9_0);   // value is a property-list typed object corresponding to what the originating application passed in UIDocumentInteractionController's annotation property
UIKIT_EXTERN NSString *const UIApplicationOpenURLOptionsOpenInPlaceKey NS_AVAILABLE_IOS(9_0);   // value is a bool NSNumber, set to YES if the file needs to be copied before use

如何判断如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    
    //用户自己点击启动
    if(!launchOptions)
    {
        NSLog(@"用户点击app启动");
    }
    else
    {
        NSURL *url = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey];
        //app 通过urlscheme启动
        if (url) {
            NSLog(@"app 通过urlscheme启动 url = %@",url);
        }
        UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
        //通过本地通知启动
        if(localNotification)
        {
            NSLog(@"app 通过本地通知启动 localNotification = %@",localNotification);
        }
        NSDictionary *remoteCotificationDic = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
        //远程通知启动
        if(remoteCotificationDic)
        {
            NSLog(@"app 通过远程推送通知启动 remoteCotificationDic = %@",remoteCotificationDic);
        }
        //位置变动的通知
        if(launchOptions[@"UIApplicationLaunchOptionsLocationKey"])
        {
            //写你的重新定位的代码
        }
    }
    
    
    
    
    return YES;
}

09/26/2016 16:14 下午 posted in  UIKit