RunLoop探究


RunLoop,即运行循环,你非要叫它跑圈,也不是不可以

这哥们到底是什么?

我们知道,一个程序的入口函数,也就是在main.m文件中

程序启动首先就会走到这里,这里有个自动释放池,一般在创建大量临时变量的时候会手动创建释放池,这里将整个程序运行期间都置于这个释放池内,程序运行结束之时,自动释放池结束,内存回收。这个自动释放池内,就一句代码,这是什么意思?进入UIApplicationMain函数内可以发现,这个函数返回值是int型。那我们换种写法

运行起来,会发现第十六行并不会执行,代码停留在了第十五行,不会再继续往下执行,也就是说UIApplicationMain函数一直没有返回。我们的程序一直是在主线程上执行的,我们打印下看看

NSLog(@"thread=%@",[NSThread currentThread]);

2018-03-30 22:53:12.821131+0800 RunLoopDemo[74265:4182972] thread=<NSThread: 0x604000066180>{number = 1, name = main}

的确,当前就是在主线程,这是必然的。整个项目运行期间,主线程都不会退出,一旦退出,程序肯定就挂了。究其原因,是因为UIApplicationMain函数内开启了一个死循环,用来保住主线程的生命,程序处于运行状态,使得整个项目运行期间,主线程都不会退出。这个死循环,其实就是RunLoop,RunLopp就是个死循环。这个默认启动的RunLoop是跟主线程相关联的。RunLoop这么设计,它的作用是什么呢?

  • 保证线程不退出,即程序的持续运行
  • 监听事件,询问是否有事件发生,从事件队列里取出事件并处理(网络、触摸、时钟等事件)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

那就写个代码玩一下试试看

- (void)viewDidLoad {
    [super viewDidLoad];

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0     target:self selector:@selector(timerMethod) userInfo:nil     repeats:YES];
}

- (void)timerMethod {
    static int a = 0;
    a++;
    NSLog(@"a=%zd",a);
}

一个很简单的时钟事件,这个时候你会发现方法并没有走。上面已经提到,RunLoop用来监听事件,需要将timer加到RunLoop中去

[[NSRunLoop currentRunLoop] addTimer:timer     forMode:NSDefaultRunLoopMode];

注意RunLoop无法手动创建,只能获取,不过其实第一次获取就是创建。这里面有个mode属性,暂时写默认,这时候运行程序,正常运行。这时候我们往控制器上拖个textView, 运行

问题出来了,启动后时钟事件正常运行,但是一旦拖动textView,时钟事件就停止打印,不拖动textView,时钟事件又正常运行。这是什么情况?

上面我们将timer丢给RunLoop的时候,使用的模式modeNSDefaultRunLoopMode默认模式。

iOS中有两套API来访问和使用RunLoop对象,分别是OC语言中的Foundation框架(NSRunLoop)和C语言中的Core Foundation框架(CFRunLoopRef),NSRunLoop是基于CFRunLoopRef的一层OC包装

RunLoop文档

CFRunLoopRef文档

RunLoop其实共五种模式

  • NSDefaultRunLoopMode
  • UITrackingRunLoopMode
  • NSRunLoopCommonModes
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode

NSDefaultRunLoopMode模式就是RunLoop的默认模式,而UITrackingRunLoopMode模式则是UI模式,UI模式只能被触摸事件唤醒,所以优先级最高。这里第三种模式NSRunLoopCommonModes严格来说不是一种模式,而是一种占位模式,既能起到默认模式的效果又能起到UI模式的效果。第四种和第五种模式分别是在刚启动进入App时进入的第一个Mode,启动后就不再使用,和接受系统时间的内部Mode,作为开发者这两种模式基本不会使用到,

Core Foundation中关于RunLoop有五个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopRef对应的就是NSRunLoop,CFRunLoopModeRef代表上面说到的RunLoop的几种运行模式,CFRunLoopSourceRef是事件源,或者说是输入源,现在事件源分为两种,非基于PortSource0和基于PortSource1,基于Port是指 通过内核和其他线程通信,接收、分发系统事件,CFRunLoopTimerRef是基于事件的触发器,相当于NSTimer,CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变,可以监听的时间点有以下几个

  • kCFrunLoopEntry (1UL << 0) 即将进入Loop
  • kCFRunLoopBeforeTimers (1UL << 1) 即将处理Timer
  • kCFRunLoopBeforeSources (1UL << 2) 即将处理Source
  • kCFRunLoopBeforeWaiting (1UL << 5) 即将进入休眠
  • kCFRunLoopAfterWaiting (1UL << 6) 刚从休眠中唤醒
  • kCFRunLoopExit (1UL << 7) 即将退出Loop
  • kCFRunLoopAllActivities 所有状态

上面我们是把时钟加到默认模式下,现在我们加在UI模式看下效果

[[NSRunLoop currentRunLoop] addTimer:timer     forMode:UITrackingRunLoopMode];

再次运行程序

问题又出来了,当我们拖到textView时,时钟事件可以正常打印,但是一旦停止就不再打印了。这是因为,时钟是加到RunLoopUI模式下的,而UI模式只有在触摸屏幕的时候才会唤醒,而没有触摸屏幕的时候RunLoop则处于休眠状态,所以才会出现此问题。怎么才能解决这个问题呢?将这个时钟既加到默认模式下,又加到UI模式下

 [[NSRunLoop currentRunLoop] addTimer:timer     forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer     forMode:UITrackingRunLoopMode];

这要添加两次,而🍎肯定考虑到了这点,所以有了NSRunLoopCommonModes这种占位模式

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

再次运行,会发现上述问题都得到了解决,拖拽不再影响时钟方法的执行了。简单画个图

这是🍎官网的图

上面提到,RunLoop的作用之一就是监听事件并执行,事件可以有很多种,比如时钟事件、监听事件、网络事件等等。UI模式下的这些事件必须通过触摸才能唤醒RunLoop去执行,执行完之后RunLoop就处于休眠状态,循环往复。而默认模式下,只要有这些事件,就会及时通知RunLoop去执行。

继续深究,如果这个事件是个耗时操作会怎样?我们在timerMethod这个时间下模拟一下

[NSThread sleepForTimeInterval:1.0];

我让它每次睡个2秒钟再执行

问题又出来了,你会发现UI被卡住了,因为当前是在主线程上执行,每次都强迫它睡2秒,这绝壁会卡住。既然这样,那就让这个耗时操作放在子线程去执行

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
          NSLog(@"111");
    }];
    [thread start];
}

- (void)timerMethod {
    NSLog(@"222");
    [NSThread sleepForTimeInterval:2.0];
    static int a = 0;
    a++;
}

这时候运行会发现111尽管打印了,但是222没有打印,这说明timerMethod方法并没有执行,这是什么原因?这时候我们自定义一个FN_Thread

@implementation FN_Thread
- (void)dealloc {
    NSLog(@"dealloc");
}

NSThread替换成FN_Thread

 FN_Thread *thread = [[FN_Thread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        NSLog(@"111");
    }];
[thread start];

再次运行,会发现打印完111,接着走进了FN_Thread中的dealloc方法,这说明这个线程对象thread被释放了,既然被释放了,那这个线程上的timeMethod方法自然就不会执行了。那我们强引用这个线程对象试一下,加上对应代码

@property (nonatomic, strong) FN_Thread *thread;

_thread = thread;

再次运行,会发现FN_Thread类中的dealloc方法不走了,但是timeMethod方法依然没有执行。线程对象没有释放,当前线程上的时钟方法又没有走,这又是什么原因?再加一个方法看一下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"thread=%@",_thread);
    [_thread start];
}

运行,触摸屏幕的时候再次开启这个线程,开启前打印一下

这个线程对象还在,但是开启会崩

这是因为,这个线程已经挂了,已经被释放了,肯定无法被开启,但是这个OC对象thread还在,因为被我们强引用了。强引用线程对象,并不会阻止线程的释放。所以会出现上述情况。事实上,子线程block内的代码执行完线程就会被释放,要想线程不被释放。就必须让线程有执行不完的任务。那我们可以手动让线程一直在执行,在block内加如下代码

while (true) {     }

这时候会发现程序可以正常运行了。但这并不是正确的解决方法。正确的姿势应该是将事件加到RunLoop中,然后开启它,因为


每个线程都对应一个RunLoop,但是除了主线程,所有子线程的RunLoop默认都没有开启,需要手动开启

NSLog(@"111");代码前加上

[[NSRunLoop currentRunLoop] run];

再次运行,会发现时钟方法timeMethod终于可以正常运行了,但是你会发现NSLog(@"111");这句代码又没有走了?这是因为NSRunLoop是个死循环,下面的代码根本不会执行。好在,RunLoop还提供给了一个指定线程运行时间的方法[[NSRunLoop currentRunLoop] runUntilDate:];,毕竟一开启就停不下来,这操作很难接得住

这里就让线程存活指定时间感受一下

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];

再次运行,一切都很明了了

这里如果想强制退出子线程,使用[NSThead exit],不过并不建议这么做

有必要强调一下,如果在主线程调用[NSThead exit]会怎样?

你会发现程序并不会挂掉,但是主线程卡死了,子线程仍然在继续。其实在CPU眼里,主线程和其他子线程一样,一样可以强退

iOS里有个peformSelector方法,我们看一下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    
    FN_Thread *thread = [[FN_Thread alloc] initWithBlock:^{
        NSLog(@"touch");
    }];
    [thread start];
    [self performSelector:@selector(performMethod) onThread:thread withObject:nil     waitUntilDone:NO];
}

- (void)performMethod {
    NSLog(@"performMethod");
}

这时候你会发现performMethod方法根本就没走,performSelector就是线程间的通信,performMethod是在子线程thread上执行的,thread线程的block代码走完线程就挂了,所以不会走进performMethod方法,需要加上下面这句代码保住子线程的生命

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];

这里的performSelector事件就是上面提到的RunLoop处理的Source事件源。如果是performSelectorOnMainThread方法就不需要开启RunLoop,因为主线程的RunLoop在程序启动之时就已经默认开启了

NSTimer作为定时器有时候并不是十分精准的,可能会受到RunLoop的运行模式的影响,再加上其本身的精度有限, 可以使用GCD的定时器作为代替,精度更高,且不会受到RunLoop的影响,因为其是基于C语言的

int count = 0;
- (void)GCDTimer {
    // 获得队列
    dispatch_queue_t queue = dispatch_get_main_queue(); //     dispatch_get_global_queue(0, 0);
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    self.timer = timer;
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(timer, start, interval, 0);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"count=%zd",count);
        count++;
    });
    // 启动定时器
    dispatch_resume(timer);
    // 取消定时器
    //dispatch_cancel(self.timer);
}

上面提到了RunLoop中的TimerSource,还有一个Observer,监听者,是用来监听RunLoop所处的状态,OC中无法创建,只能使用C语言

- (void)addOberser {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"activity = %zd", activity);
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,     kCFRunLoopDefaultMode);
    CFRelease(observer);
}

可以看到RunLoop的运行逻辑,每次都是从睡眠中唤醒,处理一些事件,最后处于休眠状态。监听状态变化的block中可以根据自己需求定义一些处理事件

在项目中经常会遇到加载图片的需求,现在一般都使用SDWebImage库,如果不使用这个库怎么处理?

主要逻辑就是将一个个图片下载任务放入队列,下载成功存入内存缓存,然后写入沙盒,下次再次显示时先从内存缓存里获取,获取不到则从沙盒获取,依然获取不到则进行下载任务。其实使用SDWebImage库也依然会进行这些操作。

如果,这些下载的图片非常的大,分辨率比较高,你会发现这时候滑动的时候会有明显卡顿,是因为即便下载是在子线程,但是在渲染的时候图片会解压缩,这是在主线程进行的,是个耗时的操作,图片分辨率高的情况下卡顿尤其明显。这些下载下来的图片是在一次RunLoop循环的时候渲染显示,如果一次RunLooop循环只处理一张图片,卡顿就会得到有效解决,这需要用到Observer来监听RunLoop的状态,RunLoop在处理完事件会进入休眠状态,那就每次监听在RunLoop进入休眠状态前处理这些图片,直到没有图片处理然后进入休眠状态

RunLoopDemo

LoadManyPictures

LoadBigImage


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

GCD探究


要了解GCD(Grand Central Dispatch),先从最基本的一些概念作为切入点

1.概念理解

进程

只指在系统中正在运行的一个应用程序,比如同时打开了Xcode和Sourcetree, 系统会分别启动两个进程,Mac上有个活动监视器,可以查看已打开的进程并可以杀死进程。每个进程之间是独立的,均运行在其专用且受保护的内存空间内


线程

进程想要执行任务,必须得有线程,每个进程至少要有一条线程,一个进程所有的任务都在线程间执行


线程的串行

一个线程中任务的执行是串行的,如果要在一个线程中执行多个任务,那么只能一个一个的按顺序执行这些任务,也就是说,在同一时间内,一个线程只能执行一个任务


多线程

一个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务,多线程可以提升程序的工作效率。其实,在同一时间,CPU只能处理一条线程,只有一条线程在工作,多线程并发,只是CPU在快速的在多条线程之间调度。如果同时执行的线程过多,反而会拖慢程序的执行效率,因为切换线程也同样需要消耗资源,每条线程被调度的频次也会降低,所以多线程也并非是越多越好


多线程的优缺点

可以适当提高程序的执行效率和资源利用率。但是创建线程也是有开销的,以iOS为例,成本主要包括:内核数据结构(大约1kb),栈空间(子线程512KB,主线程1MB,也可以通过setStackSize设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的时间。如果开启大量线程,会降低程序性能,线程越多,CPU在地调度线程上的开销就越大,也有可能会导致程序设计更加复杂,比如线程之间通信、数据共享等


主线程

iOS程序运行后,默认会开启一条线程,称为主线程或UI线程,用来显示刷新UI界面,处理一些点击、滚动或拖拽等UI事件,但是尽量不要将比较耗时的操作放到主线程,否则会卡主主线程,影响UI流畅度

2.多线程实现

技术方案 简介 语言 线程声明周期 使用频率
pthread 1.一套通用的多线程API
2.适用于Unix\Linux\Windows等系统
3.跨平台\可移植
C 程序员管理 几乎不用
NSThread 1.使用更加面向对象
2.简单易用,可直接操作县城对象
OC 程序员负责创建 偶尔使用
GCD 1.旨在替代NSThread等等多核技术
2.充分利用设备的多核
C 自动管理 经常使用
NSOperation 1.基于GCD
2.比GCD多了一些更简单的实用功能
3.使用更加面向对象
OC 自动管理 经常使用
  • pthread

在查看CPU的使用上也是可以直观看到是有多条线程在同时执行

  • NSThread

一个NSThread对象就代表一条线程,线程一旦死亡不能再开启任务

线程状态

多线程的安全隐患

多个线程访问同一块资源可能发生安全隐患

这个时候就需要加锁,即互斥锁

锁定一段代码只能使用一把锁,使用多把锁匙无效的,互斥锁能有效防止因多线程抢夺同一块资源而造成的数据安全问题,但是却需要消耗大量的CPU资源,注意,在主线程加锁是没有意义的

OC在定义属性的时候有nonatomicatomic,即非原子属性和原子属性,默认是原子属性。原子属性,为setter方法加锁,非原子属性则不会加锁 。但是虽然atomic线程安全,却需要消耗大量的资源。nonatomatic非线程安全,适合内存小的移动设备。所以我们iOS开发,建议所有属性都声明为nonatomatic,在需要线程安全的地方再手动加锁

在一个进程中中,线程往往不是孤立存在的,多个线程之间经常需要通信,经典的图片下载显示就需要线程间的通信,线程间通信通常可以使用performSelector,还可以使用NSPortNSMessagePortNSMachPort等端口,不过使用不多

  • GCD

Grand Central Dispatch,很⑥的中枢调度器,纯C语言,提供了很多非常强大的函数

GCD的优势

  • GCD是🍎公司为多核的并行运算提出的解决方案

  • GCD会自动利用更多的CPU内核(双核、四核)

  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

  • 攻城狮只需要告诉GCD要执行什么任务,不需要编写任何线程管理代码

GCD的两个核心概念

  • 任务: 执行什么操作

  • 队列: 用来存放任务

GCD的使用

  • 定制任务

    • 确定想做的事情
  • 将任务添加到队列中

    • GCD会自动将队列中的任务取出,放到对应的线程中执行

    • 任务的取出遵循队列的FIFO原则:先进先出,后进后出

GCD中有两个用来执行任务的常用函数

  • 同步 diapatch_sync(dispatch_queue_t queue, dispatch_block_t block)

    • queue: 队列

    • block: 任务

  • 异步 dispatch_async(dispatch_queue_t queue,dispatch_block_t block)

注意,同步只能在当前线程中执行任务,不具备开启新线程的能力
异步可以在新的线程中执行任务,具备开启新线程的能力

GCD的队列类型

  • 并发队列(Concurrent Dispatch Queue

    • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)

    • 并发功能只有在异步(dispatch_async)函数下才有效

  • 串行队列

    • 让任务一个接着一个的执行(一个任务执行完毕后,再执行下一个任务)

上面提到的几个术语容易混淆:同步、异步、并发、串行

  • 同步和异步主要影响:能不能开启新的线程
    同步: 只是在当前线程中执行任务,不具备开启新线程的能力

    异步: 可以在新的线程中执行任务,具备开启新线程的能力

  • 并发和串行的主要影响: 任务的执行方式

    • 并发:多个任务并发(同时)执行

    • 串行:一个任务执行完毕后,再执行下一个任务

GCD的各种队列

并发队列 手动创建的串行队列 主队列
同步(sync) 没有开启新线程
串行执行任务
同左 同左
异步 (async) 有开启新线程
并发执行任务
有开启新线程
串行执行任务
没有开启新线程
串行执行任务

注意,使用sync函数往当前串行队列中添加任务,会卡住当前串行队列

线程间通信

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEF    AULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
        });
//        dispatch_sync(dispatch_get_main_queue(), ^{
//        });
        NSLog(@"");
    });

GCD中常用函数

dispatch_barrier_async(dispatch_queue_t queue,dispatch_block_t block)

在前面任务结束后他才执行,而且它后面的任务等它执行完成后才会执行, 这个queue不能是全局的并发地列,需手动创建

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ })

延迟执行, 不一定非要放到主线程

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});

一次性代码,该函数能保证某段代码在程序运行过程中只被执行一次

// .h
#define FNSingletonH + (instancetype)sharedInstance;

// .m
#define FNSingletonM \
static id _instace; \
 \
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
        _instace = [super allocWithZone:zone]; \
    }); \
    return _instace; \
} \
 \
+ (instancetype)sharedInstance \
{ \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
        _instace = [[self alloc] init]; \
    }); \
    return _instace; \
} \
 \
- (id)copyWithZone:(NSZone *)zone \
{ \
    return _instace; \
}

常用于单例模式,可以保证在程序运行过程中,一个类只有一个实例,而且该实例易于供外界访问,从而方便控制了实例个数又节约系统资源

dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t      _Nonnull queue#>, <#^(size_t)block#>)

快速迭代遍历

 dispatch_group_t group = dispatch_group_create(); 
 dispatch_queue_t queue =     dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_async(group, queue, ^{
   });
   dispatch_group_async(group, queue, ^{
 });
dispatch_group_notify(group, queue, ^{
   });

队列组,等二个异步操作都执行完毕后,再回到指定线程操作



未完待续…


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

复杂UITableView的相关优化


仅做记录使用,会比较粗糙,不过多探讨!

iOS开发中,UITableView存在于几乎每一个应用中。不是那么复杂的UITableView使用常规写法没有任何问题,但是对于复杂业务一个UITableView中存在几十个不同的UITableViewCell,再使用常规写法简直就是灾难,有些写法性能上并无问题,但是优雅度不够,维护性较差。以下从几点阐述,为了方便,多是图片

UITableVIewCell的创建

  1. 每个cell对应一个viewModel

2.每个section对应一个viewModel

UITableViewCell上的UITextField

  1. 提前创建cell,为当前cell上的textField指定好下一个响应者

2.遍历获取所有可响应键盘的textField

    #import "UIView+NextKeyBoard.h"

@implementation UIView (NextKeyBoard)

/** 获取当前视图所有可以响应键盘的textField或textView */
- (NSArray *)lvdeepResponderViews {
    NSMutableArray<UIView*> *textFields = [[NSMutableArray alloc] init];
    CGFloat height = self.frame.size.height;
    if (height > 1 && self.hidden == NO) {
        for (UIView *textField in self.subviews) {
            if ((textField) && [textField lvcanBecomeFirstResponder] && [textField isUserInteractionEnabled] && ![textField isHidden] && [textField alpha] != 0.0) {
                [textFields addObject:textField];
            }

            if (textField.subviews.count && [textField isUserInteractionEnabled] && ![textField isHidden] && [textField alpha]!= 0.0) {
                [textFields addObjectsFromArray:[textField lvdeepResponderViews]];
            }
        }
    }

    // 按照位置y排序 如果y相等 则按照x的大小排序
    return [textFields sortedArrayUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) {

        CGRect frame1 = [view1 convertRect:view1.bounds toView:self];
        CGRect frame2 = [view2 convertRect:view2.bounds toView:self];

        CGFloat x1 = CGRectGetMinX(frame1);
        CGFloat y1 = CGRectGetMinY(frame1);
        CGFloat x2 = CGRectGetMinX(frame2);
        CGFloat y2 = CGRectGetMinY(frame2);

        if (y1 < y2) {
            return NSOrderedAscending;
        } else if (y1 > y2) {
            return NSOrderedDescending;
        } else if (x1 < x2) {
            return NSOrderedAscending;
        } else if (x1 > x2) {
            return NSOrderedDescending;
        } else {
            return NSOrderedSame;
        }
    }];
    return textFields;
}

/** 判断当前的输入控件是否可以有输入响应的事件 */
- (BOOL)lvcanBecomeFirstResponder {
    BOOL canBecomeFirstResponder = NO;
    if ([self isKindOfClass:[UITextField class]]) {
        canBecomeFirstResponder = [(UITextField*)self isEnabled];
    } else if ([self isKindOfClass:[UITextView class]]) {
        canBecomeFirstResponder = [(UITextView*)self isEditable];
    }

    if (canBecomeFirstResponder) {
        canBecomeFirstResponder = ([self isUserInteractionEnabled] && ![self isHidden] && [self alpha] != 0.0 && ![self isAlertViewTextField]  && ![self isSearchBarTextField]);
    }

    return canBecomeFirstResponder;
}

- (BOOL)isAlertViewTextField {
    UIResponder *alertViewController = [self lvviewController];
    BOOL isAlertViewTextField = NO;
    while (alertViewController && isAlertViewTextField == NO) {
        if ([alertViewController isKindOfClass:[UIAlertController class]]) {
            isAlertViewTextField = YES;
            break;
        }

        alertViewController = [alertViewController nextResponder];
    }
    return isAlertViewTextField;
}

- (BOOL)isSearchBarTextField {
    UIResponder *searchBar = [self nextResponder];
    BOOL isSearchBarTextField = NO;
    while (searchBar && isSearchBarTextField == NO) {
        if ([searchBar isKindOfClass:[UISearchBar class]]) {
            isSearchBarTextField = YES;
            break;
        } else if ([searchBar isKindOfClass:[UIViewController class]]) {
            break;
        }

        searchBar = [searchBar nextResponder];
    }
    return isSearchBarTextField;
}

- (UIViewController *)lvviewController {
    UIResponder *nextResponder =  self;
    do {
        nextResponder = [nextResponder nextResponder];
        if ([nextResponder isKindOfClass:[UIViewController class]]) {
            return (UIViewController*)nextResponder;
        }
    } while (nextResponder != nil);
    return nil;
}

@end

UITableviewCell高度计算

  1. 提前创建好的cell直接获取cell的高度 (适用于相同样式cell数量相对较少的情况,对应上面的每个section对应一个viewModel情况)

  2. 通过协议或者上图展示的viewModel,提前计算好cell的高度并缓存

UITableviewCell布局

  1. 每个section对应一个viewModel情况

2.每个cell对应一个model对象

UITableviewCell上事件响应

1.delegate,block,RacSubject

2.使用响应者链ResponderChain

内存问题

以上


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

2017


有人问我,2017年你过得还好吗

什么叫好,什么叫不好?这要看怎么比了,如果跟过去的16年15年比,我敢说不好吗

不好意思,我敢!确实不好

早上闹钟准时响起,忘了关了,反手一个动作,熟练干脆,再醒时已快中午了

打开微信,打开QQ(原谅我还在使用QQ,在下是个念旧的人), 都在狂晒十八岁的照片,晒之前可能还要思考哪张照片看起来没那么傻,哪张看起来更青春,晒个照片徒增不少烦恼

我就好了,毕竟还得两年才能到十八岁,嘴角不经意上扬了一下,不知道有没有四十五度

挺好的,虽然一个个都像是在给葬爱家族开年会,稚嫩青涩但又不失朝气,像是刚出炉的蛋糕,即使不小心被人用指头摁下去,也会很快恢复到之前光滑无痕的模样

这盛世美颜,难以置信

时至年末,又到了坐下来清算flag的时候了

尽管,我们总是身体力行,坚定且执着的反复验证着同一个真理:当初立下的flag,只是用来方便日后推倒的

想起曾经斗志满满、正气凛然的在朋友圈立下的那些flag,有没有觉得现在脸上有些疼

没错,就是这个感觉。立flag的时候总是有种舍我其谁的兴奋感油然而生,立的好的话可能感觉还有点秒不而言,与我而言,通常帅不过三秒

其实在朋友圈打嘴炮立flag这事儿,不必太在意,毕竟生活里总是需要大大小小的仪式感和目标来约束放纵的自己

年轻时我立下了无数flag,就如同我说过想戒熬夜一样多,正是这些flag的存在才让我成为更好现在的自己

“这次考试怎么也要拿个第一名,再不济也要前三”

“大学非清北不上,再不济也要985、211”

“我要连拿四年奖学金,这不是开玩笑”

“今年要再看十本纸质书,读书使我快乐”

“我要开始炫‘腹’,练出八块腹肌,让运动成为一种习惯,健身成为一种日常”

“我要让余太不仅垂涎与我的才华,更要拜倒在我的厨艺之下”

“世界这么大,我要去看看,我要带余太去浪漫的土耳其,去看更广阔的世界”

“这个双十一我就看看什么都不买”

“教师资格证、银行从业资格证、证券资格证,一证在手,天下我有”

“每天专业技能学习,谁也不能阻止我学习的脚步”

“每天早点睡,早起两小时”

“今年先挣它一个亿,目标不在小,在于积累”

….

这哪是立flag啊,有些就是在白日做梦

鲁迅说过:“一千个人有一千个flag,倒或者不倒,它都在那里,见证你的酸甜苦辣”

不管曾经立下的flag,是依然屹立在风中,还是已经飘然倒下,但不可否认的是一直在进步,只是离预期目标还有点远而已

既然有些东西必须要经历,又何必畏手畏脚?都是有故事的人,谁能输给谁

想我17年立的几个flag,简单、易实现、没难度。预计存款一万,结果负债九万, 还差十万就达标了,差距不是很大

讲道理,不必在意那些打脸的flag,毕竟真正的勇士,敢于直面打脸的flag,履立屡破,屡破屡立

新的一年,我的flag很简单

过得比17年好就行

如果没做到,明年我还得这么立

看起来都一片歌舞升平,腾讯新闻很合时机的推送了三分钟的2017大事件

既有”中国首艘国产航母下水”、”国产大飞机C919成功首飞”等振奋国人的成就,又有”杭州保姆纵火案”、”江哥案”等让人扼腕叹息的悲剧

每年的这个时候,我觉得有篇文章都值得我们反复诵读,永远致敬

原文刊登于1999年的《南方周末》,题为《总有一种力量让你泪流满面》,别说我没告诉你

登过顶峰方知初心可贵,有过低谷更知人间冷暖

没有谁能留住时间,唯有努力才不会让光阴虚度

再见2017

您好2018


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

ReactiveCocoa探究-编程思想


谈到ReactiveCocoa(RAC),说它比较火似乎也没有那么火,一些大厂用的也并不多,说它不火貌似又经常看到或听到一些鼓吹RAC的文章和言论。尽管它很强大,但是也并没有很普及,即便它很强大!

在了解ReactiveCocoa前,先简单粗略回顾一下block的几种常用用法及几种常见的编程思想。

block常用用法

1. block作为对象的属性

新建一个Single工程,创建一个Person类,在Person类中声明一个block。系统有个内置的block代码块

注意,在非ARC环境下block需要使用copy声明,使用copy的原因,是把block从栈区拷贝到堆区,因为栈区中的变量出了作用域之后就会被销毁,无法在全局使用,所以应该把栈区的属性拷贝到堆区中全局共享,这样就不会被销毁了,在MRC手动管理的就是堆区,不需要系统管理,MRC环境必须使用copy把变量拷贝到全局的堆区。而如果在ARC环境下就可以不使用copy修饰,因为ARC下的属性本来就在堆区。所以现在在默认是ARC环境下依然沿袭了之前的使用方式copy,使用strong也没有问题。

在Person类中声明一个block

@property (nonatomic, strong) void(^myBlock)(void);

在控制器中持有一个Person对象,并声明一个block接受Person的这个block属性

self.p = [[Person alloc] init];

void(^testBlock)(void) = ^() {

 NSLog(@"block as params");

    };

self.p.myBlock = testBlock

在点击屏幕的时候实现这个block

self.p.myBlock();

2.block作为方法的参数

同样在Person类中声明一个带有block参数的方法并实现

- (void)eat:(void(^)(NSString *someThing))block;

- (void)eat:(void(^)(NSString * someThing))block {

block(@"delicious");

}

在控制器中调用,其中block中的已知数据类型的参数是Person对象提供给外界使用的,AFNetworing中返回的block结果就是如此。

[self.p eat:^(NSString *someThing) {
       NSLog(@"eat %@",someThing);
   }];

3.作为方法的返回值

block作为方法的返回值这种用法开发中其实并不常用,但是一些第三方,最典型的常用约束库
Masonry库就将block作为方法的返回值来使用。

同样在Person中声明一个run方法并实现,将block作为方法的返回值

- (void(^)(int m))run;

- (void(^)(int m))run {
    return ^(int m) {
        NSLog(@"run %d",m);
    };
}

在控制器找那个可以这样调用

- (void)methodTree {
    void(^block)(int m) =  self.p.run;
    block(100);
}

并没需要一个block先接收再实现,可以直接调用

[self.p run](100);

有没有觉得这种调用怪怪的,其实就是调用方法返回一个block再调用 这个block,block的调用方式就是block()

有没有更简单的写法呢?答案是有!

self.p.run(100);

为什么可以使用点语法调用?

在OC中只有形如get类型的方法才可以使用点语法点出来,否则是能使用形如[self run]这种加空格的调用方法。而这种将block作为返回值的方法语法类型和get方法相同,所以可以使用点语法调用,但是如果将block作为方法的返回值,而且这个方法又有其他参数,是无法通过点语法调用的,因为系统判断这不是get方法,get方法是没有参数的。

Demo地址

编程思想

了解几种常规的编程思想,如链式编程,响应式编程,函数式编程等。

1. 链式编程思想

链式编程就是可以通过”点”语法,将需要执行的代码块连续的书写下去,使得代码简单易读,书写方便。典型的使用链式编程思想的有Masonry库,通过分析Mansory库的使用机制来了解一下链式编程。

同样新建一个Single工程,添加一个View

UIView *redView = [[UIView alloc] init];

redView.backgroundColor = [UIColor redColor];

[self.view addSubview:redView];

[redView mas_makeConstraints:^(MASConstraintMaker *make) {

    make.left.top.equalTo(@100);

    make.right.bottom.equalTo(@-100);

}];

上面的MASConstraintMaker,我们称之为约束制造者,这里面就将block作为方法的参数来使用了,为什么对redView添加约束,而block里面使用的确是约束制造者呢?

点进mas_makeConstraints这个方法看一看

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

首先这个方法关掉了老式的自动布局self.translatesAutoresizingMaskIntoConstraints = NO;,防止和Autolayout冲突,然后创建一个约束制造者并返回。而在创建这个约束制造者的时候将self作为参数传入,[[MASConstraintMaker alloc] initWithView:self];,而这个self就是redView,即这个方法的调用者。到这个方法里面看一下

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;

    self.view = view;
    self.constraints = NSMutableArray.new;

    return self;
}

可见这个约束制造者将view作为属性绑定起来了,到[constraintMaker install];这个方法里面
可以看到就是我们不愿意写的各种
[NSLayoutConstraint constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:]

通过上面流程我们知道mas_makeConstraints的执行流程

1.创建约束制造者MASConstraintMaker,并且绑定控件,生成一个保存所有约束的数组

2.执行mas_makeConstraints传入的Block

3.让约束制造者安装约束!

3.1 清空之前的所有约束

3.2 遍历约束数组,一个一个安装

接下来看一下block中的代码make.left.top.equalTo(@100);,这里使用的即是链式编程,我们发现make.left后依然可以.top,.right,为什么可以这样写?点进去可以看到

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

这是一个get方法,上文有提到OC中只有形如get方法才可以使用点语法点出来。而这个get方法返回的是一个MASConstraint,你会发现MASConstraint类中这些全是get方法,而返回值全是调用者本身MASConstraint

随便点进去一个get方法

- (MASConstraint *)addConstraintWithLayoutAttribute:    (NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be     chained before defining the constraint relation");

    return [self.delegate constraint:self     addConstraintWithLayoutAttribute:layoutAttribute];
}

可以发现都是通过代理来实现,最终返回的是一个约束MASConstraint,并添加到约束数组中,这也就可以解释了为什么make.left后依然可以.top,.right继续点出来,事实上只要你愿意,你可以一直这么写下去,因为返回值都是这些方法的调用者。而这也是链式编程最重要的思想特点,即:

方法的返回值必须要有方法调用者

为什么是 “要有方法调用者”而不是“要是方法的调用者”呢?

我们接着看make.left.top.equalTo(@100)中的equalTo方法

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute,     NSLayoutRelationEqual);
    };
}

可见,这个equalTo方法返回的不是方法调用者,而是一个返回值为方法调用者的block。而我们在调用equalTo方法的时候紧跟着后面加了“(100)”,也就是说调用了这个返回的block,bclok执行后返回的又是一个方法调用者MASConstraint。也就是说,只要你愿意,在make.left.top.equalTo(@100)之后,你依然可以继续调用get方法,形如

make.left.top.equalTo(@100).right.bottom.equalTo(@-100);

只是在equalTo之后继续调用get方法,后面的约束值会覆盖掉前面的约束值。所以,理论上可以,然而并不应该这么做。

那如果我们自己也想玩一个具备链式编程特点的类该怎么写呢?记住,链式编程,方法返回值必须要有方法调用者。然后可以参考Masonary库粗略玩一下。
  1. 先创建一个计算管理器吧CalculateManager,定义简单的加减乘除方法并逐一趋实现,并声明一个属性去保存计算结果, 这些方法都有一个共同的特点,就是返回值是一个block,而block的返回值是这个类本身,这就确保了在调用方法后依然可以继续调用。
@interface CalculateManager : NSObject

@property(assign,nonatomic)double result;

+ (instancetype)sharedInstance;

-(CalculateManager *(^)(float value))add;

-(CalculateManager *(^)(float value))minus;

-(CalculateManager *(^)(float value))multi;

-(CalculateManager *(^)(float value))divis;

@end

@implementation CalculateManager

    + (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static CalculateManager *mgr = nil;
    dispatch_once(&onceToken, ^{
        mgr = [[self alloc] init];
    });
    return mgr;
}

-(CalculateManager * (^)(float value))add{
    return ^(float value){
        _result += value;
        return self;
    };
}

-(CalculateManager *(^)(float value))minus {
    return ^(float value){
        _result -= value;
        return self;
    };
}

-(CalculateManager *(^)(float value))multi {
    return ^(float value){
        _result *= value;
        return self;
    };
}

-(CalculateManager *(^)(float value))divis {
    return ^(float value){
        _result /= value;
        return self;
    };
}

2.接下来你会发现已经可以使用链式编程了

CalculateManager *manger = [[CalculateManager alloc] init];
double result = manger.add(3).minus(3).result;

只要你愿意,依然可以在返回结果前继续点下去。但是你会发现在返回结果后没法继续调用方法了。而且这种调用依然需要创建实例,和Masonry形式依然不同,那就继续改进。在使用Masonry时你会发现任何继承自UIView的控件都可以调用mas_makeConstraints或其他方法,那说明这绝壁是个UIView的分类。仿照此思路,我们创建一个NSObject的分类,使得NSObject任何子类实例都可以调用。

 @interface NSObject (Calculate)

- (double)fn_calculate:(void(^)(CalculateManager * mgr))block;

@end

@implementation NSObject (Calculate)

- (double)fn_calculate:(void(^)(CalculateManager * mgr))block {
    CalculateManager * mgr = [CalculateManager sharedInstance];
    block(mgr);
    return mgr.result;
}

上面这个方法的参数是参数为CalculateManager的block,返回结果为CalculateManager的属性值。现在就可以这么使用了:

double result = [self fn_calculate:^(CalculateManager *mgr) {
        mgr.add(10).add(10).divis(3).multi(5.8).minus(2.2);
    }];

@end

只要你愿意,你可以一直点下去。这种写法其实在OC中非常少见,但是像Masonry一些第三方库都使用了这种思想。

Demo下载

函数编程思想(FP)

函数编程是把操作尽量写成一系列嵌套的函数或者方法调用。每个方法必须要有返回值,把函数或者block当作参数。

继续上面的例子,在CalculateManager中声明并实现一个方法

- (instancetype)manager:(double(^)(double))block;


- (instancetype)manager:(double(^)(double))block {
    _result = block(_result);
        return self;
}

这个方法参数是一个block,这个block又有返回值和参数,方法的返回值又是自己的一个实例。这里方法的返回值完全可以不是自己的实例,而是一个具体的结果。但是如果是一个具体的结果,如果这个类中有很多其他的属性,那对应的就相应有很多雷同的方法,所以返回一个实例,调用的时候可以通过这个实例去获得对应的属性值。

CalculateManager * mgr = [[CalculateManager alloc] init];
    double h_Result = [[mgr manager:^double(double parameter) {
       parameter += 10;
          parameter *= 2;
          return parameter;
   }] result];
   NSLog(@"%f",h_Result);

响应式编程思想(RP)

响应式编程不需要考虑调用顺序,只需要考虑结果。 iOS中最具代表性的就是KVO的使用。看一下简单实用。

先创建一个Person类,声明一个name属性,监听Personname属性

你会发现没什么毛病。如果这时候,我们把name属性变成一个成员变量,再去使用箭头函数方式去修改name的值

这个时候你会发现并没有走进监听函数。这和上面正常使用的时候有什么区别呢?那就是修改name属性的时候,成员变量不会调用set方法, 而使用点语法总是会调用set函数。有理由相信,使用KVO的时候重写了属性的set方法。

怎么可以重写set方法?

  1. 可以使用分类的形式重写
  2. 可以使用子类去重写

想想看,苹果有没有可能去使用分类的形式去重写? 绝壁不可能!因为如果分类中去重写属性的set方法,则原有类中即便重写了该属性的set方法也不会调用,会优先使用分类中的方法。而我们在日常开发中,经常会去重写set方法去做一些其他操作。所以只可能是第二种情况,使用子类去重写了set方法。那过程应该是怎样的呢?

  1. 自定义被监听类的子类
  2. 重写被监听属性的set方法,在内部调用super恢复父类的做法,通知观察者

还有一个问题? 如何让外界调用自定义子类的方法?所以还有最重要的一点

iOS中有个isa指针, 修改当前对象的isa指针,指向自定义的子类!

验证一下猜测,查看一下在走进监听方法后对象的isa指针指向。

知道了KVO的内部机制,那我们自己就可以写一个简单的KVO

以上面为例,监听Person类的name属性,创建一个NSObject的分类,添加监听方法并实现,最后再使用添加的这个方法监听即可。

注意在使用objc_msgSend时需要在Build Setting中将Enable Strict Checking of objc_msgSend Calls设置为NO

Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

机器学习-CoreML目标跟踪


物体跟踪需要处理连续的视频帧,所以需要VNSequenceRequestHandler类来处理多帧图像,之前识别单张图片使用的是VNImageRequestHandler类。跟踪多物体时,可以使用VNDetectedObjectObservation.uuid区分跟踪对象,并做相应处理。这里粗略演示一下追踪单个物体。

  1. 前期UI准备

2 捕获对象,设置精准追踪

3.效果演示



Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

机器学习-CoreML人脸识别


使用Vision框架进行人脸识别,不仅可以识别出人脸的面部轮廓,还可以识别出人脸的特征点,比如眼睛、鼻子、嘴巴等。进行人脸请求的类是VNDetectFaceRectanglesRequest,人脸特征请求的类是VNDetectFaceRectanglesRequest,请求结果的信息类是VNFaceObservation。下面直接上代码。

  1. 搭建基本UI

  1. 识别面部轮廓
1
2
3
4
5
6
func processImage(image:UIImage) {
self.resultLabel.text = "processing..."
let handler = VNImageRequestHandler.init(cgImage: image.cgImage!)
let request = VNDetectFaceRectanglesRequest.init(completionHandler: handleFaceDetect)
try? handler.perform([request])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func handleFaceDetect(request:VNRequest,error:Error?) {
guard let obversations = request.results as? [VNFaceObservation] else {
fatalError("no face")
}
self.resultLabel.text = "\(obversations.count)face"

for subView in self.btnImageView.subviews where subView.tag == 100 {
subView.removeFromSuperview()
}

for faceObversion in obversations {
self.showFaceContour(faceObversion: faceObversion, toView: self.btnImageView)

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func showFaceContour(faceObversion face : VNFaceObservation, toView view : UIView) {
let boundingBox = face.boundingBox
let imageBoundingBox = view.bounds

let w = boundingBox.size.width * imageBoundingBox.width
let h = boundingBox.size.height * imageBoundingBox.height

let x = boundingBox.origin.x * imageBoundingBox.width
let y = imageBoundingBox.height - boundingBox.origin.y * imageBoundingBox.height - h

let subview = UIView(frame: CGRect(x: x, y: y, width: w, height: h))
subview.layer.borderColor = UIColor.green.cgColor
subview.layer.borderWidth = 1
subview.layer.cornerRadius = 1
subview.tag = 100
view.addSubview(subview)
}

  • 相当于在人脸上画了一个轮廓视图添加上去。boundingBox是CGRect类型,但是boundingBox返回的是x,y,w,h的比例(value:0~1),需要进行转换。原始坐标系的原点是左上角,而这里是左下角,并且y轴的方向和原始坐标系相反。

3.面部轮廓展示

4.识别面部特征

1
2
3
4
5
6
func processImage(image:UIImage) {
self.resultLabel.text = "processing..."
let handler = VNImageRequestHandler.init(cgImage: image.cgImage!)
let request = VNDetectFaceLandmarksRequest.init(completionHandler: handleFaceDetect)
try? handler.perform([request])
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func handleFaceDetect(request:VNRequest,error:Error?) {
guard let obversations = request.results as? [VNFaceObservation] else {
fatalError("no face")
}
self.resultLabel.text = "\(obversations.count) face"

var landmarkRegions : [VNFaceLandmarkRegion2D] = []

for faceObversion in obversations {
landmarkRegions += self.showFaceFeature(faceObversion: faceObversion, toView: self.btnImageView)
resultImage = self.drawOnImage(source: self.resultImage,
boundingRect: faceObversion.boundingBox,
faceLandmarkRegions: landmarkRegions)
}
self.btnImageView.setBackgroundImage(resultImage, for: .normal)
}

获取特征点

1
2
3
4
5
6
7
8
9
10
11
func showFaceFeature(faceObversion face: VNFaceObservation, toView view: UIView) ->[VNFaceLandmarkRegion2D] {
guard let landmarks = face.landmarks else { return [] }
var landmarkRegions: [VNFaceLandmarkRegion2D] = []
if let rightEye = landmarks.rightEye {
landmarkRegions.append(rightEye)
}
if let nose = landmarks.nose {
landmarkRegions.append(nose)
}
return landmarkRegions
}

重新绘制图片(含面部特征)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func drawOnImage(source: UIImage,
boundingRect: CGRect,
faceLandmarkRegions: [VNFaceLandmarkRegion2D]) -> UIImage {
UIGraphicsBeginImageContextWithOptions(source.size, false, 1)
let context = UIGraphicsGetCurrentContext()!
context.translateBy(x: 0, y: source.size.height)
context.scaleBy(x: 1.0, y: -1.0)
context.setBlendMode(CGBlendMode.colorBurn)
context.setLineJoin(.round)
context.setLineCap(.round)
context.setShouldAntialias(true)
context.setAllowsAntialiasing(true)
context.setLineWidth(5.0)
context.setStrokeColor(UIColor.black.cgColor)

// origin image
let rect = CGRect(x: 0, y:0, width: source.size.width, height: source.size.height)
context.draw(source.cgImage!, in: rect)

// face contour
let rectWidth = source.size.width * boundingRect.size.width
let rectHeight = source.size.height * boundingRect.size.height
context.addRect(CGRect(x: boundingRect.origin.x * source.size.width, y:boundingRect.origin.y * source.size.height, width: rectWidth, height: rectHeight))
context.drawPath(using: CGPathDrawingMode.stroke)

// draw face feature
context.setStrokeColor(UIColor.red.cgColor)
for faceLandmarkRegion in faceLandmarkRegions {
var points: [CGPoint] = []
for i in 0..<faceLandmarkRegion.pointCount {
let point = faceLandmarkRegion.normalizedPoints[i]
let p = CGPoint(x: CGFloat(point.x), y: CGFloat(point.y))
points.append(p)
}
let mappedPoints = points.map { CGPoint(x: boundingRect.origin.x * source.size.width + $0.x * rectWidth, y: boundingRect.origin.y * source.size.height + $0.y * rectHeight) }
context.addLines(between: mappedPoints)
context.drawPath(using: CGPathDrawingMode.stroke)
}

let coloredImg : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return coloredImg
}

VNDetectFaceLandmarksRequest请求返回的也是VNFaceObservation,但是这个时候VNFaceObservation 对象的landmarks属性就会有值,这个属性里面存储了人物面部特征的点
如:

  • faceContour:脸部轮廓
  • leftEye、rightEye: 左眼、右眼
  • nose: 鼻子
  • noseCrest: 鼻嵴
  • outerLips、innerLips: 外唇、内唇
  • leftEyebrow、rightEyebrow: 左眉毛、右眉毛
  • leftPupil、rightPupil: 左瞳、右瞳

5.面部特征点展示



Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

机器学习-CoreML文字识别


  1. 获取带有文字的图片
    1
    2
    3
    4
    5
    6
    func btnPressed(_ sender: Any) {
    let picker = UIImagePickerController()
    picker.delegate = self
    picker.sourceType = .savedPhotosAlbum
    present(picker, animated: true, completion: nil)
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 // UIImagePickerControllerDelegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion: nil)
guard let image = (info[UIImagePickerControllerOriginalImage] as? UIImage) else {
fatalError("no image")
}
self.btnImageView.setBackgroundImage(image, for: .normal)
// self.btnImageView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
// if self.btnImageView.layer.sublayers != nil {
// for layer in self.btnImageView.layer.sublayers! {
// layer.removeFromSuperlayer()
// }
// }

var textLayers:[CAShapeLayer] = []
if let cgImage = image.cgImage {
let textRecognizedRequest = VNDetectTextRectanglesRequest.init(completionHandler: { (request:VNRequest, error:Error?) in

if (request.results as? [VNTextObservation]) != nil {
textLayers = self.addShapesToText(obversations: request.results as! [VNTextObservation], view: self.btnImageView)
for layer in textLayers {
self.btnImageView.layer.addSublayer(layer)
}
}
})

let handler = VNImageRequestHandler.init(cgImage: cgImage, options: [:])
guard let _ = try? handler.perform([textRecognizedRequest]) else {
fatalError("failed")
}
}
}

2.添加Layer展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func addShapesToText(obversations:[VNTextObservation],view:UIView) -> [CAShapeLayer] {
let layers: [CAShapeLayer] = obversations.map { observation in

let w = observation.boundingBox.size.width * view.bounds.width
let h = observation.boundingBox.size.height * view.bounds.height
let x = observation.boundingBox.origin.x * view.bounds.width
let y = view.bounds.height - observation.boundingBox.origin.y * view.bounds.height - h

let layer = CAShapeLayer()
layer.frame = CGRect(x: x , y: y, width: w, height: h)
layer.borderColor = UIColor.green.cgColor
layer.borderWidth = 2
layer.cornerRadius = 3

return layer
}

return layers
}

3.效果展示



Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

机器学习-CoreML结合Siri


使用AVFoundation拍摄照片,再使用Core ML处理分析照片,最后通过Siri告知拍摄的对象是什么。
  1. 前期配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    var captureView : UIView!
    var synthe = AVSpeechSynthesizer()
    var uttrence = AVSpeechUtterance()
    var predicte = ""

    var captureSession : AVCaptureSession!
    var cameraOutput : AVCapturePhotoOutput!
    var previewLayer : AVCaptureVideoPreviewLayer!

    override func viewDidLoad() {
    super.viewDidLoad()

    captureView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
    self.view.addSubview(captureView)
    setupCamera()
    }

    func setupCamera() {
    captureSession = AVCaptureSession()
    captureSession.sessionPreset = AVCaptureSession.Preset.photo
    cameraOutput = AVCapturePhotoOutput()

    let device = AVCaptureDevice.default(for: .video)
    if let input = try? AVCaptureDeviceInput.init(device: device!) {
    if(captureSession.canAddInput(input)) {
    captureSession.addInput(input)

    if(captureSession.canAddOutput(cameraOutput)) {
    captureSession.addOutput(cameraOutput)
    }

    previewLayer = AVCaptureVideoPreviewLayer.init(session: captureSession)
    previewLayer.videoGravity = .resizeAspectFill
    previewLayer.frame = CGRect.init(x: 0, y: 0, width: 200, height: 200)
    captureView.layer.addSublayer(previewLayer)
    captureSession.startRunning()
    }
    }
    }
  2. 获取照片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func launchUI() {
    let setting = AVCapturePhotoSettings()
    // xcode9's Bug
    let previewPixelType = setting.__availablePreviewPhotoPixelFormatTypes.first!
    let previewFormat = [ kCVPixelBufferPixelFormatTypeKey as String: previewPixelType,
    kCVPixelBufferWidthKey as String: "\(captureView.bounds.size.width)",
    kCVPixelBufferHeightKey as String: "\(captureView.bounds.size.height)"] as [String : Any]
    setting.previewPhotoFormat = previewFormat
    cameraOutput.capturePhoto(with: setting, delegate: self)
    }

    // AVCapturePhotoCaptureDelegate
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if error != nil {
    print("error occured: \(error!.localizedDescription)")
    }

    if let imageData = photo.fileDataRepresentation(),let image = UIImage.init(data: imageData) {
    // Predict
    self.predict(image: image)
    }
    }

在Xcode9上availablePreviewPhotoPixelFormatTypes这个属性前面需要加上双下划线,更高版本则不需要

3.图片处理预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 func predict(image:UIImage) {
// if let data = UIImagePNGRepresentation(image) {
// let fileName = getDocumentsDirectory().appendingPathComponent("captureImage")
// print("fleName:\(fileName)")
// try? data.write(to: fileName)
// }
let model = try! VNCoreMLModel.init(for: VGG16().model)
let request = VNCoreMLRequest.init(model: model, completionHandler: { (request:VNRequest, error:Error?) in
weak var weakSelf = self
guard let results = request.results as? [VNClassificationObservation] else {
fatalError("no result")
}
var bestPrediction = ""
var bestConfidence:VNConfidence = 0

for classfication:VNClassificationObservation in results {
if classfication.confidence > bestConfidence {
bestConfidence = classfication.confidence
bestPrediction = classfication.identifier
}
}

weakSelf!.say(string: "\(bestPrediction)")

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5, execute: {
weakSelf!.launchUI()
})
})
let handler = VNImageRequestHandler.init(cgImage: image.cgImage!)
try? handler.perform([request])
}

4.语音播报

1
2
3
4
5
6
7
8
func say(string: String) {
uttrence = AVSpeechUtterance.init(string: string)
uttrence.rate = 0.3
uttrence.voice = AVSpeechSynthesisVoice.init(language: "zh_CN")
uttrence.pitchMultiplier = 0.8
uttrence.postUtteranceDelay = 0.2
synthe.speak(uttrence)
}

这里面选择的是汉语,不管选择的是什么语音种类,英语都是支持的

5.效果演示

Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

本站总访问量 本文总阅读量