AVPlayer详解系列(一)参数设置

最近工作内容基本都是围绕视频播放展开的,从AVPlayer到IJKPlayer,期间遇到挺多问题,趟了很多bug,也总结了一些心得。对AVPlayer了解的更多一些,因为涉及点比较多,所以打算做一个系列详尽的写一下这部分内容。希望大家多多支持,有问题的地方欢迎指正。思维导图 先来一张思维导图,作为这篇文章的目录索引:为什么使用AVPlayer: 首先在iOS平台使用播放视频,可用的选项一般有这四个,他们各自的作用和功能如下:使用环境 优点 缺点MPMoviePlayerController MediaPlayer 简单易用 不可定制AVPlayerViewController AVKit 简单易用 不可定制AVPlayer AVFoundation 可定制度高,功能强大 不支持流媒体IJKPlayer IJKMediaFramework 定制度高,支持流媒体播放 使用稍复杂由此可以看出,如果我们不做直播功能AVPlayer就是一个最优的选择。 另外AVPlayer是一个可以播放任何格式的全功能影音播放器 支持视频格式: WMV,AVI,MKV,RMVB,RM,XVID,MP4,3GP,MPG等。 支持音频格式:MP3,WMA,RM,ACC,OGG,APE,FLAC,FLV等。 支持视频格式: MP4,MOV,M4V,3GP,AVI等。 支持音频格式:MP3,AAC,WAV,AMR,M4A等。 详见AVPlayer支持的视频格式 ##如何使用 AVPlayer存在于AVFoundation框架,我们使用时需要导入: #import <AVFoundation/AVFoundation.h> 几个播放相关的参数 在创建一个播放器之前我们需要先了解一些播放器相关的类AVPlayer:控制播放器的播放,暂停,播放速度 AVURLAsset : AVAsset 的一个子类,使用 URL 进行实例化,实例化对象包换 URL 对应视频资源的所有信息。 AVPlayerItem:管理资源对象,提供播放数据源 AVPlayerLayer:负责显示视频,如果没有添加该类,只有声音没有画面我们这片文章就围绕这几个参数展开,光说这些你可能还有点不明白,那我们就围绕一个最简单的播放器做起,一点点扩展功能,在具体讲解这几个参数的作用。 最简单的播放器 根据上面描述,我们知道AVPlayer是播放的必要条件,所以我们可以构建的极简播放器就是: NSURL *playUrl = [NSURL URLWithString:@"http://baobab.wdjcdn.com/14573563182394.mp4"]; self.player = [[AVPlayer alloc] initWithURL:playUrl]; [self.player play];是不是很简单,只有三行代码! 但是它太简单了,仅可以完成音频的播放,连画面都没有。回看上面播放相关类的介绍,是因为缺少AVPlayerLayer;作为一个播放器,我不能只播放一条视频啊,我还想根据需要切换视频,那我们就得把AVPlayerItem也加上。 加上这两个属性之后的播放器是这样的: NSURL *playUrl = [NSURL URLWithString:@"http://baobab.wdjcdn.com/14573563182394.mp4"]; self.playerItem = [AVPlayerItem playerItemWithURL:playUrl]; //如果要切换视频需要调AVPlayer的replaceCurrentItemWithPlayerItem:方法 self.player = [AVPlayer playerWithPlayerItem:_playerItem]; self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.frame = _videoView.bounds; //放置播放器的视图 [self.videoView.layer addSublayer:self.playerLayer]; [_player play];现在的播放器稍微完整了一些,我们在自己创建的容器里可以看到画面了! 更多功能 但是它作为一个视频播放器,还是有很多不能让人满意的地方。例如:没有暂停、快进快退、倍速播放等,另外如果遇到url错误是不是还要有播放失败的提示,还有播放完成的相关提示。 为完成这些,我们需要对AVPlayerItem和AVPlayerLayer进一步了解一下。 一、AVPlayer的控制 前面讲过该类是控制视频播放行为的,他的使用比较简单。 播放视频: [self.player play];暂停视频: [self.player pause];更改速度: self.player.rate = 1.5;//注意更改播放速度要在视频开始播放之后才会生效还有一下其他的控制,我们可以调转到系统API进行查看 二、AVPlayerItem的控制 AVPlayerItem作为资源管理对象,它控制着视频从创建到销毁的诸多状态。 1、播放状态 status typedef NS_ENUM(NSInteger, AVPlayerItemStatus) { AVPlayerItemStatusUnknown,//未知 AVPlayerItemStatusReadyToPlay,//准备播放 AVPlayerItemStatusFailed//播放失败 };我们使用KVO监测playItem.status,可以获取播放状态的变化 [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];在监听回调中: - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([object isKindOfClass:[AVPlayerItem class]]) { if ([keyPath isEqualToString:@"status"]) { switch (_playerItem.status) { case AVPlayerItemStatusReadyToPlay: //推荐将视频播放放在这里 [self play]; break; case AVPlayerItemStatusUnknown: NSLog(@"AVPlayerItemStatusUnknown"); break; case AVPlayerItemStatusFailed: NSLog(@"AVPlayerItemStatusFailed") break; default: break; } } } }虽然设置完播放配置我们可以直接调用[self.player play];进行播放,但是更稳妥的方法是在回调收到AVPlayerItemStatusReadyToPlay时进行播放 2、视频的时间信息 在AVPlayer中时间的表示有一个专门的结构体CMTime typedef struct{ CMTimeValue value; // 帧数 CMTimeScale timescale; // 帧率(影片每秒有几帧) CMTimeFlags flags; CMTimeEpoch epoch; } CMTime;CMTime是以分数的形式表示时间,value表示分子,timescale表示分母,flags是位掩码,表示时间的指定状态。 获取当前播放时间,可以用value/timescale的方式: float currentTime = self.playItem.currentTime.value/item.currentTime.timescale;还有一种利用系统提供的方法,我们用它获取视频总时间: float totalTime = CMTimeGetSeconds(item.duration);如果我们想要添加一个计时的标签不断更新当前的播放进度,有一个系统的方法: - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;方法名如其意, “添加周期时间观察者” ,参数1 interal 为CMTime 类型的,参数2 queue为串行队列,如果传入NULL就是默认主线程,参数3 为CMTime 的block类型。 简而言之就是,每隔一段时间后执行 block。 比如:我们把interval设置成CMTimeMake(1, 10),在block里面刷新label,就是一秒钟刷新10次。 正常观察播放进度一秒钟一次就行了,所以可以这么写: [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:nil usingBlock:^(CMTime time) { AVPlayerItem *item = WeakSelf.playerItem; NSInteger currentTime = item.currentTime.value/item.currentTime.timescale; NSLog(@"当前播放时间:%ld",currentTime); }];3、loadedTimeRange 缓存时间 获取视频的缓存情况我们需要监听playerItem的loadedTimeRanges属性 [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];在KVO的回调里: if ([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array = _playerItem.loadedTimeRanges; CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围 float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度 NSLog(@"当前缓冲时间:%f",totalBuffer); }4、playbackBufferEmpty 监听playbackBufferEmpty我们可以获取当缓存不够,视频加载不出来的情况: [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];在KVO回调里: if ([keyPath isEqualToString:@"playbackBufferEmpty"]) { //some code show loading }5、playbackLikelyToKeepUp playbackLikelyToKeepUp和playbackBufferEmpty 是一对,用于监听缓存足够播放的状态 [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil]; /* ... */ if([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) { //由于 AVPlayer 缓存不足就会自动暂停,所以缓存充足了需要手动播放,才能继续播放 [_player play]; }AVURLAsset 播放视频只需一个url就能进行这样太不安全了,别人可以轻易的抓包盗链,为此我们需要为视频链接做一个请求头的认证,这个功能可以借助AVURLAsset完成。 AVPlayerItem除了可以用URL初始化,还可以用AVAsset初始化,而AVAsset不能直接使用,我们看下AVURLAsset的一个初始化方法: /*! @param URL An instance of NSURL that references a media resource. @param options An instance of NSDictionary that contains keys for specifying options for the initialization of the AVURLAsset. See AVURLAssetPreferPreciseDurationAndTimingKey and AVURLAssetReferenceRestrictionsKey above. */ + (instancetype)URLAssetWithURL:(NSURL *)URL options:(nullable NSDictionary<NSString *, id> *)options;AVURLAssetPreferPreciseDurationAndTimingKey.这个key对应的value是一个布尔值, 用来表明资源是否需要为时长的精确展示,以及随机时间内容的读取进行提前准备。 除了这个苹果官方介绍的功能外,他还可以设置请求头,这个算是隐藏功能了,因为苹果并没有明说这个功能,我是费了很大劲找到的。 NSMutableDictionary * headers = [NSMutableDictionary dictionary]; [headers setObject:@"yourHeader"forKey:@"User-Agent"]; self.urlAsset = [AVURLAsset URLAssetWithURL:self.videoURL options:@{@"AVURLAssetHTTPHeaderFieldsKey" : headers}]; // 初始化playerItem self.playerItem = [AVPlayerItem playerItemWithAsset:self.urlAsset];补充:后来得知这个参数是非公开的API,但是经多人测试项目上线不受影响。 播放相关通知 1、声音类: //声音被打断的通知(电话打来) AVAudioSessionInterruptionNotification //耳机插入和拔出的通知 AVAudioSessionRouteChangeNotification根据userInfo判断具体状态 2、播放类 //播放完成 AVPlayerItemDidPlayToEndTimeNotification //播放失败 AVPlayerItemFailedToPlayToEndTimeNotification //异常中断 AVPlayerItemPlaybackStalledNotification对于播放完成的通知我们可以这么写: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerMovieFinish:) name:AVPlayerItemDidPlayToEndTimeNotification object:[self.player currentItem]];3、系统状态 //进入后台 UIApplicationWillResignActiveNotification //返回前台 UIApplicationDidBecomeActiveNotification提示:所有通知和KVO的使用我们都要记得在不用时remove掉。 小结 视频播放相关的知识比较多,细节的方面需要一点一点去扣。暂且写这么多吧,以后有需要会及时补充。 参考: ZFPlayer AVPlayer那些坑 如果还有什么不理解的可以简书私信问我,或者查看我写的Demo,欢迎star- ( ゜- ゜)つロ乾杯~

面试题总结(From J_Knight)

测试前一段时间看了J_Knight的2017年5月iOS找人心得(附面试题)。作为一个在编程前线奋斗了将近两年的iOS从业人员,面对这些题目时,有些竟感觉生疏,甚至答不上来,很是惭愧。个人感觉,像runtime、线程、信号量相关的偏底层知识虽然平时基本用不到。特别是很多人可能都没参与过稍复杂项目的开发,优化,这些内容对于很多新手iOS开发来说只存在于理论。但并不是说这些知识不重要,相反,它是我们进阶的必经之路。此篇文章的目的一方面自己整理,一方面希望和大家共同学习进步。以下内容多数为整理,时间仓促可能有不准确的地方,如果缺漏,欢迎指正。部分答案出处:iOSInterviewQuestions 基础 为什么说Objective-C是一门动态语言? 动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化。比如Ruby、Python等就是动态语言,而C、C++等语言则不属于动态语言。 所谓的动态类型语言,意思就是类型的检查是在运行时做的。 1、动态类型。 如id类型。实际上静态类型因为其固定性和可预知性而使用得更加广泛。静态类型是强类型,而动态类型属于弱类型。运行时决定接收者。 2、动态绑定。让代码在运行时判断需要调用什么方法,而不是在编译时。与其他面向对象语言一样,方法调用和代码并没有在编译时连接在一起,而是在消息发送时才进行连接。运行时决定调用哪个方法。 3、动态载入。让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。 讲一下MVC和MVVM,MVP iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构 为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别? 防止循环引用。 另外,不建议使用assign weak 当计数器为0 时对象被释放,地址指针就置为了nil 了。 assign 当计数器为0 时 对象被释放,地址指针还是指向那个地址,就会产生野指针 datasource协议里面东西是跟内容有关的,主要是cell的构造函数,各种属性 delegate协议里面的方法主要是操作相关的,移动编辑之类的,你都写上要用什么方法自己去翻就是了 delegate控制的是UI,是上层的东西;而datasource控制的是数据。他们本质都是回调,只是回调的对象不同。 block 和 delegate 都可以通知外面。block 更轻型,使用更简单,能够直接访问上下文,这样类中不需要存储临时数据,使用 block 的代码通常会在同一个地方,这样读代码也连贯。delegate 更重一些,需要实现接口,它的方法分离开来,很多时候需要存储一些临时数据,另外相关的代码会被分离到各处,没有 block 好读。 多个相关方法,避免循环引用,建议用delegate。 临时性的,只在栈中,需要存储,只调用一次,一个完成周期用block 属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的? 属性的本质就是实现实例变量和存取方法。 @property = ivar + getter + setter;对应基本数据类型默认关键字是atomic,readwrite,assign 对于普通的 Objective-C 对象atomic,readwrite,strong@property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var; @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。 @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。 NSString为什么要用copy关键字,如果用strong会有什么问题? 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性. 如何令自己所写的对象具有拷贝功能? 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。 可变集合类 和 不可变集合类的 copy 和 mutablecopy有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么? [immutableObject copy] // 浅复制 [immutableObject mutableCopy] //深复制 [mutableObject copy] //深复制 [mutableObject mutableCopy] //深复制但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制 为什么IBOutlet修饰的UIView也适用weak关键字? 因为既然有外链那么视图在xib或者storyboard中肯定存在,视图已经对它有一个强引用了。 不过这个回答漏了个重要知识,使用storyboard(xib不行)创建的vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryboard的私有数组强引用所有top level的对象,所以这时即便outlet声明成weak也没关系 nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现? atomic会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明 nonatomic 可以节省这些虽然很小但是不必要额外开销。 一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。 UICollectionView自定义layout如何实现?新建一个类继承UICollectionViewFlowLayout,实现prepareLayout方法。 新建UICollectionViewFlowLayout类,设置属性。 实现UICollectionViewDelegateFlowLayout方法。用StoryBoard开发界面有什么弊端?如何避免?难以维护 性能瓶颈 错误定位不准确解决多Storyboard协作弊端,就是尽量将项目的界面分割在多个Storyboard文件中。一个最佳实践是,按照项目功能模块来区分故事板,例如Login.Storyboard,Chat.Storyboard,Person.Storyboard等。尽量把每个Storyboard的Scene数量控制在20个以内 进程和线程的区别?同步异步的区别?并行和并发的区别? 一个进程可以包括多个线程。一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。 一个程序至少有一个进程,一个进程至少有一个线程. 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。 同步会造成阻塞,异步非阻塞,网络请求操作。 一个形象的例子: 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。 “并行”概念是“并发”概念的一个子集 线程间的通信 NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法 GCD的一些常用的函数?(group,barrier,信号量,线程同步)group 队列组通知监听函数(异步函数)dispatch_group_notify 队列组等待函数(同步函数)dispatch_group_wait 应用场景:下载两张图片,拼接图片后到主线程中刷新barrier 栅栏函数执行顺序:栅栏函数之前的任务(执行完毕)--> 栅栏函数的任务(执行完毕)--> 栅栏函数之后的任务 栅栏函数前面和后面追加的操作执行顺序都不固定,但是前面的三个输出操作必然先执行,然后再执行栅栏函数中的操作,最后执行后面的三个输出操作。 栅栏函数信号量 信号量大小是用于控制并发数量的 信号量就是一个资源计数器,对信号量有两个操作来达到互斥,分别是P和V操作。 一般情况是这样进行临界访问或互斥访问的: 设信号量值为1, 当一个进程1运行是,使用资源,进行P操作,即对信号量值减1,也就是资源数少了1个。这是信号量值为0。系统中规定当信号量值为0是,必须等待,知道信号量值不为零才能继续操作。 这时如果进程2想要运行,那么也必须进行P操作,但是此时信号量为0,所以无法减1,即不能P操作,也就阻塞。这样就到到了进程1排他访问。 当进程1运行结束后,释放资源,进行V操作。资源数重新加1,这是信号量的值变为1. 这时进程2发现资源数不为0,信号量能进行P操作了,立即执行P操作。信号量值又变为0.次数进程2咱有资源,排他访问资源。 这就是信号量来控制互斥的原理线程同步: 线程同步:@synchronized NSLock dispatch_semaphore(信号量设置为1)如何使用队列来避免资源抢夺? 当我们使用多线程来访问同一个数据的时候,就有可能造成数据的不准确性。这个时候我么可以使用线程锁的来来绑定。也是可以使用串行队列来完成。 如:fmdb就是使用FMDatabaseQueue,来解决多线程抢夺资源。 数据持久化的几个方案(fmdb用没用过)plist CoreData FMDB说一下AppDelegate的几个方法?从后台到前台调用了哪些方法?第一次启动调用了哪些方法?从前台到后台调用了哪些方法? ```//第一次启动: didFinishLaunchingWithOptions: applicationDidBecomeActive: //前台到后台: applicationWillResignActive: applicationDidEnterBackground: //后台到前台: applicationWillEnterForeground: applicationDidBecomeActive:```NSCache优于NSDictionary的几点?当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。 NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。 NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache。知不知道Designated Initializer?使用它的时候有什么需要注意的问题? 便利初始化函数只能调用自己类中的其他初始化方法 指定初始化函数才有资格调用父类的指定初始化函数 构造便利函数 实现description方法能取到什么效果? description方法默认返回对象的描述信息(默认实现是返回类名和对象的内存地址),可定义输出自己想要的内容。 objc使用什么机制管理对象内存? 1.Objective-C中所有对象都在堆区建立,由程序员负责释放对象所占用的内存。内存管理机制由3种:垃圾回收、引用计数、C语言方式。 2.垃圾回收是Mac OS10.5提供的新方案,在系统存在一个垃圾收集器。如果发现某个对象没有被任何对象使用,该对象被自动释放。 3.C语言方式,原始内存管理方式。用户手动调用malloc、calloc函数分配内存,free回收内存。 4.引用计数机制:对象创建后,运行时系统通过对象维护的一个计数器来描述有多少个其他对象在使用自己,当计数器为0时,释放该对象占用的内存空间(该对象调用dealloc方法)。 5,内存管理规则:当使用alloc,new或copy创建一个对象时,对象的引用计数被设置为1.;向对象发送retain消息,对象引用计数加1;向对象发送release消息时,对象引用计数减1;当对象引用计数为0时,运行时系统向对象发送dealloc消息并回收对象所占用的内存。 中级 Block block的实质是什么?一共有几种block?都是什么情况下生成的? Block是iOS开发中一种比较特殊的数据结构,它可以保存一段代码,在合适的地方再调用,具有语法简介、回调方便、编程思路清晰、执行效率高等优点,受到众多猿猿的喜爱。_NSConcreteGlobalBlock: 存储在全局数据区 _NSConcreteStackBlock: 存储在栈区 _NSConcreteMallocBlock: 存储在堆区 其中,_NSConcreteGlobalBlock 和 _NSConcreteStackBlock 可以由程序创建,而 _NSConcreteMallocBlock 则无法由程序创建,只能由 _NSConcreteStackBlock 通过拷贝生成。为什么在默认情况下无法修改被block捕获的变量? __block都做了什么? Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。 又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。 将变量由栈区移到堆区。 模拟一下循环引用的一个情况?block实现界面反向传值如何实现? 一个对象中强引用了block,在block中又强引用了该对象,就会发射循环引用。 解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用. id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏 id __block weakSelf = self; 或者将其中一方强制制空 xxx = nil。 Runtime objc在向一个对象发送消息时,发生了什么?通过对象的isa指针获取类的结构体。 在结构体的方法表里查找方法的selector。 如果没有找到selector,则通过objc_msgSend结构体中指向父类的指针找到父类,并在父类的方法表里查找方法的selector。 依次会一直找到NSObject。 一旦找到selector,就会获取到方法实现IMP。 传入相应的参数来执行方法的具体实现。 如果最终没有定位到selector,就会走消息转发流程。什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步? 找不到执行方法。动态方法解析 对象接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。 备用接收者 如果这个方法返回一个对象,则这个对象会作为消息的新接收者。注意这个对象不能是self自身,否则就是出现无限循环。如果没有指定对象来处理aSelector,则应该 return [super forwardingTargetForSelector:aSelector]。 完整消息转发 这是最后一次机会将消息转发给其它对象。创建一个表示消息的NSInvocation对象,把与消息的有关全部细节封装在anInvocation中,包括selector,目标(target)和参数。在forwardInvocation 方法中将消息转发给其它对象。能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么? 不能向编译后得到的类中增加实例变量; 能向运行时创建的类中添加实例变量; 解释下: 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量; 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair之前,原因同上。 runtime如何实现weak变量的自动置nil? runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。 给类添加一个属性后,在类结构体里哪些元素会发生变化?instance_size :实例的内存大小 objc_ivar_list *ivars:属性列表 RunLoop runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢? 循环检测线程任务。为了在我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。主线程的run loop默认是启动的。对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。 NSRunLoop *runloop = [NSRunLoop currentRunLoop];runloop的mode是用来做什么的?有几种mode? model 主要是用来指定事件在运行循环中的优先级的,分为: NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态 UITrackingRunLoopMode:ScrollView滑动时 UIInitializationRunLoopMode:启动时 NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合 苹果公开提供的 Mode 有两个: NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes) 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了? RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。 如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。 同时因为mode还是可定制的,所以: Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。 //将timer添加到NSDefaultRunLoopMode中 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; //然后再添加到NSRunLoopCommonModes里 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];苹果是如何实现Autorelease Pool的? autoreleasepool 以一个队列数组的形式实现,主要通过下列三个函数完成. objc_autoreleasepoolPush objc_autoreleasepoolPop objc_autorelease 看函数名就可以知道,对 autorelease 分别执行 push,和 pop 操作。销毁对象时执行release操作。 举例说明:我们都知道用类方法创建的对象都是 Autorelease 的,那么一旦 Person 出了作用域,当在 Person 的 dealloc 方法中打上断点,我们就可以看到这样的调用堆栈信息:备注:Objective-C Autorelease Pool 的实现原理 可能苹果在ARC的处理上又优化了,或者变更了。我自己测试的结果跟以上分析不一致,但原理可以参考。 类结构 isa指针?(对象的isa,类对象的isa,元类的isa都要说) 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中) 成员变量的列表, 属性列表, 它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass的指针,指向他的父类对象。 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。类方法和实例方法有什么区别?类方法: 类方法是属于类对象的 类方法只能通过类对象调用 类方法中的self是类对象 类方法可以调用其他的类方法 类方法中不能访问成员变量 类方法中不能直接调用对象方法实例方法: 实例方法是属于实例对象的 实例方法只能通过实例对象调用 实例方法中的self是实例对象 实例方法中可以访问成员变量 实例方法中直接调用实例方法 实例方法中也可以调用类方法(通过类名)介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法? 分类可以在不知道系统类源代码的情况下,为这个类添加新的方法。分类只能用来添加方法,不能添加成员变量。通过分类增加的方法,系统会认为是该类类型的一部分 Category实现原理 运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么? 可以添加属性,不可以添加成员变量。 OC类成员变量深度剖析 objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体) 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如: Person *motherInlaw = [[aPerson spouse] mother];如果 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。 如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。 如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。高级 UITableview的优化方法(缓存高度,异步绘制,减少层级,hide,避免离屏渲染) 一般在网络请求结束后,在更新界面之前就把每个 cell 的高度算好,缓存到相对应的 model 中。 另外绘制 cell 不建议使用 UIView,建议使用 CALayer。 简单的形式参考: ```//异步绘制 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ CGRect rect = CGRectMake(0, 0, 100, 100); UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0); CGContextRef context = UIGraphicsGetCurrentContext(); [[UIColor lightGrayColor] set]; CGContextFillRect(context, rect); //将绘制的内容以图片的形式返回,并调主线程显示 UIImage *temp = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // 回到主线程 dispatch_async(dispatch_get_main_queue(), ^{ //code }); });```CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

iOS蓝牙知识快速入门(详尽版)

以前写过几篇蓝牙相关的文章,但是没有涉及扫描、收发指令这些基础功能的实现。所以打算写一篇尽可能详尽的蓝牙知识汇总,一方面给有需要的同学看,一方面是对自己学习蓝牙的一个总结。 这篇文章的目的:教你实现设备的扫描,连接,数据收发,蓝牙数据解析。如果在实现上面任一功能遇到问题时,欢迎留下你的问题,我将进行补充,对于说法有误的地方也请老司机予以指正。目录0、思维导图 > 1、苹果对蓝牙设备有什么要求 > 2、操作蓝牙设备使用什么库 > 3、如何扫描 > 4、如何连接 > 5、如何发送数据和接收数据 > 6、如何解析数据 > 7、扩展思维导图第一次做图,大家凑合着看哈。这张是我总结的蓝牙知识的结构图,下面的内容将围绕这些东西展开进行。这张是蓝牙连接发送数据的流程图,下文进入coding阶段的讲解顺序,大家先有个大概印象,等阅读完本文再回来看这张图将理解的更深一些。 苹果对蓝牙设备有什么要求 BLE:bluetouch low energy,蓝牙4.0设备因为低功耗,所有也叫作BLE。苹果在iphone4s及之后的手机型号开始支持蓝牙4.0,这也是最常见的蓝牙设备。低于蓝牙4.0协议的设备需要进行MFI认证,关于MFI认证的申请工作可以看这里:关于MFI认证你所必须要知道的事情 在进行操作蓝牙设备前,我们先下载一个蓝牙工具LightBlue,它可以辅助我们的开发,在进行蓝牙开发之前建议先熟悉一下LightBlue这个工具。 操作蓝牙设备使用什么库 苹果自身有一个操作蓝牙的库CoreBluetooth.framework,这个是大多数人员进行蓝牙开发的首选框架,除此之外目前github还有一个比较流行的对原生框架进行封装的三方库BabyBluetooth,它的机制是将CoreBluetooth中众多的delegate写成了block方法,有兴趣的同学可以了解下。下面主要介绍的是原生蓝牙库的知识。 中心和外围设备如图所示,电脑、Pad、手机作为中心,心跳监听器作为外设,这种中心外设模式是最常见的。简单理解就是,发起连接的是中心设备(Central),被连接的是外围设备(Peripheral),对应传统的客户机-服务器体系结构。Central能够扫描侦听到,正在播放广告包的外设。 服务与特征 外设可以包含一个或多个服务(CBService),服务是用于实现装置的功能或特征数据相关联的行为集合。 而每个服务又对应多个特征(CBCharacteristic),特征提供外设服务进一步的细节,外设,服务,特征对应的数据结构如下所示如何扫描蓝牙 在进行扫描之前我们需要,首先新建一个类作为蓝牙类,例如FYBleManager,写成单例,作为处理蓝牙操作的管理类。引入头文件#import <CoreBluetooth/CoreBluetooth.h> CBCentralManager是蓝牙中心的管理类,控制着蓝牙的扫描,连接,蓝牙状态的改变。 1、初始化 dispatch_queue_t centralQueue = dispatch_queue_create(“centralQueue",DISPATCH_QUEUE_SERIAL);     NSDictionary *dic = @{CBCentralManagerOptionShowPowerAlertKey : YES, CBCentralManagerOptionRestoreIdentifierKey : @"unique identifier" }; self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:dic];CBCentralManagerOptionShowPowerAlertKey对应的BOOL值,当设为YES时,表示CentralManager初始化时,如果蓝牙没有打开,将弹出Alert提示框 CBCentralManagerOptionRestoreIdentifierKey对应的是一个唯一标识的字符串,用于蓝牙进程被杀掉恢复连接时用的。 2、扫描 //不重复扫描已发现设备        NSDictionary *option = @{CBCentralManagerScanOptionAllowDuplicatesKey : [NSNumber numberWithBool:NO],CBCentralManagerOptionShowPowerAlertKey:YES};        [self.centralManager scanForPeripheralsWithServices:nil options:option]; - (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;扫面方法,serviceUUIDs用于第一步的筛选,扫描此UUID的设备 options有两个常用参数:CBCentralManagerScanOptionAllowDuplicatesKey设置为NO表示不重复扫瞄已发现设备,为YES就是允许。CBCentralManagerOptionShowPowerAlertKey设置为YES就是在蓝牙未打开的时候显示弹框 3、CBCentralManagerDelegate代理方法 在初始化的时候我们调用了代理,在CoreBluetooth中有两个代理,CBCentralManagerDelegate CBPeripheralDelegateiOS的命名很友好,我们通过名字就能看出,上面那个是关于中心设备的代理方法,下面是关于外设的代理方法。我们这里先研究CBCentralManagerDelegate中的代理方法 - (void)centralManagerDidUpdateState:(CBCentralManager *)central;这个方法标了@required是必须添加的,我们在self.centralManager初始换之后会调用这个方法,回调蓝牙的状态。状态有以下几种: typedef NS_ENUM(NSInteger, CBCentralManagerState{ CBCentralManagerStateUnknown = CBManagerStateUnknown,//未知状态 CBCentralManagerStateResetting = CBManagerStateResetting,//重启状态 CBCentralManagerStateUnsupported = CBManagerStateUnsupported,//不支持 CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized,//未授权 CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff,//蓝牙未开启 CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn,//蓝牙启 } NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead”);该枚举在iOS10之后已经废除了,系统推荐使用CBManagerState,类型都是对应的 typedef NS_ENUM(NSInteger, CBManagerState{ CBManagerStateUnknown = 0, CBManagerStateResetting, CBManagerStateUnsupported, CBManagerStateUnauthorized, CBManagerStatePoweredOff, CBManagerStatePoweredOn, } NS_ENUM_AVAILABLE(NA, 10_0);- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;peripheral是外设类 advertisementData是广播的值,一般携带设备名,serviceUUIDs等信息 RSSI绝对值越大,表示信号越差,设备离的越远。如果想装换成百分比强度,(RSSI+100)/100,(这是一个约数,蓝牙信号值并不一定是-100 - 0的值,但近似可以如此表示) - (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict;在蓝牙于后台被杀掉时,重连之后会首先调用此方法,可以获取蓝牙恢复时的各种状态 如何连接 在扫面的代理方法中,我们连接外设名是MI的蓝牙设备 - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{    NSLog(@"advertisementData:%@,RSSI:%@",advertisementData,RSSI);       if([peripheral.name isEqualToString:@"MI"]){        [self.centralManager connectPeripheral:peripheral options:nil];//发起连接的命令          self.peripheral = peripheral;     } }连接的状态 对应另外的CBCentralManagerDelegate代理方法 连接成功的回调 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;连接失败的回调 - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;连接断开的回调 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;连接成功之后并没有结束,还记得CBPeripheral中的CBService和CBService中的CBCharacteristic吗,对数据的读写是由CBCharacteristic控制的。我们先用lightblue连接小米手环为例,来看一下,手环内部的数据是不是我们说的那样。其中ADVERTISEMENT DATA显示的就是广播信息。iOS蓝牙无法直接获取设备蓝牙MAC地址,可以将MAC地址放到这里广播出来FEEO是ServiceUUIDs,里面的FF01、FF02是CBCharacteristic的UUID Properties是特征的属性,可以看出FF01具有读的权限,FF02具有读写的权限。特征拥有的权限类别有如下几种: typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties{ CBCharacteristicPropertyBroadcast = 0x01, CBCharacteristicPropertyRead = 0x02, CBCharacteristicPropertyWriteWithoutResponse = 0x04, CBCharacteristicPropertyWrite = 0x08, CBCharacteristicPropertyNotify = 0x10, CBCharacteristicPropertyIndicate = 0x20, CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, CBCharacteristicPropertyExtendedProperties = 0x80, CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100, CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200};如何发送并接收数据 通过上面的步骤我们发现CBCentralManagerDelegate提供了蓝牙状态监测、扫描、连接的代理方法,但是CBPeripheralDelegate的代理方法却还没使用。别急,马上就要用到了,通过名称判断这个代理的作用,肯定是跟Peripheral有关,我们进入系统API,看它的代理方法都有什么,因为这里的代理方法较多,我就挑选几个常用的拿出来说明一下。 1、代理方法 //发现服务的回调 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error; //发现特征的回调 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error; //读数据的回调 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error; //是否写入成功的回调   - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;2、步骤 通过这几个方法我们构建一个流程:连接成功->获取指定的服务->获取指定的特征->订阅指定特征值->通过具有写权限的特征值写数据->在didUpdateValueForCharacteristic回调中读取蓝牙反馈值 解释一下订阅特征值:特征值具有Notify权限才可以进行订阅,订阅之后该特征值的value发生变化才会回调didUpdateValueForCharacteristic 3、实现上面流程的实例代码 //连接成功 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{     //连接成功之后寻找服务,传nil会寻找所有服务     [peripheral discoverServices:nil]; }//发现服务的回调 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{    if (!error) {        for (CBService *service in peripheral.services) {            NSLog(@"serviceUUID:%@", service.UUID.UUIDString);            if ([service.UUID.UUIDString isEqualToString:ST_SERVICE_UUID]) {            //发现特定服务的特征值                [service.peripheral discoverCharacteristics:nil forService:service];            }        }    } }//发现characteristics,由发现服务调用(上一步),获取读和写的characteristics - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {    for (CBCharacteristic *characteristic in service.characteristics) {        //有时读写的操作是由一个characteristic完成        if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]) {    self.read = characteristic;            [self.peripheral setNotifyValue:YES forCharacteristic:self.read];        } else if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_WRITE]) {        self.write = characteristic;        }    } }//是否写入成功的代理 - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{    if (error) {        NSLog(@"===写入错误:%@",error);    }else{        NSLog(@"===写入成功");    } }//数据接收 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {        if([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]){         //获取订阅特征回复的数据         NSData *value = characteristic.value;         NSLog(@"蓝牙回复:%@",value);     } }比如我们要获取蓝牙电量,由硬件文档查询得知该指令是**0x1B9901**,那么获取电量的方法就可以写成 - (void)getBattery{ Byte value[3]={0}; value[0]=x1B; value[1]=x99; value[2]=x01; NSData * data = [NSData dataWithBytes:&value length:sizeof(value)]; //发送数据 [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse]; }如果写入成功,我们将会在didUpdateValueForCharacteristic方法中获取蓝牙回复的信息。 如何解析蓝牙数据 如果你顺利完成了上一步的操作,并且看到了蓝牙返回的数据,那么恭喜你,蓝牙的常用操作你已经了解大半了。因为蓝牙的任务大部分就是围绕发送指令,获取指令,将蓝牙数据呈现给用户。上一步我们已经获取了蓝牙指令,但是获取的却是0x567b0629这样的数据,这是什么意思呢。这时我们参考硬件文档,看到这样一段:那么我们就可以得出设备电量是 60%。 对数据解析的流程就是:判断校验和是否正确,是不是一条正确的数据->该条数据是不是我们需要的电量数据,即首字节为0x567b->根据定义规则解析电量,传给view显示。其中第一步校验数据,视情况而定,也有不需要的情况。 扩展 iOS蓝牙中的进制转换 蓝牙固件升级 nRF芯片设备DFU升级

为博客设一个自定义域名

现在越来越多的人悬着使用githubpage搭建自己的个人博客,但是github提供的默认的域名是这种形式:username.github.io,是个二级域名,这对于很多人来说可能感觉不是很酷。那么我们今天就来做一件比较酷的事情,为站点自定义一个域名。目录0、准备工作 1、域名申请 2、为站点添加CNAME 3、在DNSPOD配置域名解析 4、修改DNS 5、验证结果准备工作一个完整的github page博客项目。 名词解释: DNS:网域名称系统。你可以把它想象成一张域名和IP地址映射的数据表。 DNS解析:就是通过我们输入的网址(域名)查找到对应的主机(IP地址) CNAME重定向:username.github.io和username.tk是两个域名,添加CNAME文件选择首选域,使其指向同一主机。 DNS原理>>申请域名 有很多网站都有域名购买服务,我使用的是Freenom。常见的域名注册网站还有万网、腾讯云、GoDaady(狗爹)。 我选择Freenom的最主要原因就是:免费+顶级域名。对,你没有听错就是免费的顶级域名,一开始就认为注册域名肯定要花费不少moneyd的人是不是感觉赚到了😉。进入该网站注册成功之后,选择Register a New Domain进行域名申请检验。它可以提供免费的顶级域名有:tk,ml,ga,cf,gq选择你喜欢的域名,进入选购界面在Period里面可以选择使用时间,最多是一年的免费使用,顶级域名耶,已经很大的优惠了。当然如果你是土豪这都无所谓了。 为站点添加CNAME文件 在Hexo的本地站点里,进入source文件,新建文件CNAME,注意没有后缀,打开文件填入刚申请的域名zhangferry.tk保存。然后发布站点,这时CNAME文件就被发布到了github上对应的站点仓库中CNAME文件的作用: CNAME是一个别名记录,它允许你将多个名字映射到同一台计算机。比如刚才添加的CNAME文件,会被github自动识别,当我们输入zhangferry.github.io和输入zhangferry.tk时,它将指向同一个ip地址,展示同样的内容。 在DNSPOD中配置域名解析 注册DNSPOS账号,进入管理控制台点击添加域名,输入我们刚刚申请的域名。确定之后就是进行添加记录添加记录的每一项,系统都会提示代表意思,这里主要解释记录类型A记录:地址记录,用来指定域名的IP地址 CNAME记录:如果需要将域名指向另一个域名,再由另一个域名提供IP地址,就需要添加CNAME记录 NS记录:域名服务器记录,如果需要把子域名交给其他DNS服务商解析,就需要添加NS记录上面的NS记录是系统默认添加的。 A记录就是指向对应IP地址,这里的192.30.252.153和192.30.252.154是github的服务器IP地址。 CNAME记录这里可填可不填,因为A记录已经将zhangferry.tk和zhangferry.github.io的域名统一为一个IP地址了。有一种情况就是为了提高访问速度,要区分国内国外不同用户使用不同的网站进行重定向需要添加对应的CNAME记录。 修改域名DNS 再回到刚才的域名申请网站,点Services->My Domains->Manage Domain->Management Tools->Nameservers将f1g1ns1.dnspod.net和f1g1ns2.dnspod.net填入到Nameserver1和Nameserver中,点击Change Nameservers保存操作。注意到刚填的域名服务就是对应NS记录的记录值。完成之后稍等片刻,DNSPod会有如下提示,否则就按照提示进行检验哪一步出了问题。验证结果 之后需要等待全球递归DNS服务器刷新(最多72小时) 在命令行执行:$ dig zhangferry.tk,出现以下结果说明配置成功,主要IP地址的对应。参考 Hexo博客系列:域名和DNS 为你的Github Pages博客绑定一个免费顶级域名吧

nRF芯片设备DFU升级(适配Xcode10.2.1)

这里主要参考这个项目:iOS-nRF-Toolbox,它是Nordic公司开发的测试工程,包含一整套nRF设备的测试解决方案。项目是用Swift写的,不过之前还是有OC版本的,但是后来由于一些**(不可描述的问题),才变成了现在的纯Swift版本。对于使用Swift开发的人员,直接仿照Demo操作即可。如果你是用Swift开发的,那下面的内容你可以不用看了。接下来我就讲一下针对OC引用DFU升级的操作步骤和我遇到的问题。 代码研究 nRF-Toolbox项目包含BGM,HRM,HTM,DFU等多个模块,我们今天只关注其中的DFU升级模块。打开项目,在对应的NORDFUViewController.swift中我们能够看到有三个引用库 import UIKit,import CoreBluetooth,import iOSDFULibrary,这里的iOSDFULibrary就是DFU升级的库,也是解决DFU升级最重要的组件。我们只要把这个库集成到我们的项目中,就能够完成nRF设备的DFU升级了。 集成步骤 有两种方案集成:通过cocoapods集成 编译出framework然后把库导入项目第一种方案是作者推荐的,但是我试了很久,引入DFULibrary会出现头文件找不到等一系列问题,无奈只能放弃,如果有人通过这种方式成功,还望告知。下面讲的是通过第二种方案的集成。 第一步:导出iOSDFULibrary 这一步是最关键也是最容易出问题的,这个库也是由Swift写成的,我们将这个库clone到本地,然后选择iOSDFULibrary进行编译最后生成两个framework:iOSDFULibrary.framework Zip.framework这时库内的代码已经变成了我们熟悉的OC语言。理论上这个库应该是没问题的了,但是事实还是有问题的,见issues#39。作者给出的解决方法是:1、On your mac please install carthage (instructions) 2、Create a file named cartfile anywhere on your computer 3、add the following content to the file:github "NordicSemiconductor/IOS-Pods-DFU-Library" ~> 2.1.2 github "marmelroy/Zip" ~> 0.61、Open a new terminal and cd to the directory where the file is 2、Enter the command carthage update --platform iOS 3、Carthage will now take care of building your frameworks, the produced .framework files will be found in a newly created directory called Carthage/Build/iOS,copy over iOSDFULibrary.framework and Zip.framework to your project and you are good to go.carthage是一种和cocoapods相似的的类库管理工具,如果不会使用的话可以参照Demo,将framework文件导入到自己的项目。 第二步、导入framework Target->General直接拖入项目默认只会导入到Linked Frameworks and Libraries,我们还需要在Embeded Binaries中引入。 第三步、使用iOSDFULibrary //create a DFUFirmware object using a NSURL to a Distribution Packer(ZIP) DFUFirmware *selectedFirmware = [[DFUFirmware alloc] initWithUrlToZipFile:url];// or //Use the DFUServiceInitializer to initialize the DFU process. DFUServiceInitiator *initiator = [[DFUServiceInitiator alloc] initWithCentralManager: centralManager target:selectedPeripheral]; [initiator withFirmware:selectedFirmware]; // Optional: // initiator.forceDfu = YES/NO; // default NO // initiator.packetReceiptNotificationParameter = N; // default is 12 initiator.logger = self; // - to get log info initiator.delegate = self; // - to be informed about current state and errors initiator.progressDelegate = self; // - to show progress bar // initiator.peripheralSelector = ... // the default selector is usedDFUServiceController *controller = [initiator start];库中有三个代理方法DFUProgressDelegate,DFUServiceDelegate,LoggerDelegate,它们的作用分别为监视DFU升级进度,DFU升级及蓝牙连接状态,打印状态日志。 常见问题 1、selectedFirmware返回nil DFUFirmware *selectedFirmware = [[DFUFirmware alloc] initWithUrlToZipFile:url];需要在General的Embeded Binaries选项卡里导入那Zip.framework和iOSDFULibrary.framework 2、崩溃报错 dyld: Library not loaded: @rpath/libswiftCore.dylibReferenced from: /private/var/containers/Bundle/Application/02516D79-BB30-4278-81B8-3F86BF2AE2A7/XingtelBLE.app/Frameworks/iOSDFULibrary.framework/iOSDFULibraryReason: image not found需要改两个地方如果不起作用,将Runpath Search Paths的选项内容删掉再重新添加一遍即可 3、打包上架时报ERROR IT MS-90087等问题 问题描述: ERROR ITMS-90087: "Unsupported Architectures. The executable for ***.app/Frameworks/SDK.framework contains unsupported architectures '[x86_64, i386]'." ERROR ITMS-90362: "Invalid Info.plist value. The value for the key 'MinimumOSVersion' in bundle ***.app/Frameworks/SDK.framework is invalid. The minimum value is 8.0" ERROR ITMS-90209: "Invalid Segment Alignment. The app binary at '***.app/Frameworks/SDK.framework/SDK' does not have proper segment alignment. Try rebuilding the app with the latest Xcode version." ERROR ITMS-90125: "The binary is invalid. The encryption info in the LC_ENCRYPTION_INFO load command is either missing or invalid, or the binary is already encrypted. This binary does not seem to have been built with Apple's linker."解决方法,添加Run Script PhaseShell脚本内容填写如下内容,再次编译即可 APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"# This script loops through the frameworks embedded in the application and # removes unused architectures. find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK do FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable) FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME" echo "Executable is $FRAMEWORK_EXECUTABLE_PATH"EXTRACTED_ARCHS=()for ARCH in $ARCHS do echo "Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME" lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH" EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH") doneecho "Merging extracted architectures: ${ARCHS}" lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}" rm "${EXTRACTED_ARCHS[@]}"echo "Replacing original executable with thinned version" rm "$FRAMEWORK_EXECUTABLE_PATH" mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"done完整OC项目 这个是对应Swift版本用OC写的完整项目,应该是OC停止维护之前的版本。会有一些bug。在将DFUFramework更新之后,我把它搬到了我的github上,有需要的同学可以下载研究:OC-nRFTool-box。以下为更新内容,时间:2017.12.26 收到很多关于无法适配Xcode9.2的反馈,因为最近比较忙没时间处理,不好意思啦,今天抽出时间来把代码更新了一下。 Xcode9.2 出现的问题1、dyld: Library not loaded: @rpath/libswiftCore.dylib Referenced from: /private/var/containers/Bundle/Application/02516D79-BB30-4278-81B8-3F86BF2AE2A7/XingtelBLE.app/Frameworks/iOSDFULibrary.framework/iOSDFULibrary Reason: image not found 2、DFUFirmware *selectedFirmware = [[DFUFirmware alloc] initWithUrlToZipFile:url]; 返回为空或者崩溃问题我的测试结果是更新iOSDFULibrary. framework和Zip.framework可以解决以上问题。 解决方案 Carthage 因为这两个库都是通过Swift维护的,所以更新framework最好还是要用适用Swift的方式,包括以后的更新也一样。所以我推荐用Carthage更新这俩库,下面是使用Carthage的简单介绍,详细的可以看这里Carthage的安装和使用。 另外OC-nRFTool-box也已经更新,里面的Framework可以直接拿来用。 1、安装brew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"2、brew更新 $ brew update3、安装Carthage $ brew install carthage4、使用Carthage $ cd ~/路径/项目文件夹 /**进入项目文件夹下*/ $ touch Cartfile /**创建carthage文件*/ $ open Cartfile /**打开carthage文件*/ # /**输入以下内容*/ $ github "NordicSemiconductor/IOS-Pods-DFU-Library" ~> 4.1 $ github "marmelroy/Zip" ~> 1.15、运行Carthage $ carthage update --platform iOS /**编译出iOS版本*/6、更新framework $ cd Carthage/Build/iOS /**framework输出位置,将老的framework替换掉*/注意 nRF Toolbox项目方法变更 [initiator withFirmwareFile:selectedFirmware];/** 旧版本方法 */ initiator = [initiator withFirmware:selectedFirmware];/** 新版本方法 */更新:2019-7-14 针对之前常出的这种问题: dyld: Library not loaded: @rpath/libswiftCore.dylib Referenced from: /private/var/containers/Bundle/Application/CDB2F4ED-C49C-4303-BE1F-5D9D990380F3/nRF Toolbox.app/Frameworks/Zip.framework/Zip Reason: image not found均是由Swift库版本不一致引起的,iOSDFULibrary目前已经支持到Swift 5,所以我们应该升级一下版本。为了方便使用,我将Carthage集成到了项目里,如果以后需要再升级,更新Cartfile文件里的版本号,执行更新命令: $ carthage update --platform iOS如果你想要将DFU的framework集成到你自己的项目里,可以在Carthage/Build/iOS/中找到iOSDFULibrary.framework, ZIPFoundation.framework 将其拖到项目中即可。 上线注意事项(由@jianxiong1997提供)需要删除ZIPFoundation.framework中的libswiftRemoteMirror.dylib Frameworksgithub项目同步更新。

基于Hexo搭建自己的博客小屋

作为一名技术人员没有属于自己的博客,就像是喜欢LOL的玩家却没有一款炫酷的皮肤一样,这不叫真爱。虽然现在是微博的时代,讲究方便阅读,易传播,但是对于博客来说,特别是技术博客,专业性永远都是第一位的。我们需要用大大的篇幅去阐述自己对技术的理解并将其分享给其他人,所以无论社交软件如何发展,我们都需要博客。下面就跟着我一块搭建属于自己的博客小屋吧。搭建环境 已经安装Git的Mac电脑,这个默认都能满足,所以就不详细介绍了。 创建github page 首先注册github账号,然后在repository选项卡里New一个新的仓库来存储我们的网站然后命名为username.github.io。安装Hexo 在安装Hexo之前我们需要安装nvm和Node.js。Hexo是目前很流行的博客管理框架,基于Node.js nvm是Node.js的版本管理工具 而Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。不太理解和想要深入了解各软件作用的同学可以自行google,接下来我们开始安装这些东西(确实挺多的)。 1、通过curl方式安装node版本管理工具nvm curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.30.2/install.sh | bash其他方式的安装可以自行google。 2、配置环境变量 完成之后nvm就被安装在了~/.nvm下,接下来配置环境变量 在~/目录下看是否有.zshrc,.bash_profile,或者.profile,如果没有就新建一个.profile文件。 注意:.开头的文件是隐藏文件,在终端查看的时候使用命令ls -a,然后打开对应的配置文件在最后一行加上: export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm这一步的目的是每次新打开一个bash,nvm都会被自动添加到环境变量中。 3、验证nvm安装 在命令行输入nvm看到如下信息: Node Version ManagerNote: <version> refers to any version-like string nvm understands. This includes: - full or partial version numbers, starting with an optional "v" (0.10, v0.1.2, v1) - default (built-in) aliases: node, stable, unstable, iojs, system - custom aliases you define with `nvm alias foo`Usage: nvm help Show this message nvm --version Print out the latest released version of nvm nvm install [-s] <version> Download and install a <version>, [-s] from source. Uses .nvmrc if available --reinstall-packages-from=<version> When installing, reinstall packages installed in <node|iojs|node version number> nvm uninstall <version> Uninstall a version nvm use [--silent] <version> Modify PATH to use <version>. Uses .nvmrc if available nvm exec [--silent] <version> [<command>] Run <command> on <version>. Uses .nvmrc if available nvm run [--silent] <version> [<args>] Run `node` on <version> with <args> as arguments. Uses .nvmrc if available nvm current Display currently activated version nvm ls List installed versions nvm ls <version> List versions matching a given description (usually `~/.nvm`)那么恭喜你!nvm安装成功了。这一步在我看来是最容易出错的。 4、安装node.js 如果上面的步骤完成了,node.js的安装就简单多了,直接: nvm install node这个指令是安装最新版node 安装成功后可以使用nvm ls查看当前node版本号 5、安装Hexo 安装Hexo也比较简单 sudo npm install hexo-cli -g配置Hexo站点 完成所需组建的安装,接下来就要建立本地站点,配置站点了。 1、本地新建博客目录 目录可以自由选择,我选择在主目录下: ~$ mkdir username.github.io ~$ cd username.github.io ~$ hexo init username.github.io2、配置站点 在站点下有一个_config.yml,这里我们可以进行一些对博客的配置 language: en #语言设置 theme: next #主题设置,因为下面将使用next主题 deploy: type: git repo: https://github.com/username/username.github.io.git这里的repo就是我们新建仓库的git地址,之后发布的时候就会将内容发布到这个地址下。更多设置可以查看更多Hexo配置 3、配置主题我使用的是目前最受欢迎的一款Hexo主题Next使用它的话,我们需要先把它clone到本地 $ cd username.github.io $ git clone https://github.com/iissnan/hexo-theme-next themes/next在theme文件夹内也有一个_config.yml文件,这里是用来配置主题的,详细设置 新建、发布博客 经过上面的努力终于可以开心的写博客了,Hexo博客是基于Markdown格式编译的,所以,我们需要了解常用的Markdown语法,不了解Markdown的可以点这里参考Markdown,以下命令均在博客站点目录操作 1、新建 hexo new "my blog"文件生成在username/source/_posts/my-blog.md,打开文件,利用markdown语法将内容写到里面。 2、编译 hexo generate //可以简写为hexo g这一步的作用是将刚才的markdown语法的博客内容编译成html语言。编译之后生成public文件夹,里面放的是生成的html文件。之后同步到github上的就是这个文件夹的内容。 3、** 开启本地服务** hexo server //可以简写hexo s这个命令的作用是开启本地服务。之后会有下面两条语句生成 INFO Start processing INFO Hexo is running at http://localhost:4000/. Press Ctrl+C to stop.我们就可以访问 http://localhos:4000/预览博客内容了。 4、部署 hexo deploy //可以简写为 hexo d部署的作用就是将博客内容发布到网络。执行完成之后我们就可以访问http://username.github.io了,当你能够看到自己写的内容呈现在自己眼前的时候有没有很激动呢。哈哈 5、清楚public内容 hexo clean 这个命令用在当我们更改source内部的资源路径之后,执行此命令可以重新编译生成public文件夹。好了,讲解到此结束,下一篇讲解如何发布博客到指定域名。这个是我的博客http://zhangferry.tk,欢迎访问

iOS蓝牙中的进制转换

最近在忙一个蓝牙项目,在处理蓝牙数据的时候,经常遇到进制之间的转换,蓝牙处理的是16进制(NSData),而我们习惯的计数方式是10进制,为了节省空间,蓝牙也会把16进制(NSData)拆成2进制记录。这里我们研究下如何在他们之间进行转换。 假设我们要向蓝牙发送0x1B9901这条数据 Byte转NSData Byte value[3]={0}; value[0]=0x1B; value[1]=0x99; value[2]=0x01; NSData * data = [NSData dataWithBytes:&value length:sizeof(value)]; //发送数据 [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];优点:这种方法比较简单,没有进行转换,直接一个字节一个字节的拼装好发送出去。缺点:当发送数据比较长时会很麻烦,而且不易更改。NSString转NSData - (NSData *)hexToBytes:(NSString *)str {     NSMutableData* data = [NSMutableData data];     int idx;     for (idx = 0; idx+2 <= str.length; idx+=2) {         NSRange range = NSMakeRange(idx, 2);         NSString* hexStr = [str substringWithRange:range];         NSScanner* scanner = [NSScanner scannerWithString:hexStr];         unsigned int intValue;         [scanner scanHexInt:&intValue];         [data appendBytes:&intValue length:1];     }     return data; } //发送数据 [self.peripheral writeValue:[self hexToBytes:@"1B9901"] forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];优点:比较直观,可以一次转换一长条数据,对于一些功能简单的蓝牙程序,这种转换能处理大部分情况。 缺点:只能发送一些固定的指令,不能参与计算。求校验和 接下来探讨下发送的数据需要计算的情况。 最常用的发送数据需要计算的场景是求校验和(CHECKSUM)。这个根据硬件厂商来定,常见的求校验和的规则有:如果发送数据长度为n字节,则CHECKSUM为前n-1字节之和的低字节 CHECKSUM=0x100-CHECKSUM(上一步的校验和)如果我要发送带上校验和的0x1B9901,方法就是: - (NSData *)getCheckSum:(NSString *)byteStr{     int length = (int)byteStr.length/2;     NSData *data = [self hexToBytes:byteStr];     Byte *bytes = (unsigned char *)[data bytes];     Byte sum = 0;     for (int i = 0; i<length; i++) {         sum += bytes[i];     }     int sumT = sum;     int at = 256 -  sumT;    printf("校验和:%d\n",at);     if (at == 256) {         at = 0;     }     NSString *str = [NSString stringWithFormat:@"%@%@",byteStr,[self ToHex:at]];     return [self hexToBytes:str]; }//将十进制转化为十六进制 - (NSString *)ToHex:(int)tmpid {     NSString *nLetterValue;     NSString *str =@"";     int ttmpig;     for (int i = 0; i<9; i++) {         ttmpig=tmpid%16;         tmpid=tmpid/16;         switch (ttmpig)         {             case 10:                 nLetterValue =@"A";break;             case 11:                 nLetterValue =@"B";break;             case 12:                 nLetterValue =@"C";break;             case 13:                 nLetterValue =@"D";break;             case 14:                 nLetterValue =@"E";break;             case 15:                 nLetterValue =@"F";break;             default:                 nLetterValue = [NSString stringWithFormat:@"%u",ttmpig];        }         str = [nLetterValue stringByAppendingString:str];         if (tmpid == 0) {             break;         }     } //不够一个字节凑0     if(str.length == 1){         return [NSString stringWithFormat:@"0%@",str];     }else{         return str;     } } //发送数据 NSData *data = [self getCheckSum:@"1B9901"];//data=<1b99014b> [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];拆分数据 这种是比较麻烦的,举个栗子:在传输某条信息时,我想把时间放进去,不能用时间戳,还要节省空间,这样就出现了一种新的方式存储时间。 这里再补充一些C语言知识:一个字节8位(bit) char 1字节 int 4字节 unsigned 2字节 float 4字节存储时间的条件是:只用四个字节(32位) 前5位表示年(从2000年算起),接着4位表示月,接着5位表示日,接着5位表示时,接着6位表示分,接着3位表示星期,剩余4位保留。这样直观的解决办法就是分别取出现在时间的年月日时分星期,先转成2进制,再转成16进制发出去。当然你这么写进去,读的时候就要把16进制数据先转成2进制再转成10进制显示。我们就按这个简单粗暴的思路来,准备工作如下: 10进制转2进制 //  十进制转二进制 - (NSString *)toBinarySystemWithDecimalSystem:(int)num length:(int)length {     int remainder = 0;      //余数     int divisor = 0;        //除数    NSString * prepare = @"";    while (true)     {         remainder = num%2;         divisor = num/2;         num = divisor;         prepare = [prepare stringByAppendingFormat:@"%d",remainder];        if (divisor == 0)         {             break;         }     }     //倒序输出     NSString * result = @"";     for (int i = length -1; i >= 0; i --)     {         if (i <= prepare.length - 1) {             result = [result stringByAppendingFormat:@"%@",                       [prepare substringWithRange:NSMakeRange(i , 1)]];        }else{             result = [result stringByAppendingString:@"0"];        }     }     return result; }2进制转10进制 //  二进制转十进制 - (NSString *)toDecimalWithBinary:(NSString *)binary {     int ll = 0 ;     int  temp = 0 ;     for (int i = 0; i < binary.length; i ++)     {         temp = [[binary substringWithRange:NSMakeRange(i, 1)] intValue];         temp = temp * powf(2, binary.length - i - 1);         ll += temp;     }    NSString * result = [NSString stringWithFormat:@"%d",ll];    return result; } ### 16进制和2进制互转 - (NSString *)getBinaryByhex:(NSString *)hex binary:(NSString *)binary {     NSMutableDictionary  *hexDic = [[NSMutableDictionary alloc] init];     hexDic = [[NSMutableDictionary alloc] initWithCapacity:16];     [hexDic setObject:@"0000" forKey:@"0"];     [hexDic setObject:@"0001" forKey:@"1"];     [hexDic setObject:@"0010" forKey:@"2"];     [hexDic setObject:@"0011" forKey:@"3"];     [hexDic setObject:@"0100" forKey:@"4"];     [hexDic setObject:@"0101" forKey:@"5"];     [hexDic setObject:@"0110" forKey:@"6"];     [hexDic setObject:@"0111" forKey:@"7"];     [hexDic setObject:@"1000" forKey:@"8"];     [hexDic setObject:@"1001" forKey:@"9"];     [hexDic setObject:@"1010" forKey:@"a"];     [hexDic setObject:@"1011" forKey:@"b"];     [hexDic setObject:@"1100" forKey:@"c"];     [hexDic setObject:@"1101" forKey:@"d"];     [hexDic setObject:@"1110" forKey:@"e"];     [hexDic setObject:@"1111" forKey:@"f"];    NSMutableString *binaryString=[[NSMutableString alloc] init];     if (hex.length) {         for (int i=0; i<[hex length]; i++) {             NSRange rage;             rage.length = 1;             rage.location = i;             NSString *key = [hex substringWithRange:rage];             [binaryString appendString:hexDic[key]];         }    }else{         for (int i=0; i<binary.length; i+=4) {             NSString *subStr = [binary substringWithRange:NSMakeRange(i, 4)];             int index = 0;             for (NSString *str in hexDic.allValues) {                 index ++;                 if ([subStr isEqualToString:str]) {                     [binaryString appendString:hexDic.allKeys[index-1]];                     break;                 }             }         }     }     return binaryString; }有了这几种转换函数,完成上面的功能就容易多了,具体怎么操作这里就不写一一出来了。但总感觉怪怪的,这么一个小功能怎么要写这么一大堆代码,当然还可以用C语言的方法去解决。这里主要是为了展示iOS中数据如何转换,C语言的实现方法这里就不写了,有兴趣的同学可以研究下。 附带两个函数 int转NSData - (NSData *) setId:(int)Id { //用4个字节接收     Byte bytes[4];     bytes[0] = (Byte)(Id>>24);     bytes[1] = (Byte)(Id>>16);     bytes[2] = (Byte)(Id>>8);     bytes[3] = (Byte)(Id);     NSData *data = [NSData dataWithBytes:bytes length:4]; }NSData转int 接受到的数据0x00000a0122 //4字节表示的int NSData *intData = [data subdataWithRange:NSMakeRange(2, 4)];     int value = CFSwapInt32BigToHost(*(int*)([intData bytes]));//655650 //2字节表示的int NSData *intData = [data subdataWithRange:NSMakeRange(4, 2)];     int value = CFSwapInt16BigToHost(*(int*)([intData bytes]));//290 //1字节表示的int char *bs = (unsigned char *)[[data subdataWithRange:NSMakeRange(5, 1) ] bytes];     int value = *bs;//34 ------------------------ //补充内容,因为没有三个字节转int的方法,这里补充一个通用方法 - (unsigned)parseIntFromData:(NSData *)data{    NSString *dataDescription = [data description];     NSString *dataAsString = [dataDescription substringWithRange:NSMakeRange(1, [dataDescription length]-2)];    unsigned intData = 0;     NSScanner *scanner = [NSScanner scannerWithString:dataAsString];     [scanner scanHexInt:&intData];     return intData; }这两个转换在某些场景下使用频率也是挺高的,蓝牙里面的数据转换基本也就这么多了,希望能够帮助大家。 更多关于字节编码的问题,大家可以点这里:传送门 扩展 基于CoreBluetooth4.0框架的连接BLE4.0的Demo:你不点一下吗

iOS获取来电和短信发送状态

获取电话状态 在我想要了解iOS获取来电状态时,经常被这是不是允许的,是不是要调用私有库等问题困扰。费了好大劲终于解决了上面问题,你可以获取系统提供的电话相关状态,而且它不属于私有库。为了需要这方面资料的人查阅时少走弯路,我把这些东西写下来,废话少说,上代码。如何获取电话状态 首先要导入CoreTelephony框架: @import CoreTelephony; 然后声明一个CTCallCenter变量: @interface ViewController () { CTCallCenter *center_; //为了避免形成retain cycle而声明的一个变量,指向接收通话中心对象 } @end 然后监听电话状态: - (void) aboutCall{ //获取电话接入信息 callCenter.callEventHandler = ^(CTCall *call){ if ([call.callState isEqualToString:CTCallStateDisconnected]){ NSLog(@"Call has been disconnected");}else if ([call.callState isEqualToString:CTCallStateConnected]){ NSLog(@"Call has just been connected");}else if([call.callState isEqualToString:CTCallStateIncoming]){ NSLog(@"Call is incoming");}else if ([call.callState isEqualToString:CTCallStateDialing]){ NSLog(@"call is dialing");}else{ NSLog(@"Nothing is done"); } }; }还可以获取运营商信息: - (void)getCarrierInfo{ // 获取运营商信息 CTTelephonyNetworkInfo *info = [[CTTelephonyNetworkInfo alloc] init]; CTCarrier *carrier = info.subscriberCellularProvider; NSLog(@"carrier:%@", [carrier description]);// 如果运营商变化将更新运营商输出 info.subscriberCellularProviderDidUpdateNotifier = ^(CTCarrier *carrier) { NSLog(@"carrier:%@", [carrier description]); };// 输出手机的数据业务信息 NSLog(@"Radio Access Technology:%@", info.currentRadioAccessTechnology); } 当然这样在真机进行测试,以下为输出信息: 2015-12-29 16:34:14.525 RWBLEManagerDemo[1489:543655] carrier:CTCarrier (0x134e065c0) { Carrier name: [中国移动] Mobile Country Code: [460] Mobile Network Code:[07] ISO Country Code:[cn] Allows VOIP? [YES] } 2015-12-29 16:34:14.526 RWBLEManagerDemo[1489:543655] Radio Access Technology:CTRadioAccessTechnologyHSDPA CoreTelephony框架是不是私有库 私有框架的目录为: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/PrivateFrameworks/可以看出CoreTelephony框架是在frameworks内而不是PrivateFrameworks,所以它是可以放心使用的。网上之所以有说CoreTelephony是私有库,是因为在iOS6的时候是私有框架,后来苹果又给公开了。 获取短信状态 关于短信的状态获取,我直接看了 #import <MessageUI/MessageUI.h> 里面就两个头文件: #import <MessageUI/MFMailComposeViewController.h> #import <MessageUI/MFMessageComposeViewController.h> 一个是邮件相关的方法,一个短信相关的方法。进到MFMessageComposeViewController.h有一个枚举值: enum MessageComposeResult { MessageComposeResultCancelled, MessageComposeResultSent, MessageComposeResultFailed };typedef enum MessageComposeResult MessageComposeResult; // available in iPhone 4.0 这是表示短信发送状态的值。要使用这个框架发送自己编辑的内容还需要添加代理:MFMessageComposeViewControllerDelegate 代码如下: - (void)showMessageView { if( [MFMessageComposeViewController canSendText] )// 判断设备能不能发送短信 { MFMessageComposeViewController*picker = [[MFMessageComposeViewControlleralloc] init]; // 设置委托 picker.messageComposeDelegate= self; // 默认信息内容 picker.body = @"nihao"; // 默认收件人(可多个) picker.recipients = [NSArray arrayWithObject:@"12345678901", nil]; [self presentModalViewController:picker animated:YES]; [picker release]; } else { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"提示信息" message:@"该设备不支持短信功能" delegate:self cancelButtonTitle:nil otherButtonTitles:@"确定", nil]; [alert show]; [alert release]; } }- (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { switch (result){ case MessageComposeResultCancelled: NSLog(@"取消发送"); break; case MessageComposeResultFailed: NSLog(@"发送失败"); break; case MessageComposeResultSent: NSLog(@"发送成功"); break; default: break; } }对于来短信的通知没有找到,应该是不能获取的。 参考资料private framework使用 http://chenjohney.blog.51cto.com/4132124/1288551 CoreTelephony框架的简单使用 http://blog.csdn.net/jymn_chen/article/details/19240903 iOS关于系统短信和电话的调用 http://blog.csdn.net/frank_jb/article/details/49815883

处理ANCS设备连接绑定问题

ANCS(Apple Notification Center Service,苹果通知中心)的目的是提供给蓝牙外设一种简单、方便的获取iOS设备通知信息的方式。使得蓝牙手环,手表可以接收到来自iPhone的来电、短信及QQ、微信等应用的通知消息。 如果你已经能够连接普通蓝牙,初次面对ANCS设备可能会有以下问题:问题一:遵循ANCS协议的的设备会直接和系统相连,即使杀掉应用,连接还是存在的。而如果蓝牙设备处于连接状态,它不会被扫描到,怎么再次连接呢?在Core Bluetooth framework里提供了两个方法,用于获取已连接的设备 //通过传入的peripherals.identifier返回与系统连接的已知设备数组 - (NSArray<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers; //通过传入设备的serviceID返回已连接的设备数组 - (NSArray<CBPeripheral *> *)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs;我们就可以通过这两个方法,获取已连接设备,并建立重连。参考代码: NSArray *peripherals = [central retrieveConnectedPeripheralsWithServices:@[serviceUUID]]; if (peripherals.count > 0) { CBPeripheral *peripheral = [peripherals firstObject]; peripheral.delegate = self; self.peripheral = peripheral;//**关键**需要转存外设值,才能发起连接 [central connectPeripheral:self.peripheral options:nil]; } else { [central scanForPeripheralsWithServices:@[serviceUUID] options:nil]; }根据不同的使用情况,可能会有不同的扫描,连接的逻辑,苹果提供了一个流程图:问题二:有绑定和解除功能,如何处理两者的关系首先我们要知道,不能通过代码,断开ANCS设备与系统之间的连接,那么如果我们想解除设备的绑定,只能控制设备与APP之间的断开。 ###绑定 再回看上面提到的苹果提供的两个获取已连接设备的方法,一个是通过serviceUUID,它可以返回同一类型的设备列表;一个是通过设备UUID,它在一定情况下就是唯一的(如果设备名唯一,这里可以使用设备名),返回的是唯一设备。那么我们就可以利用UUID的唯一性,作为绑定的标示,存到NSUserDefault里面,对于未绑定的设备通过serviceUUID去获取设备列表。参考代码: NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults]; NSString *uuidString = [userDefault objectForKey:RWBLE_BANDIDENTIFI_ID];NSArray *peripherals; if (uuidString) {         //通过uuid获取连接设备     NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];         peripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[uuid]]; }else{         //通过serviceUUID获取连接设备         peripherals = [self.centralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:ST_SERVICE_UUID]]]; } /* peripherals connect code */###解绑 不能使ANCS设备与系统连接断开,那么我们就在程序里销毁这个外设对象,这样APP与蓝牙设备的连接通讯就不存在了,造成了一种断开的感觉。参考代码: //解绑设备 - (void)unbindDevice{     [self disconnect];//通知app,设备已经断开    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];     [userDefault removeObjectForKey:RWBLE_BANDIDENTIFI_ID];//销毁uuid     self.peripheral = nil; }这么写看似已经解决问题了,但是会出现一种情况:解绑了设备,杀掉应用,再次进入设备还是能连上。why?因为虽然没有了UUID,但进入程序会通过serviceUUID再次获取连接。 这时可以在扫面做一个判断,是否刚解绑过设备。可以是个BOOL值,绑定和初始绑定为NO,解绑操作改为YES。如果刚解绑过设备,就直接返回不做后面的扫描操作,这样就解决了上面的问题。这个比较简单,就不列具体代码了。 参考文档: Best Practices for Interacting with a Remote Peripheral Device

iOS10本地通知UserNotifications快速入门

iOS10更新变动最大的就是通知这部分了,新版通知变得更加统一,使用更加方便,设计更加自由。以前本地通知和远程推送是分开的,虽然这些到了iOS10都合在一起了,但是为了便于理解,我们还是把他俩分开来进行学习。这节我们学习的是本地通知。以下的用语,如无特别表述,通知就代表本地通知,推送就代表远程服务器的推送。 ##快速添加一个通知 我们先举个完整的代码例子,大家了解下这个流程,然后分步介绍这几项: //第一步:注册通知 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch.     UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; //请求获取通知权限(角标,声音,弹框)     [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {         if (granted) { //获取用户是否同意开启通知             NSLog(@"request authorization successed!");         }     }]; }     //第二步:新建通知内容对象     UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]     content.title = @"iOS10通知";     content.subtitle = @"新通知学习笔记";     content.body = @"新通知变化很大,之前本地通知和远程推送是两个类,现在合成一个了。这是一条测试通知,";     content.badge = @1;     UNNotificationSound *sound = [UNNotificationSound soundNamed:@"caodi.m4a"];     content.sound = sound;    //第三步:通知触发机制。(重复提醒,时间间隔要大于60s)     UNTimeIntervalNotificationTrigger *trigger1 = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];    //第四步:创建UNNotificationRequest通知请求对象     NSString *requertIdentifier = @"RequestIdentifier";     UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requertIdentifier content:content trigger:trigger1];    //第五步:将通知加到通知中心     [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {         NSLog(@"Error:%@",error);    }]; }最终效果如下:通知内容UNMutableNotificationContent通知内容就是设定通知的一些展示信息,iOS10之后可以设置subtitle。 声音的设置需要借助一个新类UNNotificationSound ,通知文件要放到bundle里面。另外在实际的测试过程中发现,添加通知的声音有时候会无效。这应该是iOS10存在的一个bug,删除掉程序,再安装运行就好了。触发机制UNNotificationTriggerTrigger是新加入的一个功能,通过此类可设置本地通知触发条件。它一共有一下几种类型: 1、UNPushNotificaitonTrigger 推送服务的Trigger,由系统创建 2、UNTimeIntervalNotificaitonTrigger 时间触发器,可以设置多长时间以后触发,是否重复。如果设置重复,重复时长要大于60s 3、UNCalendarNotificaitonTrigger 日期触发器,可以设置某一日期触发。例如,提醒我每天早上七点起床: NSDateComponents *components = [[NSDateComponents alloc] init]; components.hour = 7; components.minute = 0; // components 日期 UNCalendarNotificationTrigger *calendarTrigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];4、UNLocationNotificaitonTrigger 位置触发器,用于到某一范围之后,触发通知。通过CLRegion设定具体范围。通知请求UNNotificationRequest通知请求的构造 + (instancetype)requestWithIdentifier:(NSString *)identifier content:(UNNotificationContent *)content trigger:(nullable UNNotificationTrigger *)trigger; 就是把上面三项连接起来。它有一个参数identifier,这相当于通知的一个身份。iOS10通知支持更新,就是基于此identifier再发一条通知。通知中心UNUserNotificationCenter获取通知[UNUserNotificationCenter currentNotificationCenter] 然后通过addNotificaitonRequest:就完成了一个通知的添加。 ##扩展通知的内容 通知我们已经添加上了,现在我们需要扩展一下通知的内容,给它加一些内容。扩展的内容需要支持3D-touch的手机(6s以上),重压之后全面显示添加附件iOS10之前通知的样式不能更改,在iOS10之后引入了UNNotificationationAttachment,可以在通知中添加图片,音频,视频。苹果对这些附件的大小和类型有一个限制:如果我想在通知里加一个图片,可以这样处理: NSString *imageFile = [[NSBundle mainBundle] pathForResource:@"sport" ofType:@"png"]; UNNotificationAttachment *imageAttachment = [UNNotificationAttachment attachmentWithIdentifier:@"iamgeAttachment" URL:[NSURL fileURLWithPath:imageFile] options:nil error:nil]; content.attachments = @[imageAttachment];//虽然是数组,但是添加多个只能显示第一个 /* add request and notificaiton code ... */效果如下:重压之后:添加交互//点击可以显示文本输入框 UNTextInputNotificationAction *action1 = [UNTextInputNotificationAction actionWithIdentifier:@"replyAction" title:@"文字回复" options:UNNotificationActionOptionNone];     //点击进入应用 UNNotificationAction *action2 = [UNNotificationAction actionWithIdentifier:@"enterAction" title:@"进入应用" options:UNNotificationActionOptionForeground];     //点击取消,没有任何操作 UNNotificationAction *action3 = [UNNotificationAction actionWithIdentifier:@"cancelAction" title:@"取消" options:UNNotificationActionOptionDestructive];     //通过UNNotificationCategory对象将这几个action行为添加到通知里去 UNNotificationCategory *categroy = [UNNotificationCategory categoryWithIdentifier:@"Categroy" actions:@[action1,action2,action3] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; //将categroy赋值到通知内容上 content.categoryIdentifier = @"Categroy"; //设置通知代理,用于检测点击方法 [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self]; /* add request and notificaiton code ... */效果如下:获取通知交互内容: //识别通知交互处理的代理方法 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{    NSString *categoryIdentifier = response.notification.request.content.categoryIdentifier;    if ([categoryIdentifier isEqualToString:@"Categroy"]) {         //识别需要被处理的拓展         if ([response.actionIdentifier isEqualToString:@"replyAction"]){             //识别用户点击的是哪个 action             UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse*)response;             //获取输入内容             NSString *userText = textResponse.userText;             //发送 userText 给需要接收的方法             NSLog(@"要发送的内容是:%@",userText);             //[ClassName handleUserText: userText];         }else if([response.actionIdentifier isEqualToString:@"enterAction"]){             NSLog(@"点击了进入应用按钮");         }else{             NSLog(@"点击了取消");         }     }     completionHandler(); }由此我们可以知道action,categroy,request这些东西都是通过各自的identifier获取的。这样可以很方便的定位到某一个通知或者action上,为交互的处理提供了很大的便利。##自定义通知样式 在Xcode中File->New->Targe会出现下面的视图Notification Content对应的是通知,Notification Service Extension对应的是推送。我们这里要实现通知的自定义,选择左边那个。创建成功之后会在工程里多一个文件件NotificationViewController文件是自动生成的,里面有一个 - (void)didReceiveNotification:(UNNotification *)notification 可以在这里定义一些通知的显示。 MainInterface.storyboard文件是控制通知的storyboard文件,可以编辑需要的通知样式。我们设计一下文字的颜色和显示位置接下来你可能会问,怎么把这个自定义的通知样式应用到当前通知里呢?先别急,我们看下一个文件Info.flist里面的内容第一项UNNotificationExtensionCategory就是UNNotificationCategory的标示,我们把他换成我们通知里使用的标示"Category",系统就会自动匹配通知显示的样式。 第二项UNNotificationExtensionIntialContentSizeRation初始内容 Size 的比例。也可以在 viewDidLoad 中使用 self.preferredContentSize 直接设置 Size。 第三项UNNotificationExtensionDefaultContentHidden是否隐藏默认内容,如果设为YES,默认内容会被隐藏。 显示的效果:##总结 至此,iOS通知部分的内容就学完了,参考代码:Demo。 参考文档: iOS10 User Notificaitons学习笔记 活久见的重构-iOS10 UserNotificaiotns框架解析