注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

手把手教你在Flutter项目优雅的使用ORM数据库

Flutter ORM数据库介绍Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是...
继续阅读 »

Flutter ORM数据库介绍

Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是最低效的了。例如IOS平台有coredata、realm等等的框架提供便捷的数据库操作,但来到flutter就又倒退回去裸写sql,这对大部分团队都是重大的成本。

本文将详细介绍一种在Flutter项目中优雅的使用ORM数据库的方法,我们使用的ORM框架是包含在一个Flutter插件flutter_luakit_plugin(如何使用可参考介绍文章)中的其中一个功能,本文只详细介绍这套ORM框架的使用和实现原理。我们给出了一个demo。

我们demo中实现了一个简单的功能,从一个天气网站上查询北京的天气信息,解析返回的json然后存数据库,下次启动优先从数据库查数据马上显示,再发请求向天气网站更新天气信息,就这么简单的一个功能。虽然功能简单,但是我们99%日常的业务逻辑也就是由这些简单的逻辑组成的了。下面是demo运行的效果图。


看完运行效果,我们开始看看ORM数据库的使用。ORM数据库的核心代码都是lua,其中WeatherManager.lua是业务逻辑代码,其他的lua文件是ORM数据库的核心代码,全部是lua实现的,所有代码文件加起来也就120k左右,非常轻量。

针对上面提到的天气信息的功能,我们来设计数据模型,从demo的展示我们看到每天天气信息包含几个信息,城市名、日出日落时间、最高温度、最低温度、风向、风力,然后为了区分是哪一天的数据,我们给每条信息加上个id的属性,作为主键。想好我们就开始定义第一个ORM数据模型,有几个必要的信息,db名,表名,后面的就是我们需要的各个字段了,我们提供IntegerField、RealField、BlobField、TextField、BooleandField。等常用的数据类型。weather 就是这个模型的名字,之后我们weather为索引使用这个数据模型。定义模型代码如下。

weather = {
__dbname__ = "test.db",
__tablename__ = "weather",
id = {"IntegerField",{unique = true, null = false, primary_key = true}},
wind = {"TextField",{}},
wind_direction = {"TextField",{}},
sun_info = {"TextField",{}},
low = {"IntegerField",{}},
high = {"IntegerField",{}},
city = {"TextField",{}},
},

定义好模型后,我们看看如何使用,我们跟着业务逻辑走,首先网络请求回来我们要生成模型对象存到数据库,分下面几步

获取模型对象

local Table = require('orm.class.table')
local _weatherTable = Table("weather”)

准备数据,建立数据对象

local t = {}
t.wind = flDict[v.fg]
t.wind_direction = fxDict[v.ff]
t.sun_info = v.fi
t.low = tonumber(v.fd)
t.high = tonumber(v.fc)
t.id = i
t.city = city
local weather = _weatherTable(t)

保存数据

weather:save()

读取数据

_weatherTable.get:all():getPureData()

是不是很简单,很优雅,什么建表、拼sql、transation、线程安全等等都不用考虑,傻瓜式地使用,一个业务就几行代码搞定。这里只演示了简单的存取,更多的select、update、联表等高级用法可参考db_test demo。

Flutter ORM数据库原理详解

好了,上面已经介绍完如何使用了,如果大家仅仅关心使用下面的可以不看了,如果大家想了解这套跨平台的ORM框架的实现原理,下面就会详细介绍,其实了解了实现原理,对大家具体业务使用还是很有好处的,虽然我感觉大家用的时候极少了解原理。

我们把orm框架分为三层接入层,cache层,db操作层,三个层分别处于对应的线程,具体可以参考下图。接入层可以在任意线程发起,接入层也是每次数据库操作的发起点,上面的demo所有操作都是在接入层,cache层,db操作层仅仅是ORM内部划分,对使用者来讲不需要关心cache层和db操作层。我们把所有的操作分成两种,db后续相关的,和db后续无关的。


db后续无关的操作是从接入层不同的线程进入到cache层的队列,所有操作在这个队列里先同步完成内存操作,然后即可马上返回接入层,异步再到db操作层进行db操作。db后续无关的操作包括 save、update、delete。

db后续相关的操作依赖db操作层操作的结果,这样的话就必须等真实的db操作完成了再返回接入层。db后续相关的操作包括select。

要做到这种数据同步,我们必须先把orm操作接口抽象化,只给几个常用的接口,所有操作都必须通过指定的接口来完成。我们总结了如下基本操作接口。

1、save

2、select where

3、select PrimaryKey

4、update where

5、update PrimaryKey

6、delete where

7、delete PrimaryKey

这七种操作只要在操作前返回前对内存中的cache做相应的处理,即可保证内存cache始终和db保持一致,这样以后我们就可以优先使用cache层的数据了。这七种操作的实现逻辑,这里先说明一下,cache里面的对象都是以主键为key,orm对象为value的形式存储在内存中的,这些控制逻辑是写在cache.lua里面的。

下面详细介绍七种基本操作的逻辑。

save操作,同步修改内存cache,然后马上返回接入层,再异步进行db replace into 的操作


where条件select,这个必须先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


select PrimaryKey,就是选一定PrimaryKey值的orm对象,这个操作首先看cache里面是否有primarykey 值的orm对,如果有,直接返回,如果没有,先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


update where,先同步到db线程通过where 条件select出需要update的主键值,根据主键值和需要update的内容,同步更新内存cache,然后异步进行db的update操作


update PrimaryKey,根据PrimaryKey进行update操作,先同步更新内存cache,然后异步进行db的update操作


delete where,先同步到db线程通过where 条件select出需要delete的主键值,根据主键值删除内存cache,然后异步进行db的delete操作


delete PrimaryKey,根据PrimaryKey进行delete操作,先同步删除内存cache,然后异步进行db的delete操作


只要保证上面七种基本操作逻辑,即可保证cache中的内容和db最终的内容是一致的,这种尽量使用cache的特性可以提升数据库操作的效率,而且保证同一个db的所有操作都在指定的cache线程和db线程里面完成,也可以保证线程安全。

最后,由于我们所有的db操作都集中起来了,我们可以定时的transation 保存,这样可以大幅提升数据库操作的性能。

结语

目前Flutter领域最大的痛点就是数据库操作,本文提供了一种优雅使用ORM数据库的方法,大幅降低了使用数据库的门槛。希望这篇文章和flutter_luakit_plugin可以帮到大家更方便的开发Flutter
应用。

链接:https://www.jianshu.com/p/62500ae08a07

收起阅读 »

iOS开发中的小玩意儿-加速计和陀螺仪

前言最近因为工作需要对加速计和陀螺仪进行学习和了解,过程中有所收获。正文一、加速计iPhone在静止时会受到地球引力,以屏幕中心为坐标原点,建立一个三维坐标系(如右图),此时iPhone收到的地球引力会分布到三个轴上。iOS开发者可以通过CoreMotion框...
继续阅读 »

前言

最近因为工作需要对加速计和陀螺仪进行学习和了解,过程中有所收获。

正文

一、加速计

iPhone在静止时会受到地球引力,以屏幕中心为坐标原点,建立一个三维坐标系(如右图),此时iPhone收到的地球引力会分布到三个轴上。
iOS开发者可以通过CoreMotion框架获取分布到三个轴的值。如果iPhone是如图放置,则分布情况为x=0,y=-1.0,z=0。
在CoreMotion中地球引力(重力)的表示为1.0。

手机如果屏幕朝上的放在水平桌面上,此时的(x,y,z)分布是什么?


上面答案是(0,0, -1.0);

如何检测手机的运动?
CoreMotion框架中有CMDeviceMotion类,其中的gravity属性用来描述前面介绍的重力;另外的userAcceleration是用来描述手机的运动。
当手机不动时,userAcceleration的(x, y, z)为(0, 0, 0);
当手机运动,比如在屏幕水平朝上的自由落体时,检测到的(x, y, z)将为(0, 0, 1);
当手机屏幕水平朝上,往屏幕左边以9.8m/s2的加速度运动时,检测到的(x, y, z)将为(1, 0, 0);

1、gravity是固定不变,因为地球引力的不变;但是xyz的分布会变化,收到手机朝向的影响;
2、userAcceleration是手机的运动相关属性,但是检测到的值为运动加速度相反的方向;
3、一种理解加速计的方式:在水平的路上有一辆车,车上有一个人;当车加速向右运动时,人会向左倾斜;此时可以人不需要知道外面的环境如何,根据事先在车里建立好的方向坐标系,可以知道车在向右加速运动。

二、加速计的简单应用

图片悬浮
手机旋转,但是图片始终保持水平。


实现流程
1、加载图片,创建CMMotionManager;
2、监听地球重力的变化,根据x和y轴的重力变化计算出来手机与水平面的夹角;
3、将图片逆着旋转相同的角度;
x、y轴和UIKit坐标系相反,原点在屏幕中心,向上为y轴正方向,向右为x轴正方向,屏幕朝外是z轴正方向;
在处理图片旋转角度时需要注意。

三、陀螺仪

如图,建立三维坐标系;
陀螺仪描述的是iPhone关于x、y、z轴的旋转速率;
静止时(x, y, z)为(0, 0, 0);
当右图手机绕Y轴正方向旋转,速率为每秒180°,则(x, y, z)为(0, 0, 3.14);


陀螺仪和加速计是同样的坐标系,但是新增了旋转的概念,可以用右手法则来辅助记忆;
陀螺仪回调结构体的单位是以弧度为单位,这个不是加速度而是速率;

四、CoreMotion的使用
CoreMotion的使用有两种方式 :

1、Push方式:设置间隔,由manager不断回调;

self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 0.2;
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {

}];

2、Pull方式:启动监听,自定义定时器,不断读取manager的值;

self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 0.2;
[self.motionManager startDeviceMotionUpdates];
// self.motionManager.deviceMotion 后续通过这个属性可以直接读取结果

iOS系统在监听到运动信息的时候,需要把信息回调给开发者,方式就有push和pull两种;
push 是系统在规定的时间间隔,不断的回调;
pull 是由开发则自己去读取结果值,但同样需要设定一个更新频率;
两种方式的本质并无太大区别,都需要设置回调间隔,只是读取方式的不同;
在不使用之后(比如说切后台)要关闭更新,这是非常耗电量的操作。

五、demo实践

基于加速计,做了一个小游戏,逻辑不复杂详见具体代码,分享几个处理逻辑:

1、圆球的边界处理;(以球和右边界的碰撞为例)

if (self.ballView.right > self.gameContainerView.width) {
self.ballView.right = self.gameContainerView.width;
self.ballSpeedX /= -1;
}

2、圆球是否触碰目标的检测;

- (BOOL)checkTarget {
CGFloat disX = (self.ballView.centerX - self.targetView.centerX);
CGFloat disY = (self.ballView.centerY - self.targetView.centerY);
return sqrt(disX * disX + disY * disY) <= (kConstBallLength / 2 + kConstTargetLength / 2);
}

3、速度的平滑处理;

static CGFloat lySlowLowPassFilter(NSTimeInterval elapsed,
GLfloat target,
GLfloat current) {
return current + (4.0 * elapsed * (target - current));
}


总结

加速计和陀螺仪的原理复杂但使用简单,实际应用也比较广。
之前就用过加速计和陀螺仪,但是没有系统的学习过。在完整的学习一遍之后,我才知道原来加速计的单位是以重力加速度(9.8 m/s2)为标准单位,陀螺仪的数据仅仅是速率,单位是弧度每秒。
上面的小游戏代码地址在Github

链接:https://www.jianshu.com/p/6d6b213912f5

收起阅读 »

iOS缓存设计(YYCache思路)

iOS缓存设计(YYCache思路)前言:前段时间业务有缓存需求,于是结合YYCache和业务需求,做了缓存层(内存&磁盘)+ 网络层的方案尝试由于YYCache 采用了内存缓存和磁盘缓存组合方式,性能优良,这里拿它的原理来说下如何设计一套缓存的思路,...
继续阅读 »

iOS缓存设计(YYCache思路)

前言:
前段时间业务有缓存需求,于是结合YYCache和业务需求,做了缓存层(内存&磁盘)+ 网络层的方案尝试
由于YYCache 采用了内存缓存和磁盘缓存组合方式,性能优良,这里拿它的原理来说下如何设计一套缓存的思路,并结合网络整理一套完整流程

目录

初步认识缓存
如何优化缓存(YYCache设计思想)
网络和缓存同步流程
一、初步认识缓存

1. 什么是缓存?

我们做一个缓存前,先了解它是什么,缓存是本地数据存储,存储方式主要包含两种:磁盘储存和内存存储

1.1 磁盘存储

磁盘缓存,磁盘也就是硬盘缓存,磁盘是程序的存储空间,磁盘缓存容量大速度慢,磁盘是永久存储东西的,iOS为不同数据管理对存储路径做了规范如下:
1、每一个应用程序都会拥有一个应用程序沙盒。
2、应用程序沙盒就是一个文件系统目录。
沙盒根目录结构:Documents、Library、temp。

磁盘存储方式主要有文件管理和数据库,其特性:


1.2 内存存储

内存缓存,内存缓存是指当前程序运行空间,内存缓存速度快容量小,它是供cpu直接读取,比如我们打开一个程序,他是运行在内存中的,关闭程序后内存又会释放。
iOS内存分为5个区:栈区,堆区,全局区,常量区,代码区

栈区stack:这一块区域系统会自己管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量,深递归,函数循环调用都可能导致内存耗尽而运行崩溃。
堆区heap:与栈区相对,这一块一般由我们自己管理,比如alloc,free的操作,存储一些自己创建的对象。
全局区(静态区static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的会分开存储在相邻的区域,程序结束后系统会释放
常量区:存储常量字符串和const常量
代码区:存储代码

在程序中声明的容器(数组 、字典)都可看做内存中存储,特性如下:


2. 缓存做什么?

我们使用场景比如:离线加载,预加载,本地通讯录...等,对非网络数据,使用本地数据管理的一种,具体使用场景有很多

3. 怎么做缓存?

简单缓存可以仅使用磁盘存储,iOS主要提供四种磁盘存储方式:

NSKeyedArchiver: 采用归档的形式来保存数据, 该数据对象需要遵守NSCoding协议, 并且该对象对应的类必须提供encodeWithCoder:和initWithCoder:方法.

//自定义Person实现归档解档
//.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCoding>
@property(nonatomic,copy) NSString * name;

@end

//.m文件
#import "Person.h"
@implementation Person
//归档要实现的协议方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:@"name"];
}
//解档要实现的协议方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
@end

使用归档解档

// 将数据存储在path路径下归档文件
[NSKeyedArchiver archiveRootObject:p toFile:path];
// 根据path路径查找解档文件
Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

缺点:归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据,如果想改动数据的某一小部分,需要解压整个数据或者归档整个数据。

NSUserDefaults: 用来保存应用程序设置和属性、用户保存的数据。用户再次打开程序或开机后这些数据仍然存在。
NSUserDefaults可以存储的数据类型包括:NSData、NSString、NSNumber、NSDate、NSArray、 NSDictionary。

// 以键值方式存储
[[NSUserDefaults standardUserDefaults] setObject:@"value" forKey:@"key"];
// 以键值方式读取
[[NSUserDefaults standardUserDefaults] objectForKey:@"key"];

Write写入方式:永久保存在磁盘中。具体方法为:

//将NSData类型对象data写入文件,文件名为FileName
[data writeToFile:FileName atomically:YES];
//从FileName中读取出数据
NSData *data=[NSData dataWithContentsOfFile:FileName options:0 error:NULL];

SQLite:采用SQLite数据库来存储数据。SQLite作为⼀一中小型数据库,应用ios中跟其他三种保存方式相比,相对复杂一些

//打开数据库
if (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) {
NSLog(@"sqlite dadabase is opened.");
} else { return;}//打开不成功就返回

//在打开了数据库的前提下,如果数据库没有表,那就开始建表了哦!
char *error;
const char *createSql="create table(id integer primary key autoincrement, name text)"; if (sqlite3_exec(database, createSql, NULL, NULL, &error)==SQLITE_OK) {
NSLog(@"create table is ok.");
} else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下⼀一次使用
}

// 建表完成之后, 插入记录
const char *insertSql="insert into a person (name) values(‘gg’)";
if (sqlite3_exec(database, insertSql, NULL, NULL, &error)==SQLITE_OK) {
NSLog(@"insert operation is ok.");
} else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下一次使用
}

上面提到的磁盘存储特性,具备空间大、可持久、但是读取慢,面对大量数据频繁读取时更加明显,以往测试中磁盘读取比内存读取保守测量低于几十倍,那我们怎么解决磁盘读取慢的缺点呢? 又如何利用内存的优势呢?

二、 如何优化缓存(YYCache设计思想)

YYCache背景知识:
源码中由两个主要类构成


YYMemoryCache (内存缓存)
操作YYLinkedMap中数据, 为实现内存优化,采用双向链表数据结构实现 LRU算法,YYLinkedMapItem 为每个子节点
YYDiskCache (磁盘缓存)
不会直接操作缓存对象(sqlite/file),而是通过 YYKVStorage 来间接的操作缓存对象。
容量管理:

ageLimit :时间周期限制,比如每天或每星期开始清理
costLimit: 容量限制,比如超出10M后开始清理内存
countLimit : 数量限制, 比如超出1000个数据就清理
这里借用YYCache设计, 来讲述缓存优化

1. 磁盘+内存组合优化
利用内存和磁盘特性,融合各自优点,整合如下:


APP会优先请求内存缓冲中的资源
如果内存缓冲中有,则直接返回资源文件, 如果没有的话,则会请求资源文件,这时资源文件默认资源为本地磁盘存储,需要操作文件系统或数据库来获取。
获取到的资源文件,先缓存到内存缓存,方便以后不再重复获取,节省时间。
然后就是从缓存中取到数据然后给app使用。
这样就充分结合两者特性,利用内存读取快特性减少读取数据时间,

YYCache 源码解析:

- (id<NSCoding>)objectForKey:(NSString *)key {
// 1.如果内存缓存中存在则返回数据
id<NSCoding> object = [_memoryCache objectForKey:key];
if (!object) {
// 2.若不存在则查取磁盘缓存数据
object = [_diskCache objectForKey:key];
if (object) {
// 3.并将数据保存到内存中
[_memoryCache setObject:object forKey:key];
}
}
return object;
}

2. 内存优化-- 提高内存命中率

但是我们想在基础上再做优化,比如想让经常访问的数据保留在内存中,提高内存的命中率,减少磁盘的读取,那怎么做处理呢? -- LRU算法


LRU算法:我们可以将链表看成一串数据链,每个数据是这个串上的一个节点,经常访问的数据移动到头部,等数据超出容量后从链表后面的一些节点销毁,这样经常访问数据在头部位置,还保留在内存中。

链表实现结构图:


YYCache 源码解析

/**
A node in linked map.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
/**
A linked map used by YYMemoryCache.
It's not thread-safe and does not validate the parameters.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}

/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

@end

_YYLinkedMapNode *_prev 为该节点的头指针,指向前一个节点
_YYLinkedMapNode *_next为该节点的尾指针,指向下一个节点
头指针和尾指针将一个个子节点串连起来,形成双向链表

来看下bringNodeToHead:的源码实现,它是实现LRU算法主要方法,移动node子结点到链头。

(详细已注释在代码中)

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
if (_head == node) return; // 如果当前节点是链头,则不需要移动

// 链表中存了两个指向链头(_head)和链尾(_tail)的指针,便于链表访问
if (_tail == node) {
_tail = node->_prev; // 若当前节点为链尾,则更新链尾指针
_tail->_next = nil; // 链尾的尾节点这里设置为nil
} else {
// 比如:A B C 链表, 将 B拿走,将A C重新联系起来
node->_next->_prev = node->_prev; // 将node的下一个节点的头指针指向node的上一个节点,
node->_prev->_next = node->_next; // 将node的上一个节点的尾指针指向node的下一个节点
}
node->_next = _head; // 将当前node节点的尾指针指向之前的链头,因为此时node为最新的第一个节点
node->_prev = nil; // 链头的头节点这里设置为nil
_head->_prev = node; // 之前的_head将为第二个节点
_head = node; // 当前node成为新的_head
}

其他方法就不挨个举例了,具体可翻看源码,这些代码结构清晰,类和函数遵循单一职责,接口高内聚,低耦合,是个不错的学习示例!

3. 磁盘优化 - 数据分类存储

YYDiskCache 是一个线程安全的磁盘缓存,基于 sqlite 和 file 来做的磁盘缓存,我们的缓存对象可以自由的选择存储类型,
下面简单对比一下:

sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。
file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。
所以 YYDiskCache 使用两者配合,灵活的存储以提高性能。

另外:
YYDiskCache 具有以下功能:

它使用 LRU(least-recently-used) 来删除对象。
支持按 cost,count 和 age 进行控制。
它可以被配置为当没有可用的磁盘空间时自动驱逐缓存对象。
它可以自动抉择每个缓存对象的存储类型(sqlite/file)以便提供更好的性能表现。
YYCache源码解析

// YYKVStorageItem 是 YYKVStorage 中用来存储键值对和元数据的类
// 通常情况下,我们不应该直接使用这个类
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; ///< key
@property (nonatomic, strong) NSData *value; ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size; ///< value's size in bytes
@property (nonatomic) int modTime; ///< modification unix timestamp
@property (nonatomic) int accessTime; ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end


/**
YYKVStorage 是基于 sqlite 和文件系统的键值存储。
通常情况下,我们不应该直接使用这个类。

@warning
这个类的实例是 *非* 线程安全的,你需要确保
只有一个线程可以同时访问该实例。如果你真的
需要在多线程中处理大量的数据,应该分割数据
到多个 KVStorage 实例(分片)。
*/
@interface YYKVStorage : NSObject

#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path; /// storage 路径
@property (nonatomic, readonly) YYKVStorageType type; /// storage 类型
@property (nonatomic) BOOL errorLogsEnabled; /// 是否开启错误日志

#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;

#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
...

#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
...

#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
...

#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;

@end

我们只需要看一下 YYKVStorageType 这个枚举,它决定着 YYKVStorage 的存储类型。

YYKVStorageType

/**
存储类型,指示“YYKVStorageItem.value”存储在哪里。

@discussion
通常,将数据写入 sqlite 比外部文件更快,但是
读取性能取决于数据大小。在测试环境 iPhone 6s 64G,
当数据较大(超过 20KB)时从外部文件读取数据比 sqlite 更快。
*/
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0, // value 以文件的形式存储于文件系统
YYKVStorageTypeSQLite = 1, // value 以二进制形式存储于 sqlite
YYKVStorageTypeMixed = 2, // value 将根据你的选择基于上面两种形式混合存储
};

总结:

这里说了YYCache几个主要设计优化之处,其实细节上也有很多不错的处理,比如:

线程安全
如果说 YYCache 这个类是一个纯逻辑层的缓存类(指 YYCache 的接口实现全部是调用其他类完成),那么 YYMemoryCache 与 YYDiskCache 还是做了一些事情的(并没有 YYCache 当甩手掌柜那么轻松),其中最显而易见的就是 YYMemoryCache 与 YYDiskCache 为 YYCache 保证了线程安全。
YYMemoryCache 使用了 pthread_mutex 线程锁来确保线程安全,而 YYDiskCache 则选择了更适合它的 dispatch_semaphore,上文已经给出了作者选择这些锁的原因。

性能

YYCache 中对于性能提升的实现细节:

异步释放缓存对象
锁的选择
使用 NSMapTable 单例管理的 YYDiskCache
YYKVStorage 中的 _dbStmtCache
甚至使用 CoreFoundation 来换取微乎其微的性能提升

3. 网络和缓存同步流程

结合网络层和缓存层,设计了一套接口缓存方式,比较灵活且速度得到提升; 比如首页界面可能由多个接口提供数据,没有采用整块存储而是将存储细分到每个接口中,有API接口控制,基本结构如下:

主要分为:

应用层 :显示数据
管理层: 管理网络层和缓存层,为应用层提供数据支持
网络层: 请求网络数据
缓存层: 缓存数据
层级图:


服务端每套数据对应一个version (或时间戳),若后台数据发生变更,则version发生变化,在返回客户端数据时并将version一并返回。
当客户端请求网络时,将本地上一次数据对应version上传。
服务端获取客户端传来得version后,与最新的version进行对比,若version不一致,则返回最新数据,若未发生变化,服务端不需要返回全部数据只需返回304(No Modify) 状态值
客户端接到服务端返回数据,若返回全部数据非304,客户端则将最新数据同步到本地缓存中;客户端若接到304状态值后,表示服务端数据和本地数据一致,直接从缓存中获取显示
这也是ETag的大致流程;详细可以查看 https://baike.baidu.com/item/ETag/4419019?fr=aladdin

源码示例

- (void)getDataWithPage:(NSNumber *)page pageSize:(NSNumber *)pageSize option:(DataSourceOption)option completion:(void (^)(HomePageListCardModel * _Nullable, NSError * _Nullable))completionBlock {
NSString *cacheKey = CacheKey(currentUser.userId, PlatIndexRecommendation);// 全局静态常量 (userid + apiName)
// 根据需求而定是否需要缓存方式,网络方式走304逻辑
switch (option) {
case DataSourceCache:
{
if ([_cache containsObjectForKey:cacheKey]) {
completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
} else {
completionBlock(nil, LJDError(400, @"缓存中不存在"));
}
}
break;
case DataSourceNetwork:
{
[NetWorkServer requestDataWithPage:page pageSize:pageSize completion:^(id _Nullable responseObject, NSError * _Nullable error) {
if (responseObject && !error) {
HomePageListCardModel *model = [HomePageListCardModel yy_modelWithJSON:responseObject];
if (model.errnonumber == 304) { //取缓存数据
completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
} else {
completionBlock(model, error);
[self->_cache setObject:model forKey:cacheKey]; //保存到缓存中
}
} else {
completionBlock(nil, error);
}
}];
}
break;

default:
break;
}
}

这样做好处:

对于不频繁更新数据的接口,节省了大量JSON数据转化时间
节约流量,节省加载时长
用户界面显示加快
总结:项目中并不一定完全这样做,有时候过渡设计也是一种浪费,多了解其他设计思路后,针对项目找到适合的才是最好的!

参考文献:
YYCache: https://github.com/ibireme/YYCache
YYCache 设计思路 :https://blog.ibireme.com/2015/10/26/yycache/

链接:https://www.jianshu.com/p/b592ee20f09a

收起阅读 »

iOS进阶:WebViewJavascriptBridge源码解读

WebViewJavascriptBridge GitHub地址jsBridge框架是解决客户端与网页交互的方法之一。最主要的实现思路是客户端在webivew的代理方法中拦截url,根据url的类型来做不同处理。接下去会以jsBridge提供demo中的为例,...
继续阅读 »

WebViewJavascriptBridge GitHub地址

jsBridge框架是解决客户端与网页交互的方法之一。最主要的实现思路是客户端在webivew的代理方法中拦截url,根据url的类型来做不同处理。接下去会以jsBridge提供demo中的为例,从使用的角度,一步步分析它是如何实现的。

注:在iOS8后,苹果推出了WKWebView。对于UIWebView和WKWebView,jsBridge都能实现客户端与网页交互,且实现的方式类似,因此本文会以UIWebView为例来分析。

本文会通过以下几点来介绍框架的实现:

框架结构
WebViewJavascriptBridge_JS
WebViewJavascriptBridge WKWebViewJavascriptBridge
WebViewJavascriptBridgeBase
网页通知客户端的实现
客户端通知网页的实现
js环境注入问题
总结

框架结构


WebViewJavascriptBridge_JS

WebViewJavascriptBridge_JS 简单的说就是网页的js环境,需要客户端在网页初始化的时候注入到网页中去。如果不注入就无法实现网页与客户端的交互。该类只有一个返回值为NSString 的方法:NSString * WebViewJavascriptBridge_js(); 。

至于究竟何时注入,如何注入,会在接下去的分析中写到。

WebViewJavascriptBridge WKWebViewJavascriptBridge

这两个类分别对应UIWebView和WKWebView。看名字就可以知道这两个类是交互的桥梁,不管是网页同时客户端还是客户端通知网页,都是通过这两个类来完成通知的。

WebViewJavascriptBridgeBase

WebViewJavascriptBridgeBase个人认为类似数据处理工具类。

该类中存着客户端注册的方法以及对应实现:@property (strong, nonatomic) NSMutableDictionary* messageHandlers;

也存着客户端通知网页后的回调实现:@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;

同时,该类还实现了之前提的网页js环境注入方法:-(void)injectJavascriptFile;

还有一些url类别判断方法,这里不一一举例了。

网页通知客户端的实现

要让客户端能够响应网页的通知,首先必须使用桥梁注册方法名和实现,然后存起来,等待网页的通知。

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];

客户端注册方法时,bridge做了些什么事情呢?其实bridge只是简单地将方法名和实现block分别作为键值存到了messageHandlers属性中。

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}

接下来,网页想要调用客户端的testObjcCallback方法了。网页上有一个按钮,点击后调用客户端方法,网页的js代码如下:

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}

这里网页调用的方法为bridge.callHandler,这里你可能会有疑问,为什么bridge对象哪来的,callHandler方法又是哪来的。关于这个,这边先简单的说一下:这个bridge其实就是我们之前提到的js环境提供的,callHandler方法也是环境中的代码实现的,如果没有js环境,网页就拿不到bridge,也就无法成功调起客户端的方法。这边可以简单的理解为这个环境就相当于是我们客户端的WebViewJavascriptBridge框架,客户端如果不导入,也就无法使用jsbridge。网页也是类似,如果不注入,就无法使用jsbridge。而区别就在于,客户端的这个框架是运行前导入的,而网页这个环境是由客户端加载到该网页时,动态注入的。

至于详细的注入,会在下文中分析说明。

js环境文件中,bridge.callHandler方法实现:

function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}


function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

由于本质上网页处理发送通知的思路和客户端的一致,而我们队客户端的oc代码更好理解,因此我打算将这段代码的分析跳过,等到分析客户端通知网页时,再仔细讲。这边只需要知道

1.字典中加了一个callbackId字段,这个字段是用来等客户端调用完方法后,网页能找到对应的实现的。同时网页将实现存到了它管理的字典中:responseCallbacks[callbackId] = responseCallback;

2.网页最终将字典压到了sendMessageQueue中,并调用了messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';

3.字典中的数据是:

{   
handlerName : "testObjcCallback",
data : {'foo': 'bar'},
callbackId : 'cb_'+(uniqueId++)+'_'+new Date().getTime()
}

这时,客户端的webview代码方法就能拦截到url:


正是网页调用的:https://__wvjb_queue_message__/。然后客户端是如果去判断url并做相应处理呢?下面为拦截的源码:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }

NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}

这时,由于传过来的是https://__wvjb_queue_message__/,会进[_base isQueueMessageURL:url]的判断中,然后做以下处理:

NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];

第一行代码为从网页的sendMessageQueue中获取到数据,还记得之前网页把调用的相关数据存到了sendMessageQueue中吗?这个时候,客户端又把它取出来了。然后第二行代码,客户端开始处理这个数据:

- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}

id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];

NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}

WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}

handler(message[@"data"], responseCallback);
}
}
}

这段代码有点多,核心思路是将获得的数据转换成字典,然后从客户端的messageHandlers中取出方法名对应的block,并调用:handler(message[@"data"], responseCallback);


这边还需要特别注意的是,callbackId问题。在这个例子中,是存在callbackId的,因为网页是有写调用完客户端后的回调的,所以这边做了处理,如果有callbackId的话,再创建一个responseCallback,等客户端调用完网页通知的方法后再调用。

还记得当初客户端注册方法时的代码吗:

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];

这边就将这个handler的block取出来,然后将message[@"data"]和responseCallback作为参数调用。调用完后又调用了responseCallback,将数据又发回网页去。这边具体的发送会在下文客户端通知网页分析中写到。这边这需要知道,如果存在callbackId,就会将callbackId和数据又发回网页。

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

以上就是网页通知客户端的大致实现。

客户端通知网页

其实客户端通知网页的大致思路是和上文类似的。在客户端调用之前,网页肯定是已经注册好了客户端要调用的方法,就如上文中,客户端也已经注册好了网页通知的方法一样。下面为网页注册的代码:

bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})

看看registerHandler方法如何实现:

function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}

恩,是不是和客户端的注册非常相似?

接下来再看看客户端是如何调用的:

- (void)callHandler:(id)sender {
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
}

callHandler方法实现:

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

sendData实现:

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];

if (data) {
message[@"data"] = data;
}

if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}

if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}

客户端将数据封装成一个字段,这时这个字典的值为:

{
callbackId = "objc_cb_1";
data = {
greetingFromObjC = "Hi there, JS!";
};
handlerName = testJavascriptHandler;
}

还是和网页的处理非常一致。下面看看客户端是如何通知网页的:

- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}

- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];

} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}

客户端将字段转成js字符串,然后注入到网页中实现通知。具体方法是调用了js环境中的_handleMessageFromObjC方法,参数为字典转换后的字符串。下面看看_handleMessageFromObjC方法的实现:

function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}

function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}

var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}

这边的处理其实和上文客户端处理message字典时没什么区别的。

这边要提一下的是这个responseId的判断逻辑,还记得网页通知客户端分析中,由于网页有实现通知完客户端后的代码,所以客户端将网页传递过来的callbackId作为responseId参数又传回去了:

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

这边网页的处理是,从responseCallbacks中根据这个"responseId":callbackId字段取出block并调用,代码如下:

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
}

如果看到这里有点乱了,可以再看看网页通知客户端时对于字典的处理部分。

以上就是客户端通知网页的大致实现。

js环境注入问题

上文一提到这个,就说下文讲解,现在终于可以分析这一块了。

其实这个比较简单,本质上就是网页调用了一个特殊的,jsbridge规定的url,使得客户端可以拦截到并分析出是需要注入js环境的通知。然后客户端开始注入。

网页部分的代码:

WVJBIframe.src = 'https://__bridge_loaded__';

一般这个是放在网页代码的最前面的。这样做可以让客户端在最早的情况下将环境注入到网页中。

而客户端是如何处理的呢?

if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
}

看到了吧,客户端调用WebViewJavascriptBridge_JS类的唯一的方法:NSString * WebViewJavascriptBridge_js(); ,然后通过_evaluateJavascript注入。

总结

以网页通知客户端为例:客户端会将要被调用的方法存到字典中,同时拦截网页的调用,当网页调用时,从字典中取出方法并调用。调用完后,判断网页是否有调用完的回调,如果有,再将回调的id和参数通过客户端调用网页的方式通知过去。这就完成了网页通知客户端的总体流程。

最后

这个框架是在去年就已经看完了,由于忙+懒,拖到今天才终于准备写一下。花了一下午的时间,将大体的逻辑理清楚并用文字的方式表达出来,但是由于昨晚没睡舒服,现在脑子还是有点乱,所以文章中应该有部分错别字,麻烦看到了指出一下方便我改正。还有一点,对于之前没接触过的同学,由于在调用时有responseId和callbackId,会比较乱,在此建议多看几遍。如果实在理解不了,可以评论或加我微信,我会尽我努力让你理解。最后,谢谢你的耐心阅读😆😆

链接:https://www.jianshu.com/p/7bd7260daf94

收起阅读 »

iOS组件化开发实践

目录:1.组件化需求来源2.组件化初识3.组件化必备的工具使用4.模块拆分5.组件工程兼容swift环境6.组件之间的通讯7.组件化后的资源加载8.OC工程底层换swift代码9.总结1. 组件化需求来源起初的这个项目,App只有一条产品线,代码逻辑相对比较清...
继续阅读 »

目录:

1.组件化需求来源
2.组件化初识
3.组件化必备的工具使用
4.模块拆分
5.组件工程兼容swift环境
6.组件之间的通讯
7.组件化后的资源加载
8.OC工程底层换swift代码
9.总结

1. 组件化需求来源

起初的这个项目,App只有一条产品线,代码逻辑相对比较清晰,后期随着公司业务的迅速发展,现在App里面承载了大概五六条产品线,每个产品线的流程有部分是一样的,也有部分是不一样的,这就需要做各种各样的判断及定制化需求。大概做了一年多后,出现了不同产品线提过来的需求,开发人员都需要在主工程中开发,但是开发人员开发的是不同的产品线,也得将整个工程跑起来,代码管理、并行开发效率、分支管理、上线时间明显有所限制。大概就在去年底,我们的领导提出了这个问题,希望作成组件化,将代码重构拆分成模块,在主工程中组装拆分的模块,形成一个完整的App。

2. 组件化初识

随着业务线的增多,业务的复杂度增加,App的代码逻辑复杂度也增加了,后期的开发维护成本也增加了,为什么这么说呢?业务逻辑没有分类,查找问题效率降低(针对新手),运行也好慢哦,真的好烦哦......我们要改变这种局面。而组件化开发,就是将一个臃肿,复杂的单一工程的项目, 根据功能或者属性进行分解,拆分成为各个独立的功能模块或者组件 ; 然后根据项目和业务的需求,按照某种方式, 任意组织成一个拥有完整业务逻辑的工程。

组件化开发的缺点:

1、代码耦合严重
2、依赖严重
3、其它app接入某条产品线难以集成
4、项目复杂、臃肿、庞大,编译时间过长
5、难以做集成测试
6、对开发人员,只能使用相同的开发模式
......
组件化开发的优点:

1、项目结构清晰
2、代码逻辑清晰
3、拆分粒度小
4、快速集成
5、能做单元测试
6、代码利用率高
7、迭代效率高
......
组件化的实质:就是对现有项目或新项目进行基础、功能及业务逻辑的拆分,形成一个个的组件库,使宿主工程能在拆分的组件库里面查找需要的功能,组装成一个完整的App。

3. 组件化必备的工具使用

组件的存在方式是以每个pod库的形式存在的。那么我们组合组件的方法就是通过利用CocoaPods的方式添加安装各个组件,我们就需要制作CocoaPods远程私有库,将其发不到公司的gitlab或GitHub,使工程能够Pod下载下来。

Git的基础命令:

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master

CocoaPods远程私有库制作:
1、Create Component Project

pod lib create ProjectName

2、Use Git

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master

3、Edit podspec file

vim CoreLib.podspec
Pod::Spec.new do |s|
s.name = '组件工程名'
s.version = '0.0.1'
s.summary = 'summary'

s.description = <<-DESC
description
DESC

s.homepage = '远程仓库地址'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '作者' => '作者' }
s.source = { :git => '远程仓库地址', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = 'Classes/**/*.{swift,h,m,c}'
s.resources = 'Assets/*'

s.dependency 'AFNetworking', '~> 2.3'
end

4、Create tag

//create local tag
git tag '0.0.1'

git tag 0.0.1

//local tag push to remote
git push --tags

git push origin 0.0.1

//delete local tag
git tag -d 0.0.1

//delete remote tag
git tag origin :0.0.1

5、Verify Component Project

pod lib lint --allow-warnings --no-clean

6、Push To CocoaPods

pod repo add CoreLib git@git.test/CoreLib.git
pod repo push CoreLib CoreLib.podspec --allow-warnings

4. 模块拆分


基础组件库:
基础组件库放一些最基础的工具类,比如金额格式化、手机号/shenfen证/邮箱的有效校验,实质就是不会依赖业务,不会和业务牵扯的文件。

功能组件库:
分享的封装、图片的轮播、跑马灯功能、推送功能的二次封装,即开发一次,以后都能快速集成的功能。

业务组件库:
登录组件、实名组件、消息组件、借款组件、还款组件、各条产品线组件等。

中间件(组件通讯):
各个业务组件拆分出来后,组件之间的通讯、传参、回调就要考虑了,此时就需要一个组件通讯的工具类来处理。

CocoaPods远程私有库:
每个拆分出去的组件存在的形式都是以Pod的形式存在的,并能达到单独运行成功。

宿主工程:
宿主工程就是一个壳,在组件库中寻找这个工程所需要的组件,然后拿过来组装成一个App。

5. 组件工程兼容swift环境

在做组件化之前,这个项目使用的是Objective-C语言写的,还没有支持在项目里面使用Swift语言的能力,考虑到后期肯定会往Swift语言切过去的,于是借着这次重构的机会,创建的组件工程都是swift工程。

Podfile文件需要添加==use_frameworks!==

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
inhibit_all_warnings!
use_frameworks!

target 'CoreLib_Example' do
pod 'CoreLib', :path => '../'
end

这里其实有个大坑需要特别注意,在支持Swift环境后,部分Objective-C语言的三方库采用的是==静态库==,在OC文件中引用三方库头文件,会一直报头文件找不到,我们在遇到这个问题时找遍了百度,都没找到解决方案,整整花了一个星期的时间尝试。

解决方案:我们对这些三方库(主要有:UMengAnalytics、Bugly、AMapLocation-NO-IDFA)再包一层,使用CocoaPods远程私有库管理,对外暴露我们写的文件,引用我们写的头文件,就能调用到。

Pod::Spec.new do |s|
s.name = ''
s.version = '0.0.1'
s.summary = '包装高德地图、分享、友盟Framework.'

s.description = <<-DESC
DESC

s.homepage = ''
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '' => '' }
s.source = { :git => '', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = ['Classes/UMMob/**/*.{h,m}','Classes/Bugly/**/*.{h,m}','Classes/AMap/**/*.{h,m}']
s.public_header_files = ['Classes/*.h']
s.libraries = 'sqlite3', 'c++', 'z', 'z.1.1.3', 'stdc++', 'stdc++.6.0.9'
s.frameworks = 'SystemConfiguration', 'CoreTelephony', 'JavaScriptcore', 'CoreLocation', 'Security', 'Foundation'
s.vendored_frameworks = 'Frameworks/**/*.framework'
s.xcconfig = { "FRAMEWORK_SEARCH_PATHS" => "Pods/WDContainerLib/Frameworks" }

s.requires_arc = true
end

6. 组件之间的通讯

在将业务控制器拆分出去后,如果一个组件要调用另一个组件里面的控制器,平常的做法是直接==#import "控制器头文件"==,现在在不同的组件里面是无法import的,那该怎么做呢?答案就是使用==消息发送机制==。

思路:

1.每个业务组件库里面会有一个控制器的配置文件(路由配置文件),标记着每个控制器的key;
2.在App每次启动时,组件通讯的工具类里面需要解析控制器配置文件(路由配置文件),将其加载进内存;
3.在内存中查询路由配置,找到具体的控制器并动态生成类,然后使用==消息发送机制==进行调用函数、传参数、回调,都能做到。

((id (*)(id, SEL, NSDictionary *)) objc_msgSend)((id) cls, @selector(load:), param);
((void(*)(id, SEL,NSDictionary*))objc_msgSend)((id) vc, @selector(callBack:), param);

Or

[vc performSelector:@selector(load:) withObject:param];
[vc performSelector:@selector(callBack:) withObject:param];

好处:

解除了控制器之间的依赖;
使用iOS的消息发送机制进行传参数、回调参数、透传参数;
路由表配置文件,能实现界面动态配置、动态生成界面;
路由表配置文件放到服务端,还可以实现线上App的跳转逻辑;
将控制器的key提供给H5,还可以实现H5跳转到Native界面;

7. 组件化后的资源加载

新项目已开始就采用组件化开发,还是特别容易的,如果是老项目重构成组件化,那就比较悲剧了,OC项目重构后,app包里面会有一个==Frameworks==文件夹,所有的组件都在这个文件夹下,并且以==.framework==(比如:WDComponentLogin.framework)结尾。在工程中使用的==xib、图片==,使用正常的方式加载,是加载不到的,原因就是xib、图片的路径==(工程.app/Frameworks/WDComponentLogin.framework/LoginViewController.nib、工程.app/Frameworks/WDComponentLogin.framework/login.png)==发生了变化。

以下是在组件库中加载nib文件/图片文件的所有情况:

/**
从主工程mainBundle或从所有的组件(组件名.framework)中加载图片

@param imageName 图片名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName;

/**
从指定的组件中加载图片,主要用于从当前组件加载其他组件中的图片

@param imageName 图片名称
@param frameworkName 组件名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName frameworkName:(NSString *_Nonnull)frameworkName;

/**
从指定的组件的Bundle文件夹中加载图片,主要用于从当前组件加载其他组件Bundle文件夹中的图片

@param imageName 图片名称
@param bundleName Bundle文件夹名
@param frameworkName 组件名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName frameworkName:(NSString *_Nonnull)frameworkName;

/**
从主工程mainBundle的指定Bundle文件夹中去加载图片

@param imageName 图片名称
@param bundleName Bundle文件夹名
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName;

/**
从指定的组件(组件名.framework)中加载图片
说明:加载组件中的图片,必须指明图片的全名和图片所在bundle的包名

@param imageName 图片名称
@param targetClass 当前类
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName targetClass:(Class _Nonnull)targetClass;

/**
从指定的组件(组件名.framework)中的Bundle文件夹中加载图片
说明:加载组件中的图片,必须指明图片的全名和图片所在bundle的包名

@param imageName 图片名称
@param bundleName Bundle文件夹名
@param targetClass 当前类
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName targetClass:(Class _Nonnull)targetClass;

/**
加载工程中的nib文件
eg:[_tableview registerNib:[WDLoadResourcesUtil loadNibClass:[WDRepaymentheaderView class]] forHeaderFooterViewReuseIdentifier:kWDRepaymentheaderView]
@param class nib文件名
@return 返回所需要的nib对象
*/
+ (UINib *_Nullable)loadNibClass:(NSObject *_Nonnull)targetClass;

控制器加载方式:

@implementation WDBaseViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
NSString *classString = [[NSStringFromClass(self.class) componentsSeparatedByString:@"."] lastObject];
if ([[NSBundle bundleForClass:[self class]] pathForResource:classString ofType:@"nib"] != nil) {
//有xib
return [super initWithNibName:classString bundle:[NSBundle bundleForClass:[self class]]];
}else if ([[NSBundle mainBundle] pathForResource:classString ofType:@"nib"] == nil) {
//没有xib
return [super initWithNibName:nil bundle:nibBundleOrNil];
} else {
return [super initWithNibName:(nibNameOrNil == nil ? classString : nibNameOrNil) bundle:nibBundleOrNil];
}
}
@end

UIView视图加载方式:

OC版本

+ (id)loadFromNIB {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSBundle bundleForClass:[self class]].bundlePath]) {
return [[[NSBundle bundleForClass:[self class]] loadNibNamed:[self description]
owner:self
options:nil] lastObject];
}else{
return [[[NSBundle mainBundle] loadNibNamed:[self description] owner:self options:nil] lastObject];
}

}

+ (id)loadFromNIB:(NSInteger)index {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSBundle bundleForClass:[self class]].bundlePath]) {
return [[NSBundle bundleForClass:[self class]] loadNibNamed:[self description]
owner:self
options:nil][index];
}else{
return [[NSBundle mainBundle] loadNibNamed:[self description] owner:self options:nil][index];
}

}

Swift版本

// MARK: - 通过nib加载视图
@objc public static func loadFromNIB() -> UIView! {
return (Bundle(for: self.classForCoder()).loadNibNamed(self.description().components(separatedBy: ".")[1], owner: self, options: nil)?.first as? UIView)!
}

8. OC工程底层换swift代码

目前正在做OC底层的统一,换成swift写的代码。

1、控制器Base、Web控制器Base使用OC代码,因为OC控制器不能继承Swift,而Swift控制器可以继承OC写的控制器。
2、导航栏、工具栏、路由、基础组件、功能组件、混合开发插件都是用Swift语言。
3、Swift移动组件大部分完成,OC工程、Swift工程都统一使用开发的移动组件库。

9. 总结

经过半年的努力重构,终于将工程拆分成组件化开发了,也从中学到了很多,希望自己能再接再厉和同事一起进步。

链接:https://www.jianshu.com/p/196ec57cdc75

收起阅读 »

iOS 应用分享平台fir使用遇到的一些坑

前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分...
继续阅读 »

前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分享出来给大家,希望能对你有所帮助。

首先要确定你们使用平台的需求,我这里有蒲公英(fir同类型网站)对于应用分享需求的介绍


如果只是小范围的几个人来安装,使用Ad-hoc方式,去一个个添加UDID就好了,好处是使用你自己的免费证书也可以申请。
如果是想做线下推广,没办法及时获取添加目标UDID的话,最好还是要使用In-house方式,不过装机数量苹果好像还是有一定限制,这个具体政策不太清楚。

我的目的是给测试团队装机,所以选择Ad-hoc方式做。

简化下来一共需要三大步
1 . 在你的Apple Developer 页面的Devices中添加目标的(于我就是“测试团队”)苹果手机UDID。(关于UDID的获取看这里 http://fir.im/udid 这个网址使用苹果手机的Safari浏览器访问)


在这里点击“+”输入用户的UDID(name是你自己定的,建议起个和此UDID手机拥有者相关的名字,后面会用到),点击下方的注册,会跳转确认注册页面


确认账号无误后可以点击下方的确定,目标UDID就乖乖加入到你的Devices列表中了😊。

注意:这里就会有一个坑,我导入的第一个UDID出现这种情况


你会发现这个缺少了Model:这一项,目前我没有发现是因为什么(隐约赶脚是因为录入这个UDID时,网络或者苹果官网之类的问题😊)。这种账号是无法添加进描述文件的,添加进去也无法识别和使用。

还有一种情况是你添加了目标UDID,在Devices列表中找不到,再次注册该UDID又会提示它不是有效的,多次尝试无果也只好作罢。

2 . 在Distribution中添加一个用于测试的描述文件,并在此步骤中添加目标手机到描述文件中。


在此点击“+”,添加一个新的描述文件。


选择你需要的方式,我的是Ad-hoc


然后是选择自己项目


选择开发者(或团队)


选择你要添加的目标UDID(此时使用的是你创建Device时的名字)


给你的描述文件命名(项目中添加Provisioning Profile时使用这个名字)

creat之后点击下载,描述文件就会下载到电脑。

这里倒是没有什么坑,就是苹果官网如果访问起来困难,部分页面会不显示你已有的一些资料,会提示要你新建一个项目。如果你确定自己有项目的话,刷新一下就好了。

3 . 将描述文件添加到Xcode,然后在项目中选择相应的打包选项,生成.ipa文件。然后大功告成,将其上传到fir平台后点击“预览”会自动生成一个带有二维码的网址。(需要使用iphone自带的safari浏览器访问该链接)

现在可以关掉万恶的苹果官网,来到桌面上,建议先彻底关闭Xcode,然后双击一下你下载下来的描述文件,Xcode会自动打开,此时描述文件就已经添加好了。


在 Xcode 中点击project图标,在info这个tab下找到configuration设置,里面默认的是debug和release。点击+,选择Duplicate the “Release configuration”,给生成的新东西起个名字,推荐使用ad hoc distribution


点击targets图标,在build settings这个tab下,找到code signing部分。将Code Signing Identity中的ad hoc distribution证书设置为刚刚导入到 Xcode 中对应测试应用的证书。注意不要改动Debug和Release中的证书。
在下方的Provisioning Profile中选择你下载下来的描述文件。
保证target中info这个tab下的bundle indentifier里面有预设值,其必须和provision portal输入匹配。这个很重要,否则将来会出错。


在Xcode左上角run按钮右侧有一个下拉菜单,选择device或者simulator,点击菜单下方的edit schema。保证Archive中Build Configuration中的值是ad hoc distribution


配置工作到此结束。点击Product中的Archive,程序开始编译,编译完成后弹出设置框,点选"Export" 然后选"Save for Ad Hoc Develoyment"

按操作提示就会生成一个.ipa文件。此.ipa可以被安装到之前设置的测试应用设备中。

然后创建一个fir账号,在其上发布就好了。

本文借鉴于http://blog.csdn.net/yuanbohx/article/details/9213879
该博客6楼指出其在文章中的错误,实测6楼所说是正确的。

链接:https://www.jianshu.com/p/cbda1e434add

收起阅读 »

超强的游戏模拟器, 做游戏开发必备 - OpenEmu

OpenEmuOpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 S...
继续阅读 »

OpenEmu

alt text


OpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 Sparkle,它用于自动更新。OpenEmu 使用模块化架构,允许使用游戏引擎插件,允许 OpenEmu 支持大量不同的仿真引擎和后端,同时保留熟悉的 macOS 原生前端。

目前 OpenEmu 可以加载以下游戏引擎作为插件:



最低要求

macOS 10.14


demo及常见问题:https://github.com/OpenEmu/OpenEmu

源码下载:OpenEmu-master.zip





收起阅读 »

iOS-单元测试汇总

前言:对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。我看...
继续阅读 »

前言:
对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。

我看了几篇单元测试的文章,其中写到单元测试多数用于:

1.调试接口是否正常使用。比如要测试一个网络接口,通常每次都要重新启动,经过繁复的操作之后,才能测试到网络接口。要是用单元测试,就可以直接测试那个方法,相对方便很多。

2.比如由于修改较多,想测试分享功能是否正常,(而不是重新启动程序,进入到分享界面,点击分享,填写分享内容。),在单元测试通过了,直接用到相应的地方。

3.自动发布、自动测试(特别在一些大的项目,以防止程序被误改或引起新的问题)。
4.用户注册/登陆等

了解一下单元测试:
单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

通常来说,程序员每修改一次代码就会修改某个单元,那我们就可以对这个单元做修改的验证(单元测试),在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(产品需求)要求的工作目标,而且没有程序错误。

每个理想的测试案例独立于其它case,测试时需隔离模块。单元测试通常由软件开发人员编写,用于确保所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是手动的,或是构建自动化的一部分。

单元测试允许程序员在未来重构代码,且确保模块依然工作正确。这个过程是为所有方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。

可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以总是维持准确性。

了解一下单元测试目的:
保证代码的质量 (帮助你编写高质量代码、减少bu)
代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。

有一部分bug的原因是开发人员在编写工作代码的时候没有考虑到某些case或者边际条件。造成这种问题的原因很多,其中很重要的一个原因是我们对工作代码所要完成的功能思考不足,而编写单元测试,特别是先写单元测试再写工作代码就可以帮助开发人员思考编写的代码到底要实现哪些功能。例如实现一个简单的用户注册功能的业务类方法,用单元测试再写工作代码的方式来工作的话开发人员就会先考虑各种场景相关,例如正常注册、用户名重复、没有满足必要的填写内容......等等,之后就会编写相关的测试用例。编写单元测试代码的过程就是促使开发人员思考工作代码实现内容和逻辑的过程,之后实现工作代码的时候,开发人员思路会更清晰,实现代码的质量也会有相应的提升。

保证代码的可维护性 (提升代码的反馈速度,减少重复工作,保证你最后的代码修改不会破坏之前代码的功能)
保证原有单元测试正确的情况下,无论如何修改单元内部代码,测试的结果应该是正确的,且修改后不会影响到其他的模块。

开发人员实现某个功能或者修补了某个bug,如果有相应的单元测试支持的话,开发人员可以马上通过运行单元测试来验证之前完成的代码是否正确,而不需要反复通过编译运行simulator、等待应用启动、通过输入数据等繁琐的步骤来验证所完成的功能。用单元测试代码来验证代码和通过发布应用以人工的方式来验证代码这两者的效率差很多,所以单元测试其实还能节约人力成本。

项目越做越大,代码越来越多,特别涉及到一些公用接口之类的代码或是底层的基础库,谁也不敢保证这次修改的代码不会破坏之前的功能,所以与此相关的需求会被搁置或推迟,由于不敢改进代码,代码也变得越来越难以维护,质量也越来越差。而单元测试就是解决这种问题的很好方法(不敢说最好的)。由于代码的历史功能都有相应的单元测试保证,修改了某些代码以后,通过运行相关的单元测试就可以验证出新调整的功能是否有影响到之前的功能。当然要实现到这种程度需要很大的付出,不但要能够达到比较高的测试覆盖率,而且单元测试代码的编写质量也要有保证。

保证代码的可扩展性
为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行,说明代码的依赖性很高。

了解一下单元测试的本质:
是一种验证行为
单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。

是一种设计行为
为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。

是一种快速回归的方式
在原代码基础上开发及修改功能时,单元测试是一种快捷,可靠的回归。

除了那些大拿们编写的代码,我相信很多易于维护、设计良好的代码都是通过不断的重构才得到的。虽然说单元测试本身不能直接改进生产代码的质量,但它为生产代码提供了“安全网”,让开发人员可以勇敢地改进代码,从而让代码的clean和beautiful不再是梦想。

是程序优良的文档
从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

由于给代码写很多单元测试,相当于给代码加上了规格说明书,开发人员通过读单元测试代码也能够帮助开发人员理解现有代码。很有Open Source的项目(如,AFNetworking, FMDB,喵神的VVDoucment等)都有相当量的单元测试代码,通过读这些测试代码会有助于理解生产源代码。

两种测试思想
  测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。

行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。

在iOS单元测试框架中,kiwi是BDD的代表。

介绍
OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架。

GHUnit是一个可视化的测试框架。
有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。

OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。

UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。

案例 1

简单的单元测试
1-1 创建一个新的项目


1-2点开测试文件,进入到这个类

setUp       :每个测试方法调用前执行
tearDown :每个测试方法调用后执行
testExample :是测试方法,和我们新建的没有差别。
测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法
测试方法的执行顺序: 字典序排序。
快捷键:Command + U进行单元测试,这个快捷键是全部测试。


1-3在testExample方法中输入如下:

NSLog(@"自定义测试testExample");
int a= 3;
XCTAssertTrue(a == 0,"a 不能等于 0");


备注:红色的叉子:代表测试未通过。绿色叉子:代表测试通过。

案例 2

iOS-Main - 单元测试 &基本体验

案例 3
进行网络请求的测试
使用CocoaPods安装AFNetworking和STAlertView(CocoaPods安装和使用教程 )
Pofile:

platform :ios, '7.0'
target 'UnitTestDemoTests' do
pod 'AFNetworking', '~> 2.5.0'
pod 'STAlertView', '~> 1.0.0'
end
target 'UnitTestDemoTestsTests' do
pod 'AFNetworking', '~> 2.5.0'
pod 'STAlertView', '~> 1.0.0'
end

iOS9的http安全问题:现在进行异步请求的网络测试,由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。
也可以在方法结束前设置等待,调回回来的时候再让它继续执行。(另一种异步函数的单元测试)定义宏如下:

//waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。
#define WAIT do {\
[self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\
[self waitForExpectationsWithTimeout:30 handler:nil];\
} while (0);

#define NOTIFY \
[[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];

增加测试方法:

-(void)testRequest{
// 1.获得请求管理者
AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];

// 2.发送GET请求
[mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"responseObject:%@",responseObject);
XCTAssertNotNil(responseObject, @"返回出错");
NOTIFY //继续执行
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error:%@",error);
XCTAssertNil(error, @"请求出错");
NOTIFY //继续执行
}];
WAIT //暂停
}

有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框STAlertView,前面已经设置。
STAlertView的使用方法:

- (void)testAlertView
{

self.stAlertView = [[STAlertView alloc]initWithTitle:@"验证码" message:nil textFieldHint:@"请输入手机验证码" textFieldValue:nil cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancelButtonBlock:^{
//点击取消返回后执行
[self testAlertViewCancel];
NOTIFY //继续执行
} otherButtonBlock:^(NSString *b) {
//点击确定后执行
[self alertViewComfirm:b];
NOTIFY //继续执行
}];

[self.stAlertView show];
WAIT //设置等待时间
}

案例 4
测试的执行顺序


通过上述测试得出结论:
可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。
目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,小者优先,例如testA,testB1,testB2三个方法相继执行。
案例 5
Xcode集成了对单元测试的支持,XCode4.x集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,XCode7.x时代XCtest还可以进行UI测试。下面我们简单介绍下XCTest的使用。

在xcode新建项目中,默认会建一个单元测试的target,并建立一个继承于XCTestCase的测试用例类


 本例实现了一个个税计算方法,在测试用例中测试输入后输出是否符合结果。
创建一个名为ASRevenueBL的 .h .m文件,如下面所示:


ASRevenueBL.h

#import <Foundation/Foundation.h>
@interface ASRevenueBL : NSObject
- (double)calculate:(double)revenue;
@end

ASRevenueBL.m

import "ASRevenueBL.h"

#define baseNum 3500.0 // 起征点

@implementation ASRevenueBL

/*
* method:传入收入计算税值
* revenue:收入
*/
- (double)calculate:(double)revenue
{
double tax = 0.0; // 税
// 应纳税所得额 = 工资收入金额 - 各项社会保险费 - 起征点(3500元)
// 应纳税额 = 应纳税所得额 x 税率 - 速算扣除数
double dbTaxRevenue = revenue - baseNum;
if(dbTaxRevenue <= 1500){
tax = dbTaxRevenue * 0.03;
} else if (dbTaxRevenue > 1500 && dbTaxRevenue <= 4500){
tax = dbTaxRevenue *0.1 -105;
} else if (dbTaxRevenue > 4500 && dbTaxRevenue <= 9000){
tax = dbTaxRevenue * 0.2 - 555;
}else if (dbTaxRevenue > 9000 && dbTaxRevenue <= 35000) {
tax = dbTaxRevenue * 0.25 - 1005;
} else if (dbTaxRevenue > 35000 && dbTaxRevenue <= 55000) {
tax = dbTaxRevenue * 0.3 - 2755;
} else if (dbTaxRevenue > 55000 && dbTaxRevenue <= 80000) {
tax = dbTaxRevenue * 0.35 - 5505;
} else if (dbTaxRevenue > 80000) {
tax = dbTaxRevenue * 0.45 - 13505;
}
return tax;
}

导入测试方法所在的类的头文件,并创建一个类,在测试方法调用前,初始化类对象,测试完毕后,将对象置nil,其方法测试如下方测试代码:

#import <XCTest/XCTest.h>
#import "ASRevenueBL.h"

@interface UnitTestsTwoTests : XCTestCase
@property (nonatomic, strong) ASRevenueBL *revenueBL;
@end

@implementation UnitTestsTwoTests

- (void)setUp {
[super setUp];

self.revenueBL = [[ASRevenueBL alloc] init];
}

- (void)tearDown {
self.revenueBL = nil;
[super tearDown];
}

- (void)testLevel1
{
double revenue = 5000;
double tax = [self.revenueBL calculate:revenue];
XCTAssertEqual(tax, 45.0,@"测试案例1失败");
XCTAssertTrue(tax == 45.0);
}

- (void)testLevel2 {
XCTestExpectation *exp = [self expectationWithDescription:@"超时"];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperationWithBlock:^{
double revenue = 1500;
double tax = [self.revenueBL calculate:revenue];
sleep(1);
NSLog(@"%f",tax);
XCTAssertEqual(tax, 45, @"用例2测试失败");
[exp fulfill]; // exp结束
}];

[self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}


- (void)testExample {

}

- (void)testPerformanceExample {

[self measureBlock:^{
for (int a = 0; a<10; a+=a) {
NSLog(@"%zd", a);
}
}];

}
@end


testLevel1通过revenueBL计算出来的tax与预期相同,测试通过;testLevel2通过revenueBL计算出来的tax与预期不同,测试不通过,反映出了程序一些逻辑漏洞;testPerformanceExample中的平均执行时间比基准值低,测试通过。

案例 6 命令行测试
在命令行中也可以启动测试,便于持续集成。

Assuner$ cd Desktop/
Desktop Assuner$ cd ASUnitTestFirstDemo/
ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
// 可以有多个destination

结果

Test Suite 'All tests' started at 2017-09-11 11:12:16.348
Test Suite 'ASUnitTestFirstDemoTests.xctest' started at 2017-09-11 11:12:16.349
Test Suite 'ASUnitTestFirstDemoTests' started at 2017-09-11 11:12:16.349
Test Case '-[ASUnitTestFirstDemoTests testLevel1]' started.
Test Case '-[ASUnitTestFirstDemoTests testLevel1]' passed (0.001 seconds).
Test Case '-[ASUnitTestFirstDemoTests testLevel2]' started.
/Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46: error: -[ASUnitTestFirstDemoTests testLevel2] : ((tax) equal to (45.0)) failed: ("-60") is not equal to ("45") - 用例2测试失败
Test Case '-[ASUnitTestFirstDemoTests testLevel2]' failed (1.007 seconds).
Test Suite 'ASUnitTestFirstDemoTests' failed at 2017-09-11 11:12:17.358.
Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.009) seconds
Test Suite 'ASUnitTestFirstDemoTests.xctest' failed at 2017-09-11 11:12:17.359.
Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.010) seconds
Test Suite 'All tests' failed at 2017-09-11 11:12:17.360.
Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.012) seconds
Failing tests:
-[ASUnitTestFirstDemoTests testLevel2]
** TEST FAILED **

如果是workspace

xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test

每个test方法都会跑一遍,并给出结果描述。

案例 7 代码的执行时间测试-(性能测试)
性能测试主要使用 measureBlock 方法 ,用于测试一组方法的执行时间,通过设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。


假如直接执行方法,因为block中没有内容,所以方法的执行时间为0.0s,如果我们把baseline设成0.05,偏差10%,是可以通过的测试的。但是如果设置如果我们把baseline为1,偏差10%,那测试会失败,因为不满足条件。
如上图所示,这个方法是用来测试block内代码的执行时间的,我们可以通过打印很清楚的看到它其实执行了10次,用处也很宽广,比如想测试shenfenzheng的识别时间,请求的时间,转模型的速度等等都可以通过它来测试,这里只是举个简单的例子.

我们可以看下打印发现他确实是执行了十次.


再来看看左边的执行代码相关信息,这里由于打印"1"执行的太快无法看出效果,所以我将测试内容换成了使用for循环打印1-9999,看看他们的执行时间.


可以很清楚的看到,10次的平均时间是1.382秒,第一次时间是1.85秒,并且可以看到第一次执行时间超过了平均时间33%,这里的测试结果都是和机器性能有关系的.

案例 8 登陆模块测试


案例 9 加法测试

- (void)testExample {
//设置变量和设置预期值
NSUInteger a = 10;NSUInteger b = 15;
NSUInteger expected = 24;
//执行方法得到实际值
NSUInteger actual = [self add:a b:b];
//断言判定实际值和预期是否符合
XCTAssertEqual(expected, actual,@"add方法错误!");
}

-(NSUInteger)add:(NSUInteger)a b:(NSUInteger)b{
return a+b;
}

从这也能看出一个测试用例比较规范的写法,1:定义变量和预期,2:执行方法得到实际值,3:断言

案例 10 代码来自于AFNetworking,用于测试backgroundImageForState方法

- (void)testThatBackgroundImageChanges {
XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
return [button backgroundImageForState:UIControlStateNormal] != nil;
}];

[self expectationForPredicate:predicate
evaluatedWithObject:self.button
handler:nil];
[self waitForExpectationsWithTimeout:20 handler:nil];
}

利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。

expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。

- (void)testAsynExample1 {
[self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
[[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];

//设置延迟多少秒后,如果没有满足测试条件就报错
[self waitForExpectationsWithTimeout:3 handler:nil];
}

这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解 expectationForNotification 方法和 expectationWithDescription的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。

func testAsynExample1() {
let expectation = expectationWithDescription("监听通知的名称xxx")
let sub = NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx", object: nil, queue: nil) { (not) -> Void in
expectation.fulfill()
}

NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx", object: nil)
waitForExpectationsWithTimeout(1, handler: nil)
NSNotificationCenter.defaultCenter().removeObserver(sub)
}

XCTest常见的断言

XCTFail(format...)  生成一个失败的测试
XCTAssertNil(a1,format...)为空判断, a1为空时通过,反之不通过;
XCTAssertNotNil(a1,format...) 不为空判断,a1不为空时通过,反之不通过;
XCTAssert(expression,format...) 当expression求值为true时通过;
XCTAssertTrue(expression,format...) 当expression求值为true时通过;
XCTAssertFalse(expression,format...) 当expression求值为False时通过;
XCTAssertEqualObjects(a1, a2,format...) 判断相等 [a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertNotEqualObjects(a1, a2,format...) 判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

特别注意下XCTAssertEqualObjects和XCTAssertEqual。 XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES

备注:

1.关于私有方法的测试,只能通过扩展来实现

2.关于case的方法名字,一定要以test开头并注意驼峰命名法,且不能加入参数。

3.单元测试类继承自XCTestCase,他有一些重要的方法,其中最重要的有3个,setUp ,tearDown,measureBlock.

4.md + 5切换到测试选项卡后会看到很多小箭头,点击可以单独或整体测试.

5.cmd + U运行整个单元测试

6.使用pod的项目中,在XC测试框架中测试内容包括第三方包时,需要手动去设置Header Search Paths才能找到头文件 ,还需要设置test target的PODS_ROOT。

7.xcode7要使用真机做跑测试时,证书必须配对,否则会报错exc_breakpoint错误

链接:https://www.jianshu.com/p/4001e06b150e

收起阅读 »

iOS开发堆栈你理解多少?

浅谈堆栈理解Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。1、栈区(stack):...
继续阅读 »

浅谈堆栈理解
Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;

栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。

1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。
2、堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。

操作系统iOS 中应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在几个个不同的内存区域 。


栈区(stack):
1、存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;
2、程序猿不需要管理栈区变量的内存; 栈区地址从高到低分配;

堆区(heap):
1、堆区的内存分配使用的是alloc;
2、需要程序猿管理内存;
3、ARC的内存的管理,是编译器再编译的时候自动添加 retain、release、autorelease;
4、堆区的地址是从低到高分配)

全局区/静态区(static):
包括两个部分:未初始化过 、初始化过; 也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域; eg:int a;未初始化的。int a = 10;已初始化的。

常量区:常量字符串cString等就是放在这里;

代码区:存放App代码;

例子:

int a = 10;  全局初始化区
char *p; 全局未初始化区

main{
int b; 栈区
char s[] = "abcdef" 栈
char *p1; 栈
char *p2 = "qwerty"; \\\\qwerty在常量区,p2在栈上。
static int c =0; 全局(静态)初始化区
leap1 = (char *)malloc(100);
leap2 = (char *)malloc(200);
分配得来得100和200字节的区域就在堆区。
}

“stack”
局部变量、参数、返回值都存在这里,函数调用开始会参数入栈、局部变量入栈;调用结束依次出栈。

正如名称所示,stack 是后进先出(LIFO )结构。当函数调用其他的函数时,stack frame会被创建;当其他函数退出后,这个frame会自动被破坏。

“heap”

动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap 中的数据,你最少要求有一个保存在stack中的指针,因为你要通过stack中的指针访问heap 中的数据。

你可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。实际上,它有一点点复杂,但这是它的基本结构。

简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。

在iOS 操作系统的stack 段和heap 段中,一般来说你都可以创建数据对象。

stack 对象的优点主要有两点,一是创建速度快,二是管理简单,它有严格的生命周期。stack 对象的缺点是它不灵活。创建时长度是多大就一直是多 大,创建时是哪个函数创建的,它的owner 就一直是它。不像heap 对象那样有多个owner ,其实多个owner 等同于引用计数。只有 heap 对象才是采用“引用计数”方法管理它。

堆空间和栈空间的大小是可变的,堆空间从下往上生长,栈空间从上往下生长。

stack 对象的创建

只要栈的剩余空间大于 stack 对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出。

heap 对象的创建

操作系统对于内存heap 段是采用链表进行管理的。操作系统有一个记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找第一个空间大于所申请的heap 节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。

例如:

NSString 的对象就是 stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。

两类对象的创建方法也不同,前者直接创建 NSString * str1=@"welcome"; ,而后者需要先分配再初始化 NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];。

引用计数是放在堆内存中的一个整型,对象alloc开辟堆内存空间后,引用计数自动置1;

NSString直接赋值是创建在_TEXT段中,_TEXT段是在编译时保存程序代码段的机器码,也就是说NSString会以字符串的形式保存起来,只要字符串名称相同,其地址就相同,就算在新建一个名字一样的NSString,还是原来那个;顺便讲一下_DATA段,他是保存全局变量和静态变量的值的)

_TEXT段:整个程序的代码,以及所有的常量。这部分内存是是固定大小的,只读的。
_DATA段:初始化为非零值的全局变量。
BSS段:初始化为0或未初始化的全局变量和静态变量。
更多细节我后面会讲一篇Mach-O内核方面的文章;

静态和全局的区别

static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;

static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;

static函数与普通函数有什么区别:static函数与普通函数作用域不同,只在定义该变量的源文件内有效;

全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。

补充:内存引用计数的实现

GNUstep的实现是将引用计数保存在对象占用内存块头部的变量中

好处是:

少量的代码即可完成。

能够统一管理引用计数内存块和对象引用计数内存块

苹果的实现是保存在引用计数hash表中

好处是:

对象用内存块的分配无需考虑内存块的头部

引用计数表各记录中存有内存块地址,可以从各个记录追溯到各对象的内存块,这点对调试非常重要

weak对象释放是自动致nil实现:

也是通过一个weakhash表实现的,将weak的对象地址注册到weakhash表中,如果该对象被destroy销毁,则在weak表中将该对象地址致nil,并清除记录

链接:https://www.jianshu.com/p/1f075bdc2e29

收起阅读 »

LLDB调试利器及高级用法

LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我...
继续阅读 »

LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我会把重点放在常用命令的使用方式和技巧上。

一、 LLDB常用调试命令
❶ p、po及 image命令
1、是打印对象,是打印对象的description,演示如下:


2、p命令修改变量,演示如下:


3、imagelookup -a用于寻找栈地址对应的代码位置,演示如下:


3.1 从上图中我们可以看到当程序崩溃时并不能定位到指定的代码位置,使用image寻址命令可以定位到具体的崩溃位置在viewDidLoad方法中的第51行。


3.2 这里说明为什么是程序的名称,因为LLDBDebug在编译后就是一个Macho的可执行文件,也可以理解为镜像文件,image并不是图像的意思,而是代表镜像。这里跟上我们自己的工程名,即用image定位寻址才是寻找我们自己的代码。
❷ bt及frame命令
1、使用命令可以查看函数调用堆栈,然后用 命令即可查看对应函数详细,演示如下:


1.1 上面函数执行的顺序如下:点击登录按钮--验证手机号--验证密码--开始登录。

- (IBAction)login:(UIButton *)sender {

[self validationPhone];
}
#pragma mark --验证手机号
-(void)validationPhone{

[self validationPwd];
}
#pragma mark --验证密码
-(void)validationPwd{

[self startLogin];
}
#pragma mark --开始登陆
-(void)startLogin{

NSLog(@"------开始登录...------");
}

1.2 从bt命令的打印信息中,我们可以很清楚看到函数调用顺序,如下图:


1.3 接下来我们执行 frame select命令即可以查看函数相关信息,同时配合up和down命令追踪函数的调用和被调用关系,演示如下:


1.4 同时可以使用frame variable很方便的查方法的调用者及方法名称,如下图:


❸ breakpoint命令
1、b命令给函数下断点,演示如下图


1.1 当我们的断点下成功后,控制台会打印如下信息:
Breakpoint 1: where = LLDBDebug`-[ViewController login:] at ViewController.m:53, address = 0x00000001034fb0a0

1.2 我们可以看到断点的位置在.m文件的53行,Breakpoint 1这里的1代表的是编号为1的组断点。
使用 我们可以看到断点的数量,同时使用 后面跟上组号,即可删除,演示如下:


3、\color{red}{breakpoint}的\color{red}{c},\color{red}{n},\color{red}{s}以及\color{red}{finish}命令,对应关系如下图:


3.1 我们执行\color{red}{c},\color{red}{n},\color{red}{s}及\color{red}{finish}命令演示如下:


❹ breakpoint命令
1.target stop-hook add -o "frame variable"每次进入断点都会自动打印详细的参数信息,演示如下:


二、 LLDB高级用法
❶ 我们先来简单看下\color{red}{menthods}和\color{red}{pviews}命令的执行效果,演示如下图:


1.1 \color{red}{menthods}命令可以打印当前对象的属性和方法,如下所示:

(lldb) methods p1
<Person: 0x60000003eac0>:
in Person:
Properties:
@property (copy, nonatomic) NSString* name; (@synthesize name = _name;)
@property (nonatomic) long age; (@synthesize age = _age;)
Instance Methods:
- (void) eat; (0x1098bf3e0)
- (void) .cxx_destruct; (0x1098bf4f0)
- (id) description; (0x1098bf410)
- (id) name; (0x1098bf430)
- (void) setName:(id)arg1; (0x1098bf460)
- (void) setAge:(long)arg1; (0x1098bf4c0)
- (long) age; (0x1098bf4a0)
(NSObject ...)

1.2 \color{red}{pviews}命令可以打印当前视图的层级结构,如下所示:

(lldb) pviews
<UIWindow: 0x7fd1719060a0; frame = (0 0; 414 736); gestureRecognizers = <NSArray: 0x60c000058660>; layer = <UIWindowLayer: 0x60c0000364c0>>
| <UIView: 0x7fd16fc06d10; frame = (0 0; 414 736); alpha = 0.8; autoresize = W+H; layer = <CALayer: 0x60000003e7e0>>
| | <UIButton: 0x7fd16fe0b520; frame = (54 316; 266 53); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x60400003b040>>
| | | <UIButtonLabel: 0x7fd16fe023f0; frame = (117.667 17.6667; 30.6667 18); text = '登录'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400008ac80>>
| | | | <_UILabelContentLayer: 0x600000220260> (layer)
| | <UILabel: 0x7fd16fc04a60; frame = (164 225; 80 47); text = 'Qinz'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000088fc0>>
(lldb)

1.3 如果你在原生的XCode中,是敲不出这些命令的,上面只是演示了两个常见的LLDB插件命令的用法,更加高级的用法下面会详细说明。不过在这之前,我们要安装两个插件,接下来先讲解环境的配置。
❷ LLDB插件配置:chisel及LLDB
2.1 chisel是facebook开源的一款LLDB插件,里面封装了很多好用的命令,当然这些命令都是基于苹果提供的api。chisel下载
2.2 这里建议使用包管理工具Homebrew来安装,然后配置脚本路径,演示如下:


2.3 然后在lldb窗口执行命令,演示如下:


2.4 看到输出"command script import /usr/local/opt/chisel/libexec/fblldb.py"即代表安装成功,这里还会看到一个"command script import /opt/LLDB/lldb_commands/dslldb.py
"路径,这是我们接下来要安装的第二个插件

Executing commands in '/Users/Qinz/.lldbinit'.
command script import /usr/local/opt/chisel/libexec/fblldb.py
command script import /opt/LLDB/lldb_commands/dslldb.py
(lldb)

2.5 这个插件的名称也叫LLDB,LLDB下载。我们先clone文件,我这里放置在opt文件夹下,你可以选择自己的文件目录放置,然后依次找到dslldb文件,在~/.initlldb文件中配置路径,演示如下:


2.6 接下来依然在lldb窗口执行 command source ~/.lldbinit命令。到此LLDB插件的配置环境完成,接下来我们讲解这些插件的实用命令。
❸ lldb高级用法
1. 搭配,让你快速找准控件,演示如下:


1.1 taplog是点击控件,会打印控件的地址,大小及透明度等信息,我们拿到地址后执行flicker 0x7fd321e09710命令,此时控件会进行闪烁,这里动态图显示的闪烁效果明显。
2. 和显示和隐藏控件,演示如下:


\color{red}{vs}命令方便动态查看控件的层级关系,演示如下:


3.1 当我们执行\color{red}{vs}命令后会进入动态调试阶段,会出现以下五个命令,每个命令我做了详细注释如下:

(lldb) vs 0x7fe73550a090
Use the following and (q) to quit.
(w) move to superview //移动到父视图
(s) move to first subview //移动到第一个子视图
(a) move to previous sibling //移动上一个兄弟视图
(d) move to next sibling //移动下一个兄弟视图
(p) print the hierarchy //打印视图层级结构

\color{red}{pactions}直接打印对象调用者及方法,演示如下:


\color{red}{border}&\color{red}{unborder}给控件增加和去除边框,演示如下:


5.1 这里的-c即是color,-w即设置边框的宽度。通过这个命令我们可以很方便的查看边框的边缘的问题,而不需要每次重启运行。
6.\color{red}{pclass}打印对象的继承关系,演示如下图:


\color{red}{presponder}命令打印响应链,演示如下图:


\color{red}{caflush}这个命令会重新渲染,即可以重新绘制界面, 相当于执行了 [CATransaction flush] 方法,演示如下:


\color{red}{search}搜索已经存在于栈中的控件及其子控件,演示如下:


\color{red}{lookup}搜索,可执行正则表达式。演示如下:


10.1 上面的搜索会搜索所用镜像模块,我们重点看与我们工程名字相同的模块,即可查看哪些地方调用了这些方法。
11. \color{red}{pbundlepath}打印app路径及\color{red}{pdocspath}打印文档路径,演示如下:


总结:上面详细讲解了LLDB常用命令及高级命令的用法,熟练掌握可大幅度提高Debug能力和开发效率。

我是Qinz,希望我的文章对你有帮助。

转自:https://www.jianshu.com/p/c91f843a64fc

收起阅读 »

你还在用宏定义“iphoneX”判断安全区域(safe area)吗,教你正确使用Safe Area

你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望...
继续阅读 »

你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。
iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length)。 这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。

1、一个独立的ViewController,不包含于任何其他的ViewController。如果状态栏可见,topLayoutGuide表示状态栏的底部,否则表示这个ViewController的上边缘。

2、包含于其他ViewController的ViewController不对这个属性起决定作用,而是由容器ViewController决定这个属性的含义:

3、如果导航栏(Navigation Bar)可见,topLayoutGuide表示导航栏的底部。

4、如果状态栏可见,topLayoutGuide表示状态栏的底部。

5、如果都不可见,表示ViewController的上边缘。

6、这部分还比较好理解,总之是屏幕上方任何遮挡内容的栏的最底部。

iOS 11 开始弃用了这两个属性, 并且引入了 Safe Area 这个概念。苹果建议: 不要把 Control 放在 Safe Area 之外的地方

// These objects may be used as layout items in the NSLayoutConstraint API
@available(iOS, introduced: 7.0, deprecated: 11.0)
open var topLayoutGuide: UILayoutSupport {get}
@available(iOS, introduced: 7.0, deprecated: 11.0)
open var bottomLayoutGuide: UILayoutSupport { get}

今天, 来研究一下 iOS 11 中新引入的这个 API。

UIView 中的 safe area
iOS 11 中 UIViewController 的 topLayoutGuide 和 bottonLayoutGuide 两个属性被 UIView 中的 safe area 替代了。

open var safeAreaInsets: UIEdgeInsets {get}
@available(iOS 11.0, *)
open func safeAreaInsetsDidChange()

safeAreaInsets

这个属性表示相对于屏幕四个边的间距, 而不仅仅是顶部还有底部。这么说好像没有什么感觉, 我们来看一看这个东西分别在 iPhone X 和 iPhone 8 中是什么样的吧!

什么都没有做, 只是新建了一个工程然后在 Main.storyboard 中的 UIViewController 中拖了一个橙色的 View 并且设置约束为:


在 ViewController.swift 的 viewDidLoad 中打印

override func viewDidLoad() {
super.viewDidLoad()
print(view.safeAreaInsets)
}
// 无论是iPhone 8 还是 iPhone X 输出结果均为
// UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)


iPhone 8 VS iPhone X Safe Area (竖屏)


iPhone 8 VS iPhone X Safe Area (横屏)

这样对比可以看出, iPhone X 同时具有上下, 还有左右的 Safe Area。

**再来看这个例子: ** 拖两个自定义的 View, 这个 View 上有一个 显示很多字的Label。然后设置这两个 View 的约束分别是:

let view1 = MyView()
let view2 = MyView()
view.addSubview(view1)
view.addSubview(view2)
let screenW = UIScreen.main.bounds.size.width
let screenH = UIScreen.main.bounds.size.height
view1.frame = CGRect(x: 0, y: 0, width:screenW, height: 200)
view2.frame = CGRect( x: 0, y: screenH - 200, width:screenW, height: 200)


可以看出来, 子视图被顶部的刘海以及底部的 home 指示区挡住了。我们可以使用 frame 布局或者 auto layout 来优化这个地方:

let insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? UIEdgeInsets.zero  
view1.frame = CGRect(x: insets.left,y: insets.top,width:view.bounds.width - insets.left - insets.right,height: 200)
view2.frame = CGRect(x: insets.left,y: screenH - insets.bottom - 200,width:view.bounds.width - insets.left - insets.right,height: 200)


这样起来好多了, 还有另外一个更好的办法是直接在自定义的 View 中修改 Label 的布局:

override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 11.0, *) {
label.frame = safeAreaLayoutGuide.layoutFrame
}
}


这样, 不仅仅是在 ViewController 中能够使用 safe area 了。

UIViewController 中的 safe area

在 iOS 11 中 UIViewController 有一个新的属性

@available(iOS 11.0, *)
open var additionalSafeAreaInsets: UIEdgeInsets

当 view controller 的子视图覆盖了嵌入的子 view controller 的视图的时候。比如说, 当 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 状态的时候, 就有 additionalSafeAreaInsets


自定义的 View 上面的 label 布局兼容了 safe area。

// UIView
@available(iOS 11.0, *)
open func safeAreaInsetsDidChange()
//UIViewController
@available(iOS 11.0, *)
open func viewSafeAreaInsetsDidChange()

这两个方法分别是 UIView 和 UIViewController 的 safe area insets 发生改变时调用的方法,如果需要做一些处理,可以重写这个方法。有点类似于 KVO 的意思。

模拟 iPhone X 的 safe area


额外的 safe area insets 也能用来测试你的 app 是否支持 iPhone X。在没有 iPhone X 也不方便使用模拟器的时候, 这个还是很有用的。

//竖屏
additionalSafeAreaInsets.top = 24.0
additionalSafeAreaInsets.bottom = 34.0
//竖屏, status bar 隐藏
additionalSafeAreaInsets.top = 44.0
additionalSafeAreaInsets.bottom = 34.0
//横屏
additionalSafeAreaInsets.left = 44.0
additionalSafeAreaInsets.bottom = 21.0
additionalSafeAreaInsets.right = 44.0

UIScrollView 中的 safe area
在 scroll view 上加一个 label。设置scroll 的约束为:

scrollView.snp.makeConstraints { (make)  in
make.edges.equalToSuperview()
}


iOS 7 中引入 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性在 iOS11 中被废弃掉了。取而代之的是 UIScrollView 的 contentInsetAdjustmentBehavior

@available(iOS 11.0 , *)
public enum UIScrollViewContentInsetAdjustmentBehavior : Int {
case automatic //default value
case scrollableAxes
case never
case always
}
@available(iOS 11.0 , *)
open var contentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentBehavior

Content Insets Adjustment Behavior

never 不做调整。

scrollableAxes content insets 只会针对 scrollview 滚动方向做调整。

always content insets 会针对两个方向都做调整。

automatic 这是默认值。当下面的条件满足时, 它跟 always 是一个意思

1、能够水平滚动,不能垂直滚动

2、scroll view 是 当前 view controller 的第一个视图

3、这个controller 是被navigation controller 或者 tab bar controller 管理的

4、automaticallyAdjustsScrollViewInsets 为 true

在其他情况下 automoatc 跟 scrollableAxes 一样

Adjusted Content Insets

iOS 11 中 UIScrollView 新加了一个属性: adjustedContentInset

@available(iOS 11.0, *)
open var adjustedContentInset: UIEdgeInsets {get}

adjustedContentInset 和 contentInset 之间有什么区别呢?

在同时有 navigation 和 tab bar 的 view controller 中添加一个 scrollview 然后分别打印两个值:

//iOS 10
//contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
//iOS 11
//contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
//adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

然后再设置:

`// 给 scroll view 的四个方向都加 10 的间距`
`scrollView.contentInset = UIEdgeInsets(top: ``10``, left: ``10``, bottom: ``10``, right: ``10``)`

打印:

//iOS 10
//contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
//iOS 11
//contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
//adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

由此可见,在 iOS 11 中 scroll view 实际的 content inset 可以通过 adjustedContentInset 获取。这就是说如果你要适配 iOS 10 的话。这一部分的逻辑是不一样的。

系统还提供了两个方法来监听这个属性的改变

//UIScrollView
@available(iOS 11.0, *)
open func adjustedContentInsetDidChange()
//UIScrollViewDelegate
@available(iOS 11.0, *)
optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

UITableView 中的 safe area

我们现在再来看一下 UITableView 中 safe area 的情况。我们先添加一个有自定义 header 以及自定义 cell 的 tableview。设置边框为 self.view 的边框。也就是

tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
或者
tableView.frame = view.bounds


自定义的 header 上面有一个 lable,自定义的 cell 上面也有一个 label。将屏幕横屏之后会发现,cell 以及 header 的布局均自动留出了 safe area 以外的距离。cell 还是那么大,只是 cell 的 contnt view 留出了相应的距离。这其实是 UITableView 中新引入的属性管理的:

@available(iOS 11.0, *)
open var insetsContentViewsToSafeArea: Bool

insetsContentViewsToSafeArea 的默认值是 true, 将其设置成 no 之后:


可以看出来 footer 和 cell 的 content view 的大小跟 cell 的大小相同了。这就是说:在 iOS 11 下, 并不需要改变 header/footer/cell 的布局, 系统会自动区适配 safe area

需要注意的是, Xcode 9 中使用 IB 拖出来的 TableView 默认的边框是 safe area 的。所以实际运行起来 tableview 都是在 safe area 之内的。


UICollectionView 中的 safe area

我们在做一个相同的 collection view 来看一下 collection view 中是什么情况:


这是一个使用了 UICollectionViewFlowLayout 的 collection view。 滑动方向是竖向的。cell 透明, cell 的 content view 是白色的。这些都跟上面 table view 一样。header(UICollectionReusableView) 没有 content view 的概念, 所以给其自身设置了红色的背景。

从截图上可以看出来, collection view 并没有默认给 header cell footer 添加safe area 的间距。能够将布局调整到合适的情况的方法只有将 header/ footer / cell 的子视图跟其 safe area 关联起来。跟 IB 中拖 table view 一个道理。


现在我们再试试把布局调整成更像 collection view 那样:


截图上可以看出来横屏下, 左右两边的 cell 都被刘海挡住了。这种情况下, 我们可以通过修改 section insets 来适配 safe area 来解决这个问题。但是再 iOS 11 中, UICollectionViewFlowLayout 提供了一个新的属性 sectionInsetReference 来帮你做这件事情。

@available(iOS 11.0, *)
public enum UICollectionViewFlowLayoutSectionInsetReference : Int {
case fromContentInset
case fromSafeArea
case fromLayoutMargins
}
/// The reference boundary that the section insets will be defined as relative to. Defaults to .fromContentInset.

/// NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals `.fromSafeArea`, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.
@available(iOS 11.0, *)
open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

可以看出来,系统默认是使用 .fromContentInset 我们再分别修改, 看具体会是什么样子的。

fromSafeArea

这种情况下 section content insets 等于原来的大小加上 safe area insets 的大小。

跟使用 .fromLayoutMargins 相似使用这个属性 colection view 的 layout margins 会被添加到 section content insets 上面。


IB 中的 Safe Area

前面的例子都说的是用代码布局要实现的部分。但是很多人都还是习惯用 Interface Builder 来写 UI 界面。苹果在 WWDC 2107 Session 412 中提到:Storyboards 中的 safe area 是向下兼容的 也就是说, 即使在 iOS10 及以下的 target 中,也可以使用 safe area 来做布局。唯一需要做的就是给每个 stroyboard 勾选 Use Safe Area Layout Guide。实际测试看,应该是 iOS9 以后都只需要这么做。

知识点: 在使用 IB 设置约束之后, 注意看相对的是 superview 还是 topLayoutGuide/bottomLayoutGuide, 包括在 Xcode 9 中勾选了 Use Safe Area Layout Guide 之后,默认应该是相对于 safe area 了。

总结

1、在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只会给之后的工作代码更多的麻烦。

2、如果只需要适配到 iOS9 之前的 storyboard 都只需要做一件事情。

3、Xcode9 用 IB 可以看得出来, safe area 到处都是了。理解起来很简单。就是系统对每个 View 都添加了 safe area, 这个区域的大小,是否跟 view 的大小相同是系统来决定的。在这个 View 上的布局只需要相对于 safe area 就可以了。每个 View 的 safe area 都可以通过 iOS 11 新增的 API safeAreaInsets 或者 safeAreaLayoutGuide 获取。

4、对与 UIViewController 来说新增了 **additionalSafeAreaInsets **这个属性, 用来管理有 tabbar 或者 navigation bar 的情况下额外的情况。

5、对于 UIScrollView, UITableView, UICollectionView 这三个控件来说,系统以及做了大多数的事情。

6、scrollView 只需要设置 contentInsetAdjustmentBehavior 就可以很容易的适配带 iPhoneX

7、tableView 只需要在 cell header footer 等设置约束的时候相对于 safe area 来做

8、对 collection view 来说修改 sectionInsetReference 为 .safeArea 就可以做大多数的事情了。

总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(当然是在 iPhoneX上才有值) 以后在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局。
文中有些图片都是从这里来的, 很多内容也跟这篇文章差不多 可能需要梯子

参考文章 可能需要梯子

作者:CepheusSun
链接:http://www.jianshu.com/p/63c0b6cc66fd

转自:https://www.jianshu.com/p/5bebc28e0ede

收起阅读 »

深度优先搜索和广度优先搜索

不撞南墙不回头-深度优先搜索基础部分对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321把问题形象化,假如有1,2,3三张扑克牌和编...
继续阅读 »

不撞南墙不回头-深度优先搜索

基础部分

对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。

输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321

把问题形象化,假如有1,2,3三张扑克牌和编号为1,2,3的三个箱子,把三张扑克牌分别放到三个箱子里有几种方法?

我们用深度优先遍历搜索的思想来考虑这个问题。

到1号箱子面前时,我们手里有1,2,3三种牌,我们把1放进去,然后走到2号箱子面签,手里有2,3两张牌, 然后我们把2放进去,再走到3号箱子前,手里之后3这张牌,所以把3放进去,然后再往前走到我们想象出来的一个4号箱子前,我们手里没牌了,所以,前面三个箱子中放牌的组合就是要输出的一种组合方式。(123)

然后我们后退到3号箱子,把3这张拍取出来,因为这时我们手里只有一张牌,所以再往里放的话还是原来那种情况,所以我们还要再往后推,推到2号箱子前,把2从箱子中取出来,这时候我们手里有2,3两张牌,这时我们可以把3放进2号箱子,然后走到3号箱子中把2放进去,这又是一种要输出的组合方式.(132)

就找这个思路继续下去再次回退的时候,我们就要退到1号箱,取出1,然后分别放2和3进去,然后产生其余的组合方式。

有点啰嗦,但是基本是这么一个思路。

我们来看一下实现的代码

def sortNumber(self, n):
flag = [False for i in range(n)]
a = [0 for i in range(n)]
l = []

def dfs(step):
if step == n:
l.append(a[:])
return
for i in range(n):
if flag[i] is False:
flag[i] = True
a[step] = i
dfs(step + 1)
flag[i] = False
dfs(0)
return l

输出是

[[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]

我们创建的a这个list相当于上面说到的箱子,flag这个list呢,来标识某一个数字是否已经被用过了。

其实主要的思想就这dfs方法里面的这个for循环中,在依次的排序中,我们默认优先使用最小的那个数字,这个for循环其实就代表了一个位置上有机会放所有的这些数字,这个flag标识就避免了在一个位置重复使用数字的问题。

如果if 成立,说明当前位置可以使用这个数字,所以把这个数字放到a这个数组中,然后flag相同为的标识改为True,也就是说明这个数已经被占用了,然后在调用方法本身,进行下一步。

flag[i] = False这句代码是很重要的,在上面的dfs(也就是下一步)结束之后,返回到当前这个阶段,我们必须模拟收回这个数字,也就是把flag置位False,表示这个数字又可以用了。

思路大概就是这样子的,这就是深度优先搜索的一个简单的场景。用debug跟一下,一步一步的来看代码就更清晰的了。

迷宫问题

上面我们已经简单的了解了深度优先搜索,下面我们通过一个迷宫的问题来进一步数字这个算法,然后同时引出我们的广度优先搜索。

迷宫是由m行n列的单元格组成,每个单元格要不是空地,要不就是障碍物,我们的任务是找到一条从起点到终点的最短路径。

我们抽象成模型来看一下


start代表起点,end代表终点,x代表障碍物也就是不能通过的点。

首先我们来分析一下,从start(0,0)这个点,甚至说是每一个点出发,都有四个方向可以走,上下左右,仅对于(0,0)这个点来说,只能往右和下走,因为往左和上就到了单元格外面了,我们可以称之为越界了。

我们用深度优先的思想来考虑的话,我们可以从出发点开始,全部都先往一个方向走,然后走到遇到障碍物或者到了边界的情况下,在改变另一个方向,然后再走到底,这样一直走下去。

拿到我们这个题目中,我们可以这样来思考,在走的时候,我们规定一个右下左上这样的顺序,也就是先往右走,走到不能往右走的时候在变换方向。比如我们从(0,0)走到(0,1)这个点,在(0,1)这个点也是先往右走,但是我们发现(0,2)是障碍物,所以我们就改变为往下走,走到(1,1),然后在(1,1)开始也是先向右走,这样一直走下去,直到找到我们的目标点。

其中我们要注意一点,在右下左上这四个方向中有一个方向是我们来时候的方向,在当前这个点,四个方向没有走完之前我们不要后退到上一个点,所以我们也需要一个像前面排数字代码里面的flag数组来记录当前位置时候被占用。我们必须是四个方向都走完了才能往后退到上一个换方向。

下面我贴一下代码

def depthFirstSearch(self):
m = 5
n = 4

# 5行 4 列
flag = [[False for i in range(n)] for j in range(m)]
# 存储不能同行的位置
a = [[False for i in range(n)] for j in range(m)]
a[0][2] = True
a[2][2] = True
a[3][1] = True
a[4][3] = True

global min_step
min_step = 99999

director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

def dfs(x, y, step):

# 什么情况下停止 (找到目标坐标)
if x == 3 and y == 2:
global min_step
if step < min_step:
min_step = step
return

# 右下左上
for i in range(4):
# 下一个点
nextX = x + director_l[i][0]
nextY = y + director_l[i][1]

# 是否越界
if nextX < 0 or nextX >= m or nextY < 0 or nextY >= n:
continue

# 不是障碍 and 改点还没有走过
if a[x][y] is False and flag[x][y] is False:
flag[x][y] = True
dfs(nextX, nextY, step+1)
flag[x][y] = False #回收

dfs(0, 0, 0)
return min_step

首先flag这个算是二位数组吧,来记录我们位置是否占用了,然后a这个数组,是来记录整个单元格的,也就是标识那些障碍物的位置坐标。同样的,重点是这个dfs方法,他的参数x,y是指当前的坐标,step是步数。

这个大家可以看到一个director_l的数组,他是来辅助我们根据当前左边和不同方向计算下一个位置的坐标的。

dfs中我们已经注明了搜索停止的判断方式,也就是找到(3,2)这个点,然后下面的for循环,则代表四个不同的方向,每一个方向我们都会先求出他的位置,然后判断是否越界,如果没有越界在判断是否是障碍或者是否已经走过了,满足了所有的判断条件,我们在继续往下一个点,直到找到目标,比较路径的步数。

这就是深度优先搜索了,当然,这个题目我们还有别的解法,这就到了我们说的广度优先搜索。

层层递进-广度优先搜索

我们先大体说一下广度优先搜索的思路,深度优先是先穷尽一个方向,而广度优先呢,则是基于一个位置,先拿到他所有能到达的位置,然后分别基于这些新位置,拿到他们能到达的所有位置,一次这样层层的递进,直到找到我们的终点。


从(0,0)出发,可以到达(0,1)和(1,0),然后再从(0,1)出发到达(1,1),从(1,0)出发,到达(2,0)和(1,1),以此类推。

所以我们我们维护一个队列来储存每一层遍历到达的点,当然了,不要重复储存同一个点。我们用一个指针head来标识当前的基准位置,也就是说最开始指向(0,0),当储存完毕所有(0,0)能抵达的位置时,我们就应该改变我们的基准位置了,这时候head++,就到了(0,1)这个位置,然后储存完他能到的所有位置,head++,就到了(1,0),然后继续。

def breadthFirstSearch(self):

class Node:
def __init__(self):
x = 0
y = 0
step = 0

m, n = 5, 4
# 记录
flag = [[False for i in range(n)] for j in range(m)]

# 储存地图信息
a = [[False for i in range(n)] for j in range(m)]
a[0][2] = True
a[2][2] = True
a[3][1] = True
a[4][3] = True
# 队列
l = []
startX, startY, step = 0, 0, 0
head = 0
index = 0

node = Node()
node.x = startX
node.y = startY
node.step = step
index += 1
l.append(node)
flag[0][0] = True

director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

while head < index:

last_node = l[head]
# 处理四个方向
for i in range(4):

# 当前位置
currentX = last_node.x + director_l[i][0]
currentY = last_node.y + director_l[i][1]

# 找到目标
if currentX == 4 and currentY == 2:
print('step = ' + str(last_node.step + 1))
return

#是否越界
if currentX < 0 or currentY < 0 or currentX >= m or currentY >= n:
continue

if a[currentX][currentY] is False and flag[currentX][currentY] is False:


#不是目标
flag[currentX][currentY] = True

node_new = Node()
node_new.x = currentX
node_new.y = currentY
node_new.step = last_node.step+1
l.append(node_new)
index += 1



head += 1

首先我们定义了一个节点Node的类,来封装节点位置和当前的步数,flag,a,director_l这两个数组作用跟深度优先搜索相同,l是我们维护的队列,head指针指向当前基准的那个位置的,index指针指向队列尾。首先我们先把第一个Node(也就是起点)存进队列,广度优先搜索不需要递归,只要加一个循环就行。

每次走到符合要求的位置,我们便把他封装成Node来存进对列中,每存一个index都要+1.

head指针必须在一个节点四个方向都处理完了之后才可以+1,变换下一个基准节点。

小结

简单的介绍了深度优先搜索和广度优先搜索,深度优先有一种先穷尽一个方向然后结合使用回溯来找到解,广度呢,可能就是每做一次操作就涵盖了所有的可能结果,然后一步步往后推出去,找到最后的解。这算我个人的理解吧,不准确也不官方,思想也只能算是稍有体会,还得继续努力。

题外话

碍于自己的算法基础太差,最近一直在做算法题,我是先刷了一段时间的题目,发现吃力了,才开始看的书。感觉有点本末倒置。其实应该是先看看书,把算法的一些常用大类搞清楚了,形成一个知识框架,这样在遇到问题的时候可以知道往那些方向上面思考,可能会好一些吧。

链接:https://www.jianshu.com/p/9a6a65078fc2

收起阅读 »

ReactiveObjC看这里就够了

1、什么是ReactiveObjCReactiveObjC是ReactiveCocoa系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传...
继续阅读 »

1、什么是ReactiveObjC

ReactiveObjC是ReactiveCocoa
系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传递。核心思路:创建信号->订阅信号(subscribeNext)->发送信号
通过信号signals的传输,重新组合和响应,软件代码的编写逻辑思路将变得更清晰紧凑,有条理,而不再需要对变量的变化不断的观察更新。

2、什么是函数响应式编程

响应式编程是一种和事件流有关的编程模式,关注导致状态值改变的改变的行为事件,一系列事件组成了事件流,一系列事件是导致属性值发生变化的原因,非常类似于设计模式中的观察者模式。在网上流传一个非常经典的解释响应式编程的概念,在一般的程序开发中:a = b + c,赋值之后 b 或者 c 的值变化后,a 的值不会跟着变化,而响应式编程的目标就是,如果 b 或者 c 的数值发生变化,a 的数值会同时发生变化;

3、ReactiveObjC的流程分析

ReactiveObjC主要有三个关键类:
1、RACSignal信号
RACSignal 是各种信号的基类,其中RACDynamicSignal是用的最多的动态信号
2、RACSubscriber订阅者
RACSubscriber是实现了RACSubscriber协议的订阅者类,这个协议定义了4个必须实现的方法

@protocol RACSubscriber <NSObject>
@required
- (void)sendNext:(nullable id)value; //常见
- (void)sendError:(nullable NSError *)error; //常见
- (void)sendCompleted; //常见
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end

RACSubscriber主要保存了三个block,跟三个常见的协议方法一一对应\

@property (nonatomic, copy) void (^next)(id value);
@property (nonatomic, copy) void (^error)(NSError *error);
@property (nonatomic, copy) void (^completed)(void);

3、RACDisposable清洁工
RACDisposable主要是对资源的释放处理,其中使用RACDynamicSignal时,会创建一个RACCompoundDisposable管理清洁工对象。其内部定义了两个数组,一个是_inlineDisposables[2]固定长度2的A fast array,超出2个对象的长度由_disposables数组管理,_inlineDisposables数组速度快,两个数组都是线程安全的。

4、ReactiveObjC导入工程的方式

pod 'ReactiveObjC'

5、ReactiveObjC的几种使用情况

1、NSArray 数组遍历

NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
[array.rac_sequence.signal subscribeNext:^(id _Nullable x) {
NSLog(@"数组内容:%@", x);
}];

2、NSArray快速替换数组中内容为99和单个替换数组内容,两个方法都不会改变原数组内容,操作完后都会生成一个新的数组,省去了创建可变数组然后遍历出来单个添加的步骤。

NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
/*
NSArray * newArray = [[array.rac_sequence mapReplace:@"99"] array];
NSLog(@"%@",newArray);
*/
NSArray * newArray = [[array.rac_sequence map:^id _Nullable(id _Nullable value) {
NSLog(@"原数组内容%@",value);
return @"99";
}] array];
NSLog(@"%@",newArray);

3、NSDictionary 字典遍历

NSDictionary * dic = @{@"name":@"Tom",@"age":@"20"};
[dic.rac_sequence.signal subscribeNext:^(id _Nullable x) {

RACTupleUnpack(NSString *key, NSString * value) = x;//X为为一个元祖,RACTupleUnpack能够将key和value区分开
NSLog(@"数组内容:%@--%@",key,value);
}];

4、UIButton 监听按钮的点击事件

UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(100, 200, 100, 60);
btn.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn];
//监听点击事件
[[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
}];

5、UITextField 监听输入框的一些事件

UITextField * textF = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
textF.placeholder = @"请输入内容";
textF.textColor = [UIColor blackColor];
[self.view addSubview:textF];
//实时监听输入框中文字的变化
[[textF rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
NSLog(@"输入框的内容--%@",x);
}];
//UITextField的UIControlEventEditingChanged事件,免去了KVO
[[textF rac_signalForControlEvents:UIControlEventEditingChanged] subscribeNext:^(__kindof UIControl * _Nullable x) {
NSLog(@"%@",x);
}];
//添加监听条件
[[textF.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
return [value isEqualToString:@"100"];//此处为判断条件,当输入的字符为100的时候执行下面方法
}]subscribeNext:^(NSString * _Nullable x) {
NSLog(@"输入框的内容为%@",x);
}];

6、KVO 代替KVO来监听按钮frame的改变

UIButton * loginBtn = [UIButton buttonWithType:UIButtonTypeCustom];
loginBtn.frame = CGRectMake(100, 210, 100, 60);
loginBtn.backgroundColor = [UIColor blueColor];
[loginBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[loginBtn setTitle:@"666" forState:UIControlStateNormal];
//[loginBtn setTitle:@"111" forState:UIControlStateDisabled];
[self.view addSubview:loginBtn];
//监听点击事件
[[loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
x.frame = CGRectMake(100, 210, 200, 300);
}];
//KVO监听按钮frame的改变
[[loginBtn rac_valuesAndChangesForKeyPath:@"frame" options:(NSKeyValueObservingOptionNew) observer:self] subscribeNext:^(RACTwoTuple<id,NSDictionary *> * _Nullable x) {
NSLog(@"frame改变了%@",x);
}];

//下面方法也能监听,但是在按钮创建的时候此方法也执行了,简单说就是在界面展示之前此方法就走了一遍,总感觉怪怪的。
/*
[RACObserve(loginBtn, frame) subscribeNext:^(id _Nullable x) {
NSLog(@"frame改变了%@",x);
}];

7、NSNotification 监听通知事件

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardDidShowNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
NSLog(@"监听键盘弹出"); //不知道为啥此方法不止走一次,但是原本的通知监听方法只走一次,有知道的可以私信我,谢谢
}];

8、timer 代替timer定时循环执行方法

[[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
//这里面的方法2秒一循环
}];
//如果关闭定时器,停止需要创建一个全局的disposable
//@property (nonatomic, strong) RACDisposable * disposable;//创建
/*
self.disposable = [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
NSLog(@"当前时间:%@", x); // x 是当前的系统时间
//关闭计时器
[self.disposable dispose];
}];
*/

6、开发中用到的小栗子

1、发送短信验证码的按钮倒计时

/*
@property (nonatomic, strong) RACDisposable * disposable;
@property (nonatomic, assign) NSInteger time;
*/
//上面两句要提前定义
UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(100, 200, 200, 50)];
btn.titleLabel.textAlignment = NSTextAlignmentCenter;
btn.backgroundColor = [UIColor greenColor];
[btn setTitle:@"发送验证码" forState:(UIControlStateNormal)];
[self.view addSubview:btn];
[[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
self.time = 10;
btn.enabled = NO;
[btn setTitle:[NSString stringWithFormat:@"请稍等%zd秒",self.time] forState:UIControlStateDisabled];
self.disposable = [[RACSignal interval:1.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
//减去时间
self.time --;
//设置文本
NSString *text = (self.time > 0) ? [NSString stringWithFormat:@"请稍等%zd秒",_time] : @"重新发送";
if (self.time > 0) {
btn.enabled = NO;
[btn setTitle:text forState:UIControlStateDisabled];
}else{
btn.enabled = YES;
[btn setTitle:text forState:UIControlStateNormal];
//关掉信号
[self.disposable dispose];
}
}];
}];

2、登录按钮的状态根据账号和密码输入框内容的长度来改变

UITextField *userNameTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 70, 200, 50)];
UITextField *passwordTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 130, 200, 50)];
userNameTF.placeholder = @"请输入用户名";
passwordTF.placeholder = @"请输入密码";
[self.view addSubview:userNameTF];
[self.view addSubview:passwordTF];
UIButton *loginBtn = [[UIButton alloc]initWithFrame:CGRectMake(40, 180, 200, 50)];
[loginBtn setTitle:@"马上登录" forState:UIControlStateNormal];
[self.view addSubview:loginBtn];
//根据textfield的内容来改变登录按钮的点击可否
RAC(loginBtn, enabled) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
return @(username.length >= 11 && password.length >= 6);
}];
//根据textfield的内容来改变登录按钮的背景色
RAC(loginBtn, backgroundColor) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
return (username.length >= 11 && password.length >= 6) ? [UIColor redColor] : [UIColor grayColor];
}];

结尾:
本文参考:

关于ReactiveObjC原理及流程简介https://www.jianshu.com/p/fecbe23d45c1

响应式编程之ReactiveObjC常见用法https://www.jianshu.com/p/6af75a449d90

【iOS 开发】ReactiveObjC(RAC)的使用汇总

https://www.jianshu.com/p/0845b1a07bfa

链接:https://www.jianshu.com/p/222c21007251

收起阅读 »

提升用户愉悦感的润滑剂-看SDWebImage本地缓存结构设计

手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会...
继续阅读 »

手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;
而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会永远将这个APP打入冷宫。想要优化界面的响应、节省流量,本地缓存对用户而言是透明的,却是必不可少的一环。
设计本地缓存并不是开一个数组或本地数据库,把数据丢进去就能达到预期效果的,这是因为:

1、内存读写快,但容量有限,图片容易丢失;
2、磁盘容量大,图片“永久”保存,但读写较慢。

这对计算机与生俱来的矛盾,导致缓存设计必须将两种存储方式组合使用,加上iOS系统平台特性,无形中增加了本地缓存系统的复杂度,本篇来看看 SDWebImage 是如何实现一个流畅的缓存系统的。

SDWebImage 本地缓存的整体流程如下:


缓存数据的格式

在深入具体的读写流程之前,先了解一下存储数据的格式,这有助于我们理解后续的操作步骤:

1、为了加快界面显示的需要,内存缓存的图片用 UIImage;
2、磁盘缓存的是 NSData,是从网络下载到的原始数据。

写入流程

存入图片时,调用入口方法:

- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock

先写入 SDMemoryCache :

[self.memCache setObject:image forKey:key cost:cost];

再写入磁盘,由 ioQueue 异步执行:

- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

读取流程

读取图片时,调用入口方法为:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock

首先从内存缓存中获取:

UIImage *image = [self imageFromMemoryCacheForKey:key];

如果内存中有,直接返回给外部调用者;当内存缓存获取失败时,从磁盘获取图片文件数据:

NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];

解码为 UIImage:

diskImage = [self diskImageForKey:key data:diskData options:options];

并写回内存缓存,再返回给调用者。

磁盘缓存

磁盘缓存位于沙盒的 Caches 目录
下:/Library/Caches/default/com.hackemist.SDWebImageCache.default/,
保证了缓存图片在下次启动还存在,又不会被iTunes备份。
文件名由cachedFileNameForKey生成,使用Key(即图片URL)的MD5值,顺便说明一下,图片的Key还有其他作用:

1、作为获取缓存的索引
2、防止重复写入

写入过程很简单:

- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

利用 NSData 的文件写入方法:

[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];

内存缓存

SDMemoryCache 是继承 NSCache 实现的,占用空间是用像素值来统计的(SDCacheCostForImage),因为 NSCache 的totalCostLimit 并不严格(关于 NSCache 的一些特性,请参考被忽视和误解的NSCache),用像素计算可以方
便预估和加快运算。

辅助内存缓存 weakCache

你可能从看前面流程图时,就好奇这个辅助内存缓存的作用是什么,这是由于收到内存警告时,NSCache 里的图片可能已经被系统清除,但实际图片还是被界面上的 ImageView 保留着,因此在 weakCache 再保存一份,遇到这种情况时,只要简单地将 weakCache 中的值写回 NSCache 即可,这样提高了缓存命中率,也避免在界面保有图片时,缓存系统的误判,导致重复下载或从磁盘加载图片。
weakCache 由 NSMapTable 实现,因为普通的NSDictionary无法分别对Key强引用,对值弱引用,即 weakCache 利用对 UIImage 的弱引用,可以判断是否被缓存以外的对象使用,是本地缓存加倍顺滑的关键喔。

总结

SDMemoryCache 的本地缓存很好地平衡了内存和磁盘的优缺点,最大限度利用了系统本身提供的 NSCache 和 NSData 的原生方法,巧妙地利用 weak 属性判断 UIImage 是否被引用问题,为我们开发提供了值得借鉴的思路。

链接:https://www.jianshu.com/p/49ceb5f58590

收起阅读 »

iOS崩溃统计原理 & 日志分析整理

简介当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。崩溃原因崩...
继续阅读 »

简介

当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。
包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。

崩溃原因

崩溃是指应用产生了系统不允许的行为时,系统终止其运行导致的现象。崩溃发生的原因有:

1、存在CPU无法运行的代码
不存在或者无法执行
2、操作系统执行某项策略,终止程序
启动时间过长或者消耗过多内存时,操作系统会终止程序运行
3、编程语言为了避免错误终止程序:抛出异常
4、开发者为了避免失败终止程序:Assert

产生崩溃日志

在程序出现以上问题时,系统会抛出异常,结束程序:
出现异常情况,终止程序:


分析崩溃日志

在发生崩溃时,会产生崩溃日志并且保存在设备上,用于后期对问题定位,崩溃日志的内容包括以下部分:程序信息、异常信息、崩溃堆栈、二进制镜像。下面对每部分进行说明。

崩溃日志程序信息:

Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
Hardware Model: iPad6,8
Process: TheElements [303]
Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
Identifier: com.example.apple-samplecode.TheElements
Version: 1.12
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.example.apple-samplecode.TheElements [402]

Date/Time: 2016-08-22 10:43:07.5806 -0700
Launch Time: 2016-08-22 10:43:01.0293 -0700
OS Version: iPhone OS 10.0 (14A5345a)
Report Version: 104

汇总部分包含崩溃发生环境的基本信息:

1、Incident Identifier:日志ID。

2、CrashReport Key:设备匿名ID,同一设备的崩溃日志该值相同。

3、Beta Identifier:设备和崩溃应用组合ID。

4、Process:执行程序名,等同CFBundleExecutable。

5、Version:程序版本号,等同CFBundleVersion/CFBundleVersionString。

6、Code type:程序构造:ARM-64、ARM、x86

异常信息:

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0

异常信息:

1、Exception Codes:使用十六进制表示的程序特定信息,一般不展示。

2、Exception Subtype:易读(相比十六进制地址)的异常信息。

3、Exception Message:异常的额外信息。

4、Exception Note:不特指某种异常类型的额外信息。

5、Termination Reason:程序终止的异常信息。

6、Triggered Thread:异常发生时的线程。

崩溃堆栈:

Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 TheElements 0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
1 UIKit 0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
2 UIKit 0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
3 QuartzCore 0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
4 libdispatch.dylib 0x000000018dd6d1c0 _dispatch_client_callout + 16
5 libdispatch.dylib 0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
6 CoreFoundation 0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
7 CoreFoundation 0x000000018ee8fb18 __CFRunLoopRun + 1660
8 CoreFoundation 0x000000018edbe048 CFRunLoopRunSpecific + 444
9 GraphicsServices 0x000000019083f198 GSEventRunModal + 180
10 UIKit 0x0000000194d21bd0 -[UIApplication _run] + 684
11 UIKit 0x0000000194d1c908 UIApplicationMain + 208
12 TheElements 0x00000001000653c0 main (main.m:55)
13 libdyld.dylib 0x000000018dda05b8 start + 4

Thread 1:
0 libsystem_kernel.dylib 0x000000018deb2a88 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x000000018df75188 _pthread_wqthread + 968
2 libsystem_pthread.dylib 0x000000018df74db4 start_wqthread + 4

...

第一行列出了线程信息以及所在队列,之后是追溯链中独立栈帧的详细信息:

1、栈帧号。栈帧号为0的代表当前执行停顿的函数,1则是调用当前停顿函数的主调函数,即0为1的被调函数,1为0的主调函数,以此类推。
2、执行函数所在的二进制包
3、地址信息:对于0栈帧来说,代表当前执行停顿的地址。其他栈帧则是获取控制权后接下来执行的地址。
4、函数名

二进制镜像:

Binary Images:
0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
...

日之内包含多个二进制镜像,每个二进制镜像内包含以下信息:

1、二进制镜像在程序内的地址空间
2、二进制的名称或者bundleID
3、二进制镜像的架构信息 arm64等
4、二进制镜像的UUID,每次构建都会改变,该值用于在符号化日志时定位对应的dSYM文件。
5、磁盘上的二进制路径

符号化

app.xcarchive文件,包内容包含dSYM和应用的二进制文件。
更精确的符号化,可以结合崩溃日志、项目二进制文件、dSYM文件,对其进行反汇编,从而获得更详细的信息。

符号化就是将追溯的地址信息转换成函数名及行数等信息,便于研发人员定位问题。
当程序结束运行时,会产生崩溃日志,日志内包含每个线程的堆栈信息。当我们使用Xcode进行调试时,崩溃或者断点信息都会展示出实例和方法名等信息(符号信息)。相反,当应用被发布后,符号信息并不会包含在应用的二进制文件中,所以服务端收到的是未符号化的包含十六进制地址信息的日志文件。
查看本机崩溃日志步骤如下:

1、将手机连接到Mac
2、启动Xcode->Window->Devices and simulators
3、选择View Device Logs

选择左侧应用,之后就可以在右侧看到崩溃日志信息:


日志内包含符号化内容-[__NSArrayI objectAtIndex:]和十六进制地址0x000db142 0xb1000 + 172354。这种日志类型成为部分符号化崩溃日志。
部分符号化的原因在于,Xcode只能符号化系统组件,例如UIKit、CoreFoundation等。但是对于非系统库产生的崩溃,在没有符号表的情况下就无法符号化。
分析第三行未符号化的代码:

0x000db142 0xb1000 + 172354

以上内容说明了崩溃发生在内存地址0x000db142,此地址和0xb1000 + 172354是相等的。0xb1000代表这部分许的起始地址,172354代表偏移位。

崩溃日志类型:

崩溃日志可能包含几种状态:未符号化、完全符号化、部分符号化。
未符号化的崩溃日志追溯链中没有函数的名字等信息,而是二进制镜像执行代码的十六进制地址。
完全符号化的崩溃日志中,所有的十六进制地址都被替换为对应的函数符号。

符号化流程

符号化需要两部分内容:崩溃的二进制代码和编译产生的对应dSYM。

符号表

当编译器将源码转换为机器码时,会生成一个调试符号表,表内是二进制结构到原始源码的映射关系。调试符号表保存在dSYM(debug symbol调试符号表)文件内。调试模式下符号表会保存在编译的二进制内,发布模式则将符号表保存在dSYM文件内用于减少包的体积。

当崩溃发生时,会在设备存储一份未符号化的崩溃日志
获取崩溃日志后,通过dSYM对追溯链中的每个地址进行符号化,转换为函数信息,产生的结果就是符号化后的崩溃日志。

函数调用堆栈

我们知道,崩溃日志内包含函数调用的追溯信息,明白堆栈是怎么产生的有利于我们理解和分析崩溃日志。


函数调用是在栈进行的,函数从调用和被调用方分为:主调函数和被调函数,这次我们只讨论每个函数在栈中的几个核心部分:

1、上一个函数(主调函数)的堆栈信息。
2、入参。
3、局部变量。

入参和局部变量容易理解,下面讨论为什么要保存主调函数的堆栈信息。
说到这点就需要聊到寄存器。

寄存器


寄存器的类型和基本功能:

eax:累加寄存器,用于运算。
ebx:基址寄存器,用于地址索引。
ecx:计数寄存器,用于计数。
edx:数据寄存器,用于数据传递。
esi:源变址寄存器,存放相对于DS段之源变址指针。
edi:目的变址寄存器,存放相对于ES段之目的的变址指针。
esp:堆栈指针,指向当前堆栈位置。
ebp:基址指针寄存器,相对基址位置。

寄存器约定

背景:

1、所有函数都可以访问和操作寄存器,寄存器对于单个CPU来说数量是固定的
2、单个CPU来说,某一时刻只有一个函数在执行
3、需要保证函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后使用的寄存器值

被调函数在执行时,需要使用寄存器来保存数据和执行计算,但是在被调函数完成时,需要把寄存器还原,用于主调函数的执行,所以出现了寄存器约定。

约定内容:

1、主调函数的保存寄存器,在唤起被调函数前,需要显示的将其保存在栈中。
主调寄存器:%eax、%edx、%ecx
2、被调函数的保存寄存器,使用前压栈,并在函数返回前从栈中恢复原值。
被调寄存器:%ebx、%esi、%edi
3、被调函数必须保存%ebp和%esp,并在函数返回后恢复调用前的值。

遵守寄存器约定的函数堆栈调用

了解了寄存器功能和寄存器约定后,我们再看函数调用堆栈:


1、栈帧逻辑:栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
2、保存栈帧:被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。
3、回溯:所以获取到崩溃时线程的ebp和esp 就能回溯到上一个调用,依次类推,回溯出所有的调用堆栈

总结

通过以上内容,我们了解了崩溃日志产生原理、崩溃日志内容和崩溃日志分析,下面分享几个分析崩溃日志的小提示作为结束:

1、不止关注崩溃本行,结合上下文进行分析。
2、不止关注崩溃线程,要结合其他线程的堆栈信息。
3、通过多个崩溃日志,组合分析。
4、使用地址定位和野指针工具重现内存问题。

参考资料

Apple Understanding and Analyzing Application Crash Reports
Overview Of iOS Crash Reporting Tools
Understanding Crashes and Crash Logs Video

链接:https://www.jianshu.com/p/e05498960209

收起阅读 »

如何构建优雅的ViewController

前言关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是...
继续阅读 »

前言

关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是否足够清晰。ViewController也从很大程度上反应一个程序员的真实水平,一个平庸的程序员他的ViewController永远是臃肿的、肥胖的,什么功能都可以往里面塞,不同功能间缺乏清晰的界限。而一个优秀的程序员它的ViewController显得如此优雅,让你产生一种竟不能修改一笔一画的感觉。

ViewController职责

1、UI 属性 和 布局
2、用户交互事件
3、用户交互事件处理和回调

用户交互事件处理: 通常会交给其他对象去处理

回调: 可以根据具体的设计模式和应用场景交给 ViewController 或者其他对象处理

而通常我们在阅读别人ViewController代码的时候,我们关注的是什么?

控件属性配置在哪里?
用户交互的入口位置在哪里?
用户交互会产生什么样的结果?(回调在哪里?)
所以从这个角度来说,这三个功能一开始就应该是被分离的,需要有清新明确的界限。因为谁都不希望自己在查找交互入口的时候 ,去阅读一堆控件冗长的控件配置代码, 更不愿意在一堆代码去慢慢理清整个用户交互的流程。 我们通常只关心我当前最关注的东西,当看到一堆无关的代码时,第一反应就是我想注释掉它。

基于协议分离UI属性的配置

protocol MFViewConfigurer {
var rootView: UIView { get }
var contentViews: [UIView] { get }
var contentViewsSettings: [() -> Void] { get }

func addSubViews()
func configureSubViewsProperty()
func configureSubViewsLayouts()

func initUI()
}

依赖这个协议就可以完成所有控件属性配置,然后通过extension protocol 大大减少重复代码,同时提高可读性

extension MFViewConfigurer {
func addSubViews() {
for element in contentViews {
if let rootView = rootView as? UIStackView {
rootView.addArrangedSubview(element)
} else {
rootView.addSubview(element)
}
}
}

func configureSubViewsProperty() {
for element in contentViewsSettings {
element()
}
}

func configureSubViewsLayouts() {
}

func initUI() {
addSubViews()
configureSubViewsProperty()
configureSubViewsLayouts()
}
}

这里 我将控件的添加和控件的配置分成两个函数addSubViews和configureSubViewsProperty, 因为在我的眼里函数就应该遵循单一职责这个概念:
addSubViews: 明确告诉阅读者,我这个控制器只有这些控件
configureSubViewsProperty: 明确告诉阅读者,控件的所有属性配置都在这里,想要修改属性请阅读这个函数

来看一个实例:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

// 初始化 UI
initUI()

// 绑定用户交互事件
bindEvent()

// 将ViewModel.value 绑定至控件
bindValueToUI()

}

// MARK: - UI configure

// MARK: - UI

extension MFWeatherViewController: MFViewConfigurer {
var contentViews: [UIView] { return [scrollView, cancelButton] }

var contentViewsSettings: [() -> Void] {
return [{
self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7)
self.scrollView.hiddenSubViews(isHidden: false)
}]
}

func configureSubViewsLayouts() {
cancelButton.snp.makeConstraints { make in
if #available(iOS 11, *) {
make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
} else {
make.top.equalTo(self.view.snp.top).offset(20)
}

make.left.equalTo(self.view).offset(20)
make.height.width.equalTo(30)
}

scrollView.snp.makeConstraints { make in
make.top.bottom.left.right.equalTo(self.view)
}
}

}


而对于UIView 这套协议同样适用

```Swift
// MFWeatherSummaryView
private override init(frame: CGRect) {
super.init(frame: frame)

initUI()
}


// MARK: - UI

extension MFWeatherSummaryView: MFViewConfigurer {
var rootView: UIView { return self }

var contentViews: [UIView] {
return [
cityLabel,
weatherSummaryLabel,
temperatureLabel,
weatherSummaryImageView,
]
}

var contentViewsSettings: [() -> Void] {
return [UIConfigure]
}

private func UIConfigure() {
backgroundColor = UIColor.clear
}

public func configureSubViewsLayouts() {
cityLabel.snp.makeConstraints { make in
make.top.centerX.equalTo(self)
make.bottom.equalTo(temperatureLabel.snp.top).offset(-10)
}

temperatureLabel.snp.makeConstraints { make in
make.top.equalTo(cityLabel.snp.bottom).offset(10)
make.right.equalTo(self.snp.centerX).offset(0)
make.bottom.equalTo(self)
}

weatherSummaryImageView.snp.makeConstraints { make in
make.left.equalTo(self.snp.centerX).offset(20)
make.bottom.equalTo(temperatureLabel.snp.lastBaseline)
make.top.equalTo(weatherSummaryLabel.snp.bottom).offset(5)
make.height.equalTo(weatherSummaryImageView.snp.width).multipliedBy(61.0 / 69.0)
}

weatherSummaryLabel.snp.makeConstraints { make in
make.top.equalTo(temperatureLabel).offset(20)
make.centerX.equalTo(weatherSummaryImageView)
make.bottom.equalTo(weatherSummaryImageView.snp.top).offset(-5)
}
}
}

由于我使用的是MVVM模式,所以viewDidLoad 和MVC模式还是有些区别,如果是MVC可能就是这样

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

// 初始化 UI
initUI()

// 用户交互事件入口
addEvents()


}

// MARK: callBack
......

由于MVC的回调模式很难统一,有Delegate, closure, notification 等等,所以回调通常会散落在控制器各个角落。最好加个MARK flag, 尽量收集在同一个区域中, 同时对于每个回调加上必要的注释:

1、由哪种操作触发
2、会导致什么后果
3、最终会留下哪里

所以从这个角度来说UITableViewDataSource 和 UITableViewDelegate 完全是两种不一样的行为, 一个是 configure UI , 一个是 control behavior , 所以不要在把这两个东西写一块了, 真的很难看。

总结

基于职责对代码进行分割,这样会让你的代码变得更加优雅简洁,会大大减少一些万金油代码的出现,减少阅读代码的成本也是我们优化的一个方向,比较谁都不想因为混乱的代码影响自己的心情

链接:https://www.jianshu.com/p/266cbca1439c

收起阅读 »

OC对象的本质(上) —— OC对象的底层实现原理

一个NSObject对象占用多少内存?Objective-C的本质平时我们编写的OC代码,底层实现都是C/C++代码Objective-C --> C/C++ --> 汇编语言 --> 机器码所以Objective-C的面向对象都是基于C/C...
继续阅读 »

一个NSObject对象占用多少内存?

Objective-C的本质
平时我们编写的OC代码,底层实现都是C/C++代码

Objective-C --> C/C++ --> 汇编语言 --> 机器码

所以Objective-C的面向对象都是基于C/C++的数据结构实现的,所以我们可以将Objective-C代码转换成C/C++代码,来研究OC对象的本质。

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}

我们在main函数里面定义一个简单对象,然后通过 clang -rewrite-objc main.m -o main.cpp命令,将main.m文件进行重写,即可转换出对应的C/C++代码。但是可以看到一个问题,就是转换出来的文件过长,将近10w行。


因为不同平台支持的代码不同(Windows/Mac/iOS),那么同样一句OC代码,经过编译,转成C/C++代码,以及最终的汇编码,是不一样的,汇编指令严重依赖平台环境。
我们当前关注iOS开发,所以,我们只需要生成iOS支持的C/C++代码。因此,可以使用如下命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <输出的cpp文件>
-sdk:指定sdk
-arch:指定机器cpu架构(模拟器-i386、32bit、64bit-arm64 )
如果需要链接其他框架,使用-framework参数,比如-framework UIKit
一般我们手机都已经普及arm64,所以这里的架构参数用arm64,生成的cpp代码如下



接下来,我们查看一下main_arm64.cpp源文件,如果熟悉这个文件,你将会发现这么一个结构体

struct NSObject_IMPL {
Class isa;
};

我们再来对比看一下NSObject头文件的定义

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end

简化一下,就是

@interface NSObject  {
Class isa ;
}
@end

是不是猜到点什么了?没错,struct NSObject_IMPL其实就是NSObject的底层结构,或者说底层实现。换个角度理解,可以说C/C++的结构体类型支撑了OC的面相对象。

点进Class的定义,我们可以看到 是typedef struct objc_class *Class;

Class isa; 等价于 struct objc_class *isa;

所以NSObject对象内部就是放了一个名叫isa的指针,指向了一个结构体 struct objc_class。

总结一:一个OC对象在内存中是如何布局的?


猜想:NSObject对象的底层就是一个包含了一个指针的结构体,那么它的大小是不是就是8字节(64位下指针类型占8个字节)?
为了验证猜想,我们需要借助runtime提供的一些工具,导入runtime头文件,class_getInstanceSize ()方法可以计算一个类的实例对象所实际需要的的空间大小

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject对象的大小:%zd",size);
}
return 0;
}

结果是


完美验证,it's over,let's go home!


等等,就这么简单?确定吗?答案是否定的~~~
介绍另一个库#import <malloc/malloc.h>,其下有个方法 malloc_size(),该函数的参数是一个指针,可以计算所传入指针 所指向内存空间的大小。我们来用一下

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject实例对象的大小:%zd",size);
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);
}
return 0;
}

结果是16,如何解释呢?


想要真正弄清楚其中的缘由,就需要去苹果官方的开源代码里面去一探究竟了。苹果的开源代请看这里。
先看一下class_getInstanceSize的实现。我们需要进到objc4/文件里面下载一份最新的源码,我当前最新的版本是objc4-750.1.tar.gz。下载解压之后,打开工程,就可以查看runtime的实现源码。
搜索class_getInstanceSize找到实现代码

size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}

再点进alignedInstanceSize方法的实现

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

可以看到该方法的注释说明Class's ivar size rounded up to a pointer-size boundary.,意思就是获得类的成员变量的大小,其实也就是计算类所对应的底层结构体的大小,注意后面的这个rounded up to a pointer-size boundary指的是系统在为类的结构体分配内存时所进行的内存对齐,要以一个指针的长度作为对齐系数,64位系统指针长度(字长)是8个字节,那么返回的结果肯定是8的最小整数倍。为什么需要用指针长度作为对齐系数呢?因为类所对应的结构体,在头部的肯定是一个isa指针,所以指针肯定是该结构体中最大的基本数据类型,所以根据结构体的内存对齐规则,才做此设定。如果对这里有疑惑的话,请先复习一下有关内存对齐的知识,便一目了然了。
所以class_getInstanceSize方法,可以帮我们获取一个类的的实例对象所对应的结构体的实际大小。

我们再从alloc方法探究一下,alloc方法里面实际上是AllocWithZone方法,我们在objc源码工程里面搜索一下,可以在Object.mm文件里面找到一个_objc_rootAllocWithZone方法。

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;

#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif

if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}

再点进里面的关键方法class_createInstance的实现看一下

id  class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}

继续点进_class_createInstanceFromZone方法

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;

assert(cls->isRealized());

// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();

size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;

// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}

if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}

return obj;
}

这个方法有点长,有时分析一个方法,不要过分拘泥细节,先针对我们寻找的问题,找到关键点,像这个比较长的方法,我们知道,它的主要功能就是创建一个实例,为其开辟内存空间,我们可以发现中间的这句代码obj = (id)calloc(1, size);,是在分配内存,这里的size是需要分配的内存的大小,那这句应该就是为对象开辟内存的核心代码,再看它里面的参数size,我们能在上两行代码中找到size_t size = cls->instanceSize(extraBytes);,于是我们继续点进instanceSize看看

size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

翻译一下这句注//CF requires all objects be at least 16 bytes.我们就明白了,CF作出了硬性的规定:当创建一个实例对象的时候,为其分配的空间不能小于16个字节,为什么这么规定呢,我个人目前的理解是这可能就相当于一种开发规范,或者对于CF框架内部的一些实现提供的规范。
这个size_t instanceSize(size_t extraBytes)返回的字节数,其实就是为 为一个类创建实例对象所需要分配的内存空间。这里我们的NSObject类创建一个实例对象,就分配了16个字节。
我们在点进上面代码中的alignedInstanceSize方法

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

这不就是我们上面分析class_getInstanceSize方法里面看到的那个alignedInstanceSize嘛。


总结二:class_getInstanceSize&malloc_size的区别

class_getInstanceSize:获取一个objc类的实例的实际大小,这个大小可以理解为创建这个实例对象至少需要的空间(系统实际为这个对象分配的空间可能会比这个大,这是出于系统内存对齐的原因)。
malloc_size:得到一个指针所指向的内存空间的大小。我们的OC对象就是一个指针,利用这个函数,我们可以得到该对象所占用的内存大小,也就是系统为这个对象(指针)所指向对象所实际分配的内存大小。
sizeof():获取一个类型或者变量所占用的存储空间,这是一个运算符。
[NSObject alloc]之后,系统为其分配了16个字节的内存,最终obj对象(也就是struct NSObject_IMPL结构体),实际使用了其中的8个字节内存,(也就是其内部的那个isa指针所用的8个字节,这里我们是在64位系统为前提下来说的)
关于运算符和函数的一些对比理解

函数在编译完之后,是可以在程序运行阶段被调用的,有调用行为的发生
运算符则是在编译按一刻,直接被替换成运算后的结果常量,跟宏定义有些类似,不存在调用的行为,所以效率非常高

更为复杂的自定义类

我们开发中会自定义各种各样的类,基本上都是NSObject的子类。更为复杂的子类对象的内存布局又是如何的呢?我们新建一个NSObject的子类Student,并为其增加一些成员变量

@interface Student : NSObject
{
@public
int _age;
int _no;
}

@end

@implementation Student

@end

使用我们之前介绍过的方法,查看一下这个类的底层实现代码

struct NSObject_IMPL {
Class isa;
};

struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;

};

我们发现其实Student的底层结构里,包含了它的成员变量,还有一个NSObject_IMPL结构体变量,也就是它的父类的结构体。根据我们上面的总结,NSObject_IMPL结构体需要的空间是8字节,但是系统给NSObject对象实际分配的内存是16字节,那么这里Student的底层结构体里面的成员变量NSObject_IMPL应该会得到多少的内存分配呢?我们验证一下。

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
//获取`NSObject`类的实例对象的成员变量所占用的大小
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject实例对象的大小:%zd",size);
//获取obj所指向的内存空间的大小
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);

Student * std = [[Student alloc]init];
size_t size3 = class_getInstanceSize([Student class]);
NSLog(@"Student实例对象的大小:%zd",size3);
size_t size4 = malloc_size((__bridge const void *)(std));
NSLog(@"对象std所指向的的内存空间大小:%zd",size4);
}
return 0;
}


貌似是对的了,但是为什么用malloc_size得到std所被分配的内存是32?再来一发试试

@interface Student : NSObject
{

@public
//父类的isa还会占用8个字节
int _age;//4字节
int _no;//4字节
int _grade;//4字节
int *p1;//8字节
int *p2;//8字节
}

Student结构体所有成员变量所需要的总空间为 36字节,根据内存对齐原则,最后结构体所需要的空间应该是8的倍数,那应该就是40,我们看一下结果


从结果看没错,但是同时也发现了一个规律,随着std对象成员变量的增加,系统为Student对象std分配的内存空间总是以16的倍数增加(16~32~48......),我们之前分析源码好像没看到有做这个设定


其实上面这个方法只是可以用来计算一个结构体对象所实际需要的内存大小。 [update]其实instanceSize()-->alignedInstanceSize()只是可以用来计算一个结构体对象理论上(按照内存对其规则)所需要分配的内存大小。

真正给实例对象完成分配内存操作的是下面这个方法calloc()


这个方法位于苹果源码的libmalloc文件夹中。但是里面的代码再往下深究,介于我目前的知识储备以及专业出身(数学专业),还是困难比较大。好在从一些大神那里得到了指点。
刚才文章开始,我们讨论到了结构体的内存对齐,这是针对数据结构而言的。从系统层面来说,就以苹果系统而言,出于对内存管理和访问效率最优化的需要,会实现在内存中规划出很多块,这些块有大有小,但都是16的倍数,比如有的是32,有的是48,在libmalloc源码的nano_zone.h里面有这么一段代码

#define NANO_MAX_SIZE    256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

NANO是源码库里面的其中一种内存分配方法,类似的还有frozen、legacy、magazine、purgeable。


这些是苹果基于各种场景优化需求而设定的对应的内存管理相关的库,暂时不用对其过分解读。
上面的NANO_MAX_SIZE解释中有个词Buckets sized,就是苹果事先规划好的内存块的大小要求,针对nano,内存块都被设定成16的倍数,并且最大值是256。举个例子,如果一个对象结构体需要46个字节,那么系统会找一块48字节的内存块分配给它用,如果另一个结构体需要58个字节,那么系统会找一块64字节的内存块分配给它用。
到这里,应该就可以基本上解释清楚,为什么刚才student结构需要40个字节的时候,被分配到的内存大小确实48个字节。至此,针对一个NSObject对象占用内存的问题,以及延伸出来的内存布局,以及其子类的占内存问题,应该就都可以得到解答了。

OC对象的本质(上):OC对象的底层实现
OC对象的本质(中):OC对象的分类
OC对象的本质(下):详解isa&superclass指针

面试题解答

一个NSObject对象占用多少内存?
1)系统分配了16字节给NSObject对象(通过malloc_size函数可以获得)
2)NSObject对象内部只使用了8个字节的空间,用来存放isa指针变量(64位系统下,可以通过class_getInstanceSize函数获得)

链接:https://www.jianshu.com/p/1bf78e1b3594

收起阅读 »

iOS内存(Heap堆内存 && Anonymous VM 虚拟内存) 分析和理解

在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的...
继续阅读 »

在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的时候 我们是否需要
去了解它

前言所需要的图片(如下图)


1) 什么是堆
堆是一种完全结构的二叉树 堆与二叉树的理解

堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。堆里面一般 放的是静态数据,比如static的数据和字符串常量等,资源加载后一般也放在堆里面。一个进程的所有线程共有这些堆 ,所以对堆的操作要考虑同步和互斥的问题。程序里面编译后的数据段都是堆的一部分。

— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456{row.content}在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);//分配得来的10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456{row.content}放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。}

堆:首 先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结 点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才 能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出分配方式:堆都是动态分配的,没有静态分配的堆。

1.1) 堆上消耗的内存


1、View 函数的调用
2、注册通知
3、抛出通知
4、view 的布局
5、函数代码的执行
6、sqlite 数据库的创建
7、向字典中增加对象


8、等等

都需要消耗内存, 上面的代码都是程序员创建的, 程序员去控制堆的内存

1.2) 堆上的内存是否释放

1.2.1) 已经释放的例子:

点击步骤1)箭头


查看步骤2)箭头


步骤2) 箭头中有 free 函数, 可以看出, 这个对象 已经被释放

1.2.2) 堆上内存不释放的例子:


上图中箭头执行的地方 没有free 函数 说明 这个对象已经释放

2) Anonymous VM

2.1) 苹果官方文档对虚拟内存的解释

更小的内存消耗不仅可以减少内存, 还可以减少cpu 的时间
我们可能会看到这样的情况, All Heap Allocations 是程序真实的内存分配情况,All Anonymous VM则是系统为程序分配的虚拟内存,为的就是当程序有需要的时候,能够及时为程序提供足够的内存空间,而不会现用现创建

Anonymous VM内存是虚拟内存
、All Anonymous VM。我们无法控制Anonymous VM部分 ,(更新,其实还是可以优化 比如图片绘制相关 详情参见iOS内存探究,需要对虚拟内存熟悉 才能优化)

2.2) 问题: 我们需要关注Anonymous VM 内存吗 ?
问答连接
Should you focus on the Live Bytes column for heap allocations or anonymous VM? Focus on the heap allocations because your app has more control over heap allocations. Most of the memory allocations your app makes are heap allocations.

The VM in anonymous VM stands for virtual memory. When your app launches, the operating system reserves a block of virtual memory for your application. This block is usually much larger than the amount of memory your app needs. When your app allocates memory, the operating system allocates the memory from the block it reserved.

Remember the second sentence in the previous paragraph. The operating system determines the size of the virtual memory block, not your app. That’s why you should focus on the heap allocations instead of anonymous VM. Your app has no control over the size of the anonymous VM.

2.3) 不需要关注 Anonymous VM
我们应该关注堆内存, 因为我们对堆内存有更大的掌控, 大部分我们在app的内存分配是堆内存

VM 在匿名空间中代表的是虚拟内存, 当你的app启动的时候, 操作系统为你的应用程序分配内存, 这个分配的虚拟内存一般比你的app需要的内存大很多,

操作系统决定虚拟内存的分配, 而不是你的app, 这就是你为什么要集中精力处理堆内存, 你的app 对虚拟内存没有掌控力

2.4) 虚拟内存过大 (未解之谜) 如果知道结果请评论留言, 多谢


CGBitmapContextCreateImage 函数会导致虚拟内存过大 ,并且还不释放, 用法未发现问题

CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
UIImage *result = [UIImage imageWithCGImage:alphaMaskImage scale:image.scale orientation:image.imageOrientation];
CGImageRelease(alphaMaskImage);
CGContextRelease(alphaOnlyContext);
return result;

参考文献

iOS中的堆(heap)和栈(stack)的理解

苹果虚拟内存的官方文档

转自:https://www.jianshu.com/p/dffd5c24dc9a

收起阅读 »

runtime 小结

OC被称之为动态运行时语言,最主要的原因就是因为两个特性,一个是运行时也就是runtime,一个是多态。runtimeruntime又叫运行时,是一套底层的c语言api,其为iOS内部核心之一。OC是动态运行时语言,它会将一些工作放在代码运行时去处理,而非编译...
继续阅读 »

OC被称之为动态运行时语言,最主要的原因就是因为两个特性,一个是运行时也就是runtime,一个是多态。

runtime

runtime又叫运行时,是一套底层的c语言api,其为iOS内部核心之一。OC是动态运行时语言,它会将一些工作放在代码运行时去处理,而非编译时,比如动态的遍历属性和方法,动态的添加属性和方法,动态的修改属性和方法等。

了解runtime,首先要先了解它的核心--消息传递。

消息传递

消息直到运行时才会与方法实践绑定起来。
一个实例对象调用实例方法,像这样[obj doSomething];,编译器转成消息发送objc_msgSend(obj, @selector(doSomething),,);,

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

runtime时的运行流程如下:

1、首先通过调用对象的isa找到class;
2、在class的method_list里面找该方法,这里如果是实例对象,则去实例对象的类的方法列表中找,如果是类对象调用类方法,则去元类的方法列表中找,具体下面解释;
3、如果class里没找到,继续往它的superClass里找;
4、一旦找到doSomething这个函数,就去执行它的实现IMP;

下面介绍一下对象(object),类(class),方法(method)的结构体:

//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

类对象(objc_class)

OC中类是Class来表示,实际上是一个指向objc_class结构体的指针。

//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

观察一下对象的结构体和类对象的结构体,可以看到里面都有一个isa指针,对象的isa指针指向类,类的isa指针指向元类(metaClass),元类也是类,元类的isa指针最终指向根元类(rootMetaClass),根元类的isa指针指向自己,最终形成一个闭环。



可以看到类结构体中有一个methodLists,也就解释了上文提到的成员方法记录在class method-list中,类方法记录在metaClass中。即Instance-object的信息记录在class-object中,而class-object的信息记录在meta-class中。

结构体中有一个ivars指针指向objc_ivar_list结构体,是该类的属性列表,因为编译器编译顺序是父类,子类,分类,所以这也就是为什么分类category不能添加属性,因为类在编译的时候已经注册在runtime中了,属性列表objc_ivar_list和instance_size内存大小都已经确定了,同时runtime会调用class_setIvarLayout和class_setWeakIvarLayout来处理strong和weak引用。可以通过runtime的关联属性来给分类添加属性(原因是category结构体中有一个instanceProperties,下文会讲到)。因为编译顺序是父类,子类,分类,所以消息遍历的顺序是分类,子类,父类,先进后出。

objc_cache结构体,是一个很有用的方法缓存,把经常调用的方法缓存下来,提高遍历效率。将方法的method_name作为key,method_imp作为value保存下来。

Method(objc_method)

结构体如下:

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

可以看到里面有一个SEL和IMP,这里讲一下两者的区别。

SEL是selector的OC表示,数据结构为:typedef struct objc_selector *SEL;是个映射到方法的c字符串;不同于函数指针,函数指针直接保存了方法地址,SEL只是一个编号;也是objc_cache中的key。

ps.这也带来了一个弊端,函数重载不适用,因为函数重载是方法名相同,参数名不同,但是SEL只记了方法名,没有参数,所以没法区分不同的method。

ps.在不同的类中,相同的方法名,方法选择器也是相同的。

IMP是函数指针,数据结构为typedef id (IMP)(id,SEL,**);保存了方法地址,由编译器绑定生成,最终方法执行哪段代码由IMP决定。IMP指向了方法的实现,一组id和SEL可以确定唯一的实现。

有了SEL这个中间过程,我们可以对一个编号和方法实现做些中间操作,也就是说我们一个SEL可以指向不同的函数指针,这样就可以完成一个方法名在不同的时候执行不同的函数体。另外可以将SEL作为参数传递给不同的类执行,也就是我们某些业务只知道方法名但需要根据不同的情况让不同的类执行。个人理解,消息转发就是利用了这个中间过程。

runtime是如何通过selector找到对应的IMP的?
上文讲了类对象中有实例方法的列表,元类对象中有类方法的列表,列表中记录着方法的名称,参数和实现。而selector本质就是方法名称也就是SEL,通过方法名称可以在列表中找到方法实现。

在寻找IMP的时候,runtime提供了两种方法:

1、IMP class_getMethodImplementation(Class cls, SEL name);
2、IMP method_getImplementation(Method m);
对于第一种方法来说,实例方法和类方法都是调用这个方法来找到IMP,不同的是第一个参数,实例方法传的参数是[obj class];,而类方法传的参数是objc_getMetaClass("obj");
对于第二种方法来说,传入的参数只有Method,区分类方法和实例方法在于封装Method的函数,类方法:Method class_getClassMethod(Class cls, SEL name);实例方法:Method class_getInstanceMethod(Class cls, SEL name);

Category(objc_category)

category是表示指向分类的一个结构体指针,结构体如下:

struct category_t { 
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的结构体可以看出,分类category可以添加实例方法,类方法,协议,以及通过关联对象添加属性,不可以添加成员变量。

runtime消息转发

前文讲到,到一个方法被执行,也就是发送消息,会去相关的方法列表中寻找对应的方法实现IMP,如果一直到根类都没找到就会进入到消息转发阶段,下面介绍一下消息转发的最后三个集会。

1、动态方法解析
2、备用接收者
3、完整消息转发

动态方法解析

首先,当消息传递到根类都找不到方法实现时,运行时runtime会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,并返回了yes,那运行时就会重新走一步消息发送的过程。

实现一个动态方法解析的例子如下:

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}

可以看到虽然没有实现foo这个函数,但是我们通过class_addMethod动态的添加了一个新的函数实现fooMethod,并返回了yes。

如果返回no,就会进入下一步,- forwardingTargetForSelector:。

备用接收者

实现的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}

return [super forwardingTargetForSelector:aSelector];
}

@end

可以看到我们通过-forwardingTargetForSelector:方法将当前viewController的foo函数转发给了Person的foo函数去执行了。

如果在这一步还不能处理未知的消息,则进入下一步完整消息转发。

完整消息转发

首先会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,runtime会发出-doseNotRecognizeSelector消息,程序会挂掉;如果返回一个函数标签,runtime就会创建一个NSInvocation对象,并发送-forwardInvocation:消息给目标对象。

实现例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}

return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;

Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}

}

@end

通过签名,runtime生成了一个anInvocation对象,发送给了forwardInvocation:,我们再forwardInvocation:里面让Person对象去执行了foo函数。

以上就是runtime的三次函数转发流程。

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

链接:https://www.jianshu.com/p/4ae997a6c599

收起阅读 »

解决集成EaseIMKit源码后没有图片的问题

经过上一篇文章如何集成环信EaseIMKit和EaseCallKit源码?之后,我们在实际使用时,会发现一个非常大的问题:就是图片都加载不出来了.这里我们可以借用easeCallKit的实现方式将EaseCallKit内的文件资源包复制一份,修改一下名,然后打...
继续阅读 »

经过上一篇文章如何集成环信EaseIMKit和EaseCallKit源码?之后,我们在实际使用时,会发现一个非常大的问题:

就是图片都加载不出来了.

这里我们可以借用easeCallKit的实现方式

将EaseCallKit内的文件资源包复制一份,修改一下名,然后打开包,将里面的图片都替换掉,这是一个方法.

但上述方法依然有问题,涉及到自动加载倍图问题.

解决加载倍图也是有方法的,不过都太麻烦了,我们采用一个比较笨的方法.

直接将EaseIMKit内的图片拖进项目内

就像这样:



同时,我们还需要修改加载图片的方式,项目中直接搜索:

EaseIMKit.framework

发现总共三个地方:







至此已完成.

另外我们如果使用官方demo中的代码,直接拖文件进来时,会发现好多报错.这里直接说明原因,图片重复了,搜索报错的图片名,直接保留一份即可.

最后,再次强调:

我们是可以采用EaseCallKit加载图片方式的,但此方式有一个非常大的问题:倍图

(正因为尝试过并失败了,所以放弃了)

如果我们直接采用EaseCallKit加载图片方式,不做任何处理,会自动加载一倍图,而且如果没有一倍图也不会自动加载二倍图和三倍图,我们需要手动判断和手动加载图片名后缀,比较麻烦,所以这里就偷个懒,采用上述方法来解决加载图片问题.


收起阅读 »

runloop 小结

OC的两大核心runtime和runlooprunloop简介runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoo...
继续阅读 »

OC的两大核心runtime和runloop

runloop简介

runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。

OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoopRef.
CFRunLoopRef是CoreFoundation框架提供的纯c的api,所有这些api都是线程安全的。
NSRunLoop是对CFRunLoopRef的OC封装,提供了面向对象的api,这些api不是线程安全的。

runloop和线程的关系

首先,iOS提供了两个线程对象pthread_t和NSThread,这两个线程对象不能互相转换,但是一一对应。比如:可以通过pthread_main_thread_np()和[NSThread mainThread]获取主线程;也可以通过pthread_self()和[NSThread currentThread]获取当前线程。CFRunLoopRef是基于pthread来管理的。

苹果不允许直接创建runloop,它只有两个获取的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。这两个函数的内部实现大致是:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}

/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

OSSpinLockUnLock(&loopsLock);
return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

可以看出来,线程和RunLoop是一一对应的,保存在一个全局的CFMutableDictionaryRef,key为pthread,value为runloop。线程刚创建时没有runloop,如果你没有主动获取,那它一直不会有。当你第一次获取runloop时,创建runloop,当线程结束时,runloop销毁。

主线程的runloop默认开启,程序启动时,main方法,applicationMain方法内开启runloop。

runloop的类
在Core Foundation框架中提供了五个类关于runloop:

1、CFRunLoopRef
2、CFRunLoopModeRef
3、CFRunLoopSourceRef
4、CFRunLoopTimerRef
5、CFRunLoopObserverRef

它们的关系如下:


一个runloop包含若干个Mode,一个Mode又包含若干个Source/Timer/Observer。每次调用runloop的主函数时,只能指定其中一个mode,如果想切换mode,需要退出当前runloop,再重新指定一个mode进入。这样的好处是,不同组的Source/Timer/Observer互不影响。

CFRunLoopSourceRef是事件产生的地方。Source有两个版本,Source 0(非端口Source)和Source 1(端口Source)。

1、Source 0 只包含一个回调函数指针,它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(Source 0)将该source标记为待处理,然后手动调用CFRunLoopWakeUp()唤醒runloop,处理该事件。
2、Source 1 包含一个mach port(端口)和一个回调的函数指针,被用于通过内核和其他线程相互发送消息。这种source能主动唤醒runloop。
CFRunLoopTimerRef 是基于时间的触发器。其包含一个时间长度和一个回调的函数指针。当其加入到runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行这个回调。

CFRunLoopObserverRef 是观察者,每个Observer都包含一个回调,当runloop的状态发生改变时,观察者可以通过回调接受到这个变化。可以接受到的状态有如下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

上述的Source/Timer/Observer被统称为一个mode item,一个item可以被加入多个mode,但一个item被重复加入同一个mode,是没有效果的。如果一个mode中一个item都没有,则runloop会自动退出。

runloop的mode

CFRunLoopMode和CFRunLoop的结构大致如下

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

runloop的mode包含:

1、NSDefaultRunLoopMode:默认的mode;
2、UITrackingRunLoopMode:跟踪用户触摸事件的mode,如UIScrollView的上下滚动;
3、NSRunLoopCommonModes:模式集合,将一组item关联到这个模式集合上,等于将这个item关联到这个集合下的所有模式上;
4、自定义Mode。

这里主要解释一下NSRunLoopCommonModes,这个模式集合。
默认NSDefaultRunLoopMode和UITrackingRunLoopMode都是包含在这个模式集合内的,当然也可以自定义一个mode,通过CFRunLoopAddCommonMode添加到这个模式集合中。

应用场景举例:

当一个控制器里有一个UIScrollview和一个NSTimer,UIScrollView不滚动的时候,runloop运行在NSDefaultRunLoopMode下,此时Timer会得到回调,但当UIScrollView滑动时,会将mode切换成UITrackingRunLoopMode,此时Timer得不到回调。一个解决办法就是将这个NSTimer分别绑定到NSDefaultRunLoopMode和UITrackingRunLoopMode,另一个解决办法是将这个NSTimer绑定到NSRunLoopCommonModes,两种方法都能使NSTimer在两个模式下都能得到回调。

ps.让runloop运行在NSRunLoopCommonModes模式下是没有意思的,因为runloop一个时间只能运行在一个模式下。

端口Source通信的步骤
demo如下:

- (void)testDemo3
{
//声明两个端口 随便怎么写创建方法,返回的总是一个NSMachPort实例
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
//设置线程的端口的代理回调为自己
threadPort.delegate = self;

//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

dispatch_async(dispatch_get_global_queue(0, 0), ^{

//添加一个Port
[[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

});

NSString *s1 = @"hello";

NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
//components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

});

}

//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message
{

NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);

//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];

NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);

// NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
// NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];

}

声明两个端口,sendPort,receivePort,设置receivePort的代理,分别将sendPort和receivePort绑定到两个线程的自己的runloop上,然后回到发送线程用接收端口发送数据([threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0]; from参数标注从发送端口发出),注意这里发送的数据格式为array,内容格式只能为NSPort或者NSData,在代理方法- (void)handlePortMessage:(id)message中接收数据;

RunLoop的内部实现


内部代码整理,不想看可以跳过,看下方总结:

/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息。
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

runloop的运行逻辑:

1、通知监听者,即将进入runloop;
2、通知监听者,将要处理Timer;
3、通知监听者,将要处理Source0(非端口InputSource);
4、处理Source0;
5、如果有Source1,跳到第9步;
6、通知监听者,线程即将进入休眠;
7、runloop进入休眠,等待唤醒;
   1.source0;
   2.Timer启动;
   3.外部手动唤醒
8、通知监听者,线程将被唤醒;
9、处理未处理的任务;
   1.如果用户定义的定时器任务启动,处理定时器任务并重启runloop,进入步骤2;
   2.如果输入源启动,传递相应的消息;
   3.如果runloop被显示唤醒,且没有超过设置的时间,重启runloop,进入步骤2;
10、通知监听者,runloop结束。
   1.runloop结束,没有timer或者没有source;
   2.runloop被停止,使用CFRunloopStop停止Runloop;
   3.runloop超时;
   4.runloop处理完事件。

苹果用runloop实现的功能

1、自动释放池,在主程序启动时,再即将进入runloop的时候会执行autoreleasepush(),新建一个autoreleasePoolPage,同时push一个哨兵对象到这个page中;当runloop进入休眠模式时,会执行autoreleasepop(),释放旧池,同时autoreleasepush(),创建新池;当runloop退出时,清空自动释放池。

2、定时器NSTimer实际上就是CFRunloopTimerRef。

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

链接:https://www.jianshu.com/p/8fdda9f64459

收起阅读 »

如何集成环信EaseIMKit和EaseCallKit源码?

EaseIMKit是一个基于环信sdk的UI库,封装了IM功能常用的控件、fragment等等。官网下载源码EaseCallKit源码EaseIMKit源码第二步 & 第三步整理一份路径 & 整理EaseCallKit文件及文件夹 ...
继续阅读 »

EaseIMKit是一个基于环信sdk的UI库,封装了IM功能常用的控件、fragment等等。

下面给大家分享一下如何引入EaseIMKit源码

第一步

官网下载源码

源码从这里找:环信开源GitHub


EaseCallKit源码



EaseIMKit源码


第二步 & 第三步

整理一份路径 & 整理EaseCallKit文件及文件夹
[注意!!!注意!!!注意!!! 上下两个文件夹窗口内有同名文件夹,因为就是同一个文件夹.这里专门开了两个窗口,为了更加直观!!!比如"00刚下载的源码/01整理之后的内容/02展示项目"这三个文件夹上下窗口的文件夹是同一个路径的文件夹]



第四步
整理EaseIMKit文件及文件夹



第五步
修改两个文件
(EaseIMKit.podspec & EaseCallKit.podspec)
(两个文件内容:文章末尾有文本内容可直接复制)



第六步
创建项目
这里创建项目名叫EaseSourceCode

将整理好内容的源码放入项目文件夹内,创建podfile,podfile内容如下

(podfile内容:文章末尾有文本内容可直接复制)



第七步
pod install后运行项目
先command+b编译,再引入头文件



第八步

最后集成完成,你会发现没有图片!下一篇文章将会讲解如何将图片加载出来.

附:

EaseIMKit.podspec内容如下

#=====================================

Pod::Spec.new do |s|

  s.name = 'EaseIMKit'

  s.version = '3.8.1.1'

  s.summary = 'easemob im sdk UIKit'

  s.description = <<-DESC

        EaseMob YES!!!

  DESC

  s.homepage = 'http://docs-im.easemob.com/im/ios/other/easeimkit'

  s.license          = 'MIT'

  s.platform = :ios, '10.0'

  s.author = { 'easemob' => 'dev@easemob.com' }

  s.source = { :git => 'http://XXX/EaseIMKit.git', :tag => s.version.to_s }

  s.frameworks = 'UIKit'

  s.libraries = 'stdc++'

  s.source_files = 'Class/**/*.{h,m,mm}'

  s.requires_arc = true

  s.resources = 'Class/EaseIMImage.bundle'

  s.static_framework = true

  s.dependency 'EMVoiceConvert', '~> 0.1.0'

  s.dependency 'HyphenateChat'

end

#=====================================

EaseCallKit.podspec内容如下

#=====================================

Pod::Spec.new do |s|

    s.name            ='EaseCallKit'

    s.version          ='3.8.1.1'

    s.summary          ='A UI framework with video and audio call'

    s.description      = <<-DESC

        EaseMob YES!!!

    DESC

    s.license          ='MIT'

    s.homepage ='https://www.easemob.com'

    s.author          = {'easemob'=>'dev@easemob.com'}

    s.source          = { :git =>'http://XXX/EaseCallKit.git', :tag => s.version.to_s }

    s.frameworks ='UIKit'

    s.libraries ='stdc++'

    s.ios.deployment_target ='9.0'

    s.source_files ='Classes/**/*.{h,m}'

    s.requires_arc =true

    s.resources ='Assets/EaseCall.bundle'

    s.dependency'HyphenateChat'

    s.dependency'Masonry'

    s.dependency'AgoraRtcEngine_iOS'

    s.dependency'SDWebImage'

end

#=====================================

podfile文件内容如下

#=====================================

# platform :ios, '9.0'

use_frameworks!

target 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' do

    pod'MBProgressHUD'

    pod'SDWebImage'

    pod'Masonry'

    pod'MJRefresh'

    pod'HyphenateChat'

    pod'AgoraRtcEngine_iOS' 

    pod'EaseIMKit', :path => './localPodsLibrary/EaseIMKit'

    pod'EaseCallKit',  :path =>'./localPodsLibrary/EaseCallKit'


end

#=====================================

下一篇:

解决集成EaseIMKit源码后没有图片的问题

收起阅读 »

iOS年度盛会 --- iOS 15新增8大更新

各位果粉们早上好,相信不少果粉和小编一样,熬夜看完了苹果这次WWDC开发者大会。看完发布会的第一感受--就这?这可能是近几年来最枯燥无味的一场开发者大会了,要不是以为有“one more thing...”,估计小编看到一半就睡着了。开个玩笑,虽然今年的WWD...
继续阅读 »
各位果粉们早上好,相信不少果粉和小编一样,熬夜看完了苹果这次WWDC开发者大会。看完发布会的第一感受--就这?这可能是近几年来最枯燥无味的一场开发者大会了,要不是以为有“one more thing...”,估计小编看到一半就睡着了。
开个玩笑,虽然今年的WWDC大会可能没那么精彩,但苹果还是用了近两小时的时间向我们介绍了iOS 15、iPadOS 15、 watchOS 8、tvOS 15以及MacOS Monterey系统,没有one more thing...,没有新硬件发布!


1、FaceTime视频通话升级
言归正传,接下来就给大家分享一下iOS 15都加入了哪些新功能,首先介绍的是iOS 15系统升级了FaceTime视频通话,包括加入了空间音频的支持、人声增强、人像模式背景虚化、以及第三方设备支持通过链接打开FaceTime等等。

当你使用FaceTime进行通话时,还能给一起视频的小伙伴们分享视频、歌曲。让用户可以在视频的同时,还能一起同步播放视频、歌曲。支持共享的视频包括迪士尼、NBA、HBO以及Tik Tok等知名视频平台。

2、新增「与你共享」功能
为了方便用户共享更多内容,苹果在iOS 15中加入了“与你共享”新功能,首批支持的的App包括照片、音乐、Safari浏览器、播客等等。

3、通知中心升级
iOS 15对通知中心也进行了升级,通知中心图标将更大,让用户能更轻松识别通知来源。不仅如此,iOS 15中还引入了“通知摘要”功能,用户可以自己设置某一个App的通知时间,且通知仅显示重要通知内容,过滤掉无关信息,以保证用户不会错过这条提示。

4、「专注模式」来了
另外,iOS 15还加入了「专注模式」,包括勿扰模式、工作模式、个人模式以及睡眠模式。每个状态可以设置不同的显示通知,并可与其他设备同步。

5、照片新增「实况文本」
接下来就是照片的升级,iOS 15中为照片加入了「实况文本」功能,在这个功能的帮助下,iPhone相机可自动扫描并识别文字,用户可以长按进行选择、复制与粘贴。毫不夸张的说,这个可能是本次iOS 15更新最实用的功能之一了~

得益于神经网络学习的加持,「实况文本」可识别iPhone中所有照片的文字,支持包括英语和中文等7种文字识别,用户可直接搜索照片中的文字找到这张照片。

6、iPhone门禁卡也来了
苹果在iOS 15中加入了钱包钥匙功能,这些钥匙包括公司徽章、酒店房间钥匙和家庭智能锁钥匙。你的iPhone可以解锁你的家、你的车库、你的酒店房间,甚至你的工作场所。如此看来,iPhone当门禁卡的功能来了。

7、天气App升级
天气App在iOS 15进行了升级,不仅可以显示更多关于天气的信息,新的天气App会根据天气情况的变化而改变。

8、地图更智能、更详细
全新升级的地图不仅显示信息更丰富,同时还将为驾驶员提供更多详细道路信息。地图还会自动跟踪用户的出行路线,如果用户迷路,可扫描附近建筑,通过增强现实给用户提供正确路线。假如用户乘坐公交出行,还能提醒用户什么时间下车。

以上就是iOS 15系统的主要更新内容了,小编已经第一时间更新了iOS 15系统。从使用半天的感受来看,目前iOS 15并无明显影响使用到Bug,仅部分新功能还未完全汉化,首个iOS 15测试版还是很流畅的,想要尝鲜iOS 15的果粉可以放心升级。
转自:果粉技巧公众号
收起阅读 »

性能优化你会吗 --- iOS开发中常见的性能优化技巧

性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题。但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定...
继续阅读 »

性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题。

但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定不希望玩着玩着突然闪退,然后就是不希望卡顿,其次就是耗电和耗流量不希望太严重,最后就是安装包希望能小一点。简单归类如下:

快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望。
稳:不要在用户使用过程中崩溃和无响应。
省:节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫。
小:安装包小可以降低用户的安装成本。

一、快

应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。卡顿的场景有很多,按场景可以分为4类:UI 绘制、应用启动、页面跳转、事件响应。引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,

根据iOS 系统显示原理可以看到,影响绘制的根本原因有以下两个方面:

1.绘制任务太重,绘制一帧内容耗时太长。
2.主线程太忙,根据系统传递过来的 VSYNC 信号来时还没准备好数据导致丢帧。

绘制耗时太长,有一些工具可以帮助我们定位问题。主线程太忙则需要注意了,主线程关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据,所以特别需要避免任何主线程的事情,这样应用程序才能保持对用户操作的即时响应。总结起来,主线程主要做以下几个方面工作:

1.UI 生命周期控制
2.系统事件处理
3.消息处理
4.界面布局
5.界面绘制
6.界面刷新

除此之外,应该尽量避免将其他处理放在主线程中,特别复杂的数据计算和网络请求等。

二、稳

应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用,比较常用的解决方式如下:

1.提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。
2.代码静态扫描工具。常见工具有Clang Static Analyzer、OCLint、Infer等等。
3.Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。
4.Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息。

三、省

在移动设备中,电池的重要性不言而喻,没有电什么都干不成。对于操作系统和设备开发商来说,耗电优化一致没有停止,去追求更长的待机时间,而对于一款应用来说,并不是可以忽略电量使用问题,特别是那些被归为“电池杀手”的应用,最终的结果是被卸载。因此,应用开发者在实现需求的同时,需要尽量减少电量的消耗。

1.CPU

不论用户是否正在直接使用, CPU 都是应用所使用的主要硬件, 在后台操作和处理推送通知时, 应用仍然会消耗 CPU 资源

应用计算的越多,消耗的电量越多.在完成相同的基本操作时, 老一代的设备会消耗更多的电量, 计算量的消耗取决于不同的因素

2.网络

智能的网络访问管理可以让应用响应的更快,并有助于延长电池寿命.在无法访问网络时,应该推迟后续的网络请求, 直到网络连接恢复为止. 此外,应避免在没有连接 WiFi 的情况下进行高宽带消耗的操作.比如视频流, 众所周知,蜂窝无线系统(LTE,4G,3G等)对电量的消耗远远大于 WiFi信号,根源在于 LTE 设备基于多输入,多输出技术,使用多个并发信号以维护两端的 LTE 链接,类似的,所有的蜂窝数据链接都会定期扫描以寻找更强的信号. 因此:我们需要

1)在进行任何网络操作之前,先检查合适的网络连接是否可用
2)持续监视网络的可用性,并在链接状态发生变化时给与适当的反馈
3).定位管理器和** GPS**

我们都知道定位服务是很耗电的,使用 GPS 计算坐标需要确定两点信息:

1)时间锁每个 GPS 卫星每毫秒广播唯一一个1023位随机数, 因而数据传播速率是1.024Mbit/s GPS 的接收芯片必须正确的与卫星的时间锁槽对齐
2)频率锁 GPS 接收器必须计算由接收器与卫星的相对运动导致的多普勒偏移带来的信号误差

计算坐标会不断的使用 CPU 和 GPS 的硬件资源,因此他们会迅速的消耗电池电量, 那么怎么减少呢?

1)关闭无关紧要的特性

判断何时需要跟踪位置的变化, 在需要跟踪的时候调用 startUpdatingLocation方法,无须跟踪时调用stopUpdatingLocation方法.

当应用在后台运行或用户没有与别人聊天时,也应该关闭位置跟踪,也就说说,浏览媒体库,查看朋友列表或调整应用设置时, 都应该关闭位置跟踪

2)只在必要时使用网络

为了提高电量的使用效率, IOS 总是尽可能地保持无线网络关闭.当应用需要建立网络连接时,IOS 会利用这个机会向后台应用分享网络会话,以便一些低优先级能够被处理, 如推送通知,收取电子邮件等

关键在于每当用户建立网络连接时,网络硬件都会在连接完成后多维持几秒的活动时间.每次集中的网络通信都会消耗大量的电量

要想减轻这个问题带来的危害,你的软件需要有所保留的的使用网络.应该定期集中短暂的使用网络,而不是持续的保持着活动的数据流.只有这样,网络硬件才有机会关闭

4.屏幕

屏幕非常耗电, 屏幕越大就越耗电.当然,如果你的应用在前台运行且与用户进行交互,则势必会使用屏幕并消耗电量

这里有一些方案可以优化屏幕的使用:

1)动画优化

当应用在前台时, 使用动画,一旦应用进入了后台,则立即暂停动画.通常来说,你可以通过监听 UIApplicationWillResignActiveNotification或UIApplicationDIdEnterBackgroundNotification的通知事件来暂停或停止动画,也可以通过监听UIApplicationDidBecomeActiveNotification的通知事件来恢复动画

2)视频优化


视频播放期间,最好保持屏幕常量.可以使用UIApplication对象的idleTimerDisabled属性来实现这个目的.一旦设置了 YES, 他会阻止屏幕休眠,从而实现常亮.

与动画类似,你可以通过相应应用的通知来释放和获取锁

用户总是随身携带者手机,所以编写省电的代码就格外重要, 毕竟手机的移动电源并不是随处可见, 在无法降低任务复杂性时, 提供一个对电池电量保持敏感的方案并在适当的时机提示用户, 会让用户体验良好。

四、小

应用安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高,因此,减小安装包大小可以让更多用户愿意下载和体验产品。

当然,瘦身和减负虽好,但需要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。

App安装包是由资源和可执行文件两部分组成,安装包瘦身从以下三部分优化。

资源优化:
1.删除无用的资源
2.删除重复的资源
3.无损压缩图片
4.不常用资源换为下载

编译优化:
1.去除debug符号
2.开启编译优化
3.避免编译多个架构

可执行文件优化:
1.去除无用代码
2.统计库占用,去除无用库
3.混淆类/方法名
4.减少冗余字符串
5.ARC->MRC (一般不到特殊情况不建议这么做,会提高维护成本)

缩减iOS安装包大小是很多中大型APP都要做的事,一般首先会对资源文件下手,压缩图片/音频,去除不必要的资源。这些资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。

下面是一些常见的优化方案:
TableViewCell 复用

在cellForRowAtIndexPath:回调的时候只创建实例,快速返回cell,不绑定数据。在willDisplayCell: forRowAtIndexPath:的时候绑定数据(赋值)。

高度缓存

在tableView滑动时,会不断调用heightForRowAtIndexPath:,当cell高度需要自适应时,每次回调都要计算高度,会导致 UI 卡顿。为了避免重复无意义的计算,需要缓存高度。

怎么缓存?

字典,NSCache。

UITableView-FDTemplateLayoutCell

[if !supportLineBreakNewLine]

[endif]

视图层级优化

不要动态创建视图

在内存可控的前提下,缓存subview。

善用hidden。

[if !supportLineBreakNewLine]

[endif]

减少视图层级

减少subviews个数,用layer绘制元素。

少用clearColor,maskToBounds,阴影效果等。

[if !supportLineBreakNewLine]

[endif]

减少多余的绘制操作

图片

不要用JPEG的图片,应当使用PNG图片。

子线程预解码(Decode),主线程直接渲染。因为当image没有Decode,直接赋值给imageView会进行一个Decode操作。

优化图片大小,尽量不要动态缩放(contentMode)。

尽可能将多张图片合成为一张进行显示。

[if !supportLineBreakNewLine]

[endif]

减少透明view

使用透明view会引起blending,在iOS的图形处理中,blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。

会导致blending的原因:

UIView的alpha<1。

UIImageView的image含有alpha channel(即使UIImageView的alpha是1,但只要image含有透明通道,则仍会导致blending)。

[if !supportLineBreakNewLine]

[endif]

为什么blending会导致性能的损失?

原因是很直观的,如果一个图层是不透明的,则系统直接显示该图层的颜色即可。而如果图层是透明的,则会引起更多的计算,因为需要把另一个的图层也包括进来,进行混合后的颜色计算。

opaque设置为YES,减少性能消耗,因为GPU将不会做任何合成,而是简单从这个层拷贝。

[if !supportLineBreakNewLine]

[endif]

减少离屏渲染

离屏渲染指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。

OpenGL中,GPU屏幕渲染有以下两种方式:

On-Screen

Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

Off-Screen

Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

[if !supportLineBreakNewLine]

[endif]

小结

性能优化不是更新一两个版本就可以解决的,是持续性的需求,持续集成迭代反馈。在实际的项目中,在项目刚开始的时候,由于人力和项目完成时间限制,性能优化的优先级比较低,等进入项目投入使用阶段,就需要把优先级提高,但在项目初期,在设计架构方案时,性能优化的点也需要提早考虑进去,这就体现出一个程序员的技术功底了。

什么时候开始有性能优化的需求,往往都是从发现问题开始,然后分析问题原因及背景,进而寻找最优解决方案,最终解决问题,这也是日常工作中常会用到的处理方式。

链接:https://www.jianshu.com/p/965932858d95

收起阅读 »

iOS安全之三攻三防

互联网世界每分钟都在上演黑客攻击,由此导致的财产损失不计其数。金融行业在安全方面的重视不断加深,而传统互联网行业在安全方面并没有足够重视,这样导致开发的APP在逆向开发人员面前等同于裸奔,甚至有些小厂前后台在账号密码处理上采取明文传送,本地存储,这等同于将账号...
继续阅读 »

互联网世界每分钟都在上演黑客攻击,由此导致的财产损失不计其数。金融行业在安全方面的重视不断加深,而传统互联网行业在安全方面并没有足够重视,这样导致开发的APP在逆向开发人员面前等同于裸奔,甚至有些小厂前后台在账号密码处理上采取明文传送,本地存储,这等同于将账号密码直接暴露无疑。当然即使采用加密传送,逆向APP后依然可以获取到账号密码,让你在神不知鬼不觉的情况下将账号密码发送到了黑客邮箱,所以攻防终究是一个相互博弈的过程。本文主要分析常见的几种攻击和防护手段,通过攻击你可以看到你的APP是如何被一步一步被攻破的。有了攻击,我们针对相应的攻击就是见招拆招了。

一、攻击原理

从APPStore下载正式版本版本应用,进行一键砸壳,绝大部分应用均可以脱壳成功。
使用脚本或第三方工具MonkeyDev对应用实现重签名。
利用动态调试(LLDB,Cycript,Reveal)和静态分析(反汇编),找到关键函数进行破解。
Theos编写插件,让使用更加方便。

二、攻守第一回合

1. 第一攻武器:代码注入+method_exchangeImplementations

在shell脚本实现iOS包重签名及代码注入的最后,我们成功使用method_exchange截获微信点击按钮,代码如下:

+(void)load
{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountLoginControlLogic"), @selector(onFirstViewRegester));

Method newMethod = class_getInstanceMethod(self, @selector(test));

method_exchangeImplementations(oldMethod, newMethod);
}

-(void)test{
NSLog(@"----截获到微信注册按钮点击------");
}

2. 第一防护盾:framwork+fishHook

关于为什么使用framwork而不是直接在代码中创建一个类,并在类的load方法中编写防护代码,原因是自己创建framwork的加载要早于代码注入的framwork,代码注入的framwork的执行要早于自己类load的加载,具体原理请看dyld加载应用启动原理详解。防护代码如下:


注意:当我们检查到hook代码时,比较好的处理方式是将该手机的UDID,账号等信息发送给后台服务器,让后台服务器进行封号禁设备处理,而不是直接exit(0)让程序强制退出,因为这样的好处是让黑客很难定位。

三、攻守第二回合

1. 第二攻武器:MonkeyDev

MonkeyDev可以帮助我们更加方便的实现代码重签名和hook,底层是使用了方法交换的SET和GET方法进行hook,关于MoneyDev的使用在逆向iOS系统桌面实现一键清空徽标有讲。同样以截获微信注册按钮为例,hook代码示例如下:

%hook WCAccountLoginControlLogic
- (void)onFirstViewRegester:(id)arg{
NSLog(@"---hook-----");
}

%end

2. 第二防护盾:依然framwork+fishHook

+(void)load{
//setIMP
struct rebinding gt;
gt.name = "method_getImplementation";
gt.replacement = my_getIMP;
gt.replaced = (void *)&getIMP;
//getIMP
struct rebinding st;
st.name = "method_setImplementation";
st.replacement = my_setIMP;
st.replaced = (void *)&setIMP;

struct rebinding rebs[2] = {gt,st};
rebind_symbols(rebs, 2);

}

//保存原来的交换函数
IMP (*getIMP)(Method _Nonnull m);
IMP (*setIMP)(Method _Nonnull m, IMP _Nonnull imp);


IMP my_getIMP(Method _Nonnull m){
NSLog(@"🍺----检查到了HOOk-----🍺");
return nil;
}
IMP my_setIMP(Method _Nonnull m, IMP _Nonnull imp){

NSLog(@"🍺----检查到了HOOk-----🍺");
return nil;
}

三、攻守第三回合

上面的两次攻击都是通过代码注入来实现hook目的,我们能不能防止第三方代码进行注入呢?答案当然是可以,接下来我们来防止第三方代码注入。

1. 第三防护盾:在编译设置阶段增加字段"-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null",如下图:


1.1 增加该字段后在MachO文件就会增加_RESTRICT,__restrict段,如下图:


1.2 为什么增加这个字段就可以了呢?这里我们就要回归到dyld的源码了,在dyld加载过程中有一个函数hasRestrictedSegment就是用来判断是否存在__RESTRICT,__RESTRICT中是否是__restrict名称,如果是,则会禁止加载第三方注入的库文件,源码如下:

#if __MAC_OS_X_VERSION_MIN_REQUIRED
static bool hasRestrictedSegment(const macho_header* mh)
{
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}
#endif

2. 第三攻击武器:直接修改MachO二进制文件

通过Synalyze It!工具更改MachO二进制文件字段,然后重新签名打包即可破坏该防护过程:

3. 第三防护2级护盾:代码过滤,增加白名单。

3.1 既然禁止第三方注入代码都很容易被攻破,接下来我们就从代码入手,过滤第三方库注入库,增加白名单,代码如下: 

@implementation ViewController
+(void)load
{

const struct mach_header_64 * header = _dyld_get_image_header(0);
if (hasRestrictedSegment(header)) {
NSLog(@"---- 防止状态 ------");

//如果__RESTRICT字段被绕过,开始开名单检测
CheckWhitelist()

}else{
NSLog(@"--- 防护字段被修改了 -----");
}


}

static bool hasRestrictedSegment(const struct macho_header* mh)
{
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

printf("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}

#pragma mark -- 白名单监测
bool CheckWhitelist(){

int count = _dyld_image_count();//加载了多少数量

for (int i = 0; i < count; i++) {
//遍历拿到库名称!
const char * imageName = _dyld_get_image_name(i);
if (!strstr(libraries, imageName)&&!strstr(imageName, "/var/mobile/Containers/Bundle/Application")) {
printf("该库非白名单之内!!\n%s",imageName);
return NO;
}

return YES;
}

3.2 原理就是使用系统的函数帮我们检测自己设定的__RESTRICT是否被更改,如果被更改说明我们被Hook了,接下来在被hook的字段中增加自己的处理逻辑即可。

总结:对最后一个防护代码也很容易进行攻击,比如找到hasRestrictedSegment函数,让其直接返回YES。所以建议将该函数进行封装,尽量不要使用Bool作为返回值。综上: 攻和守本来就是一个博弈的过程,没有绝对安全的城墙。
最后附上过滤白名单源码下载,直接拖入工程即可使用,达到较好的代码防护目的。如果帮助到你请给一个Star。
我是Qinz,希望我的文章对你有帮助。

链接:https://www.jianshu.com/p/655c91b61f8a

收起阅读 »

iOS逆向(6)-从fishhook看runtime,hook系统C函数

在上篇文章不知MachO怎敢说自己懂DYLD中已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc...
继续阅读 »

在上篇文章不知MachO怎敢说自己懂DYLD中已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc的回调函数_dyld_objc_notify_register等等。并且在末尾提出了MachO中还有一些符号表,而有哪些符号表,这些符号表又有些什么用呢?笔者在这篇文章就将一一道来。

老规矩,片头先上福利:点击下载demo,demo中有笔者给fishhook每句代码加的详细注释!!!
这篇文章会用到的工具有:

fishhook

在开始正文之前,假设面试官问了一个问题:
都知道Objective-C最大的特性就是runtime,大家可以用使用runtime对OC的方法进行hook,那么C函数能不能hook?

有兴趣回答的朋友可以先行在评论区回答,答完之后再继续阅读或者预先偷窥一下文末的答案,看看这被炒了无数次冷饭的runtime自己是否真的了然于胸。

本将从以下几方面回答上面所提的问题:

1、Runtime的Hook原理
2、为什么C不能hook
3、如何利用MachO“玩坏”系统C函数
4、fishhook源码分析
5、绑定系统C函数过程验证

一、Runtime的Hook原理

Runtime,从名称上就知道是运行时,也是它造就了OC运行时的特性,而要想彻底明白什么是运行时,那么就需要将之与C语言有相比较。
今天咱们就从汇编的角度看一看OC和C在调用方法(函数)上有什么区别。

注:笔者使用的是iPhone 7征集调试,所有一下汇编都是基于arm64,所以以下所有汇编默认为基于arm64。

新建一个工程取名为:FishhookDemo
敲入两个OC方法mylog和mylog2,挂上断点,如图:


开启汇编断点,如图:


运行工程,会跳转到如下图的汇编断点:


从上图可以看的出来调用了两个objc_msgSend,这两个很像是
我们的mylog和mylog2,但现在还不能确定。
想一想objc_msgSend的定义:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

第一个参数是self,第二个参数是SEL,所以可以知道SEL是放在x1的寄存器里面(什么是x1?继续关注作者,之后的文章会有相关的汇编的专门篇章)。

马不停蹄,挂上两个汇编断点,查看一下两个x1中存放的到底是什么,如图:



这也就验证了咱们OC方法都是消息转发(objc_msgSend)。而同一个C函数的地址又都是一样的(笔者这次运行的地址就是0x1026ce130) 。

所以在每次调用OC方法的时候就让我们有了一次改变消息转发「目标」的机会。

这里稍微提一下runtime的源码分析流程:
Step 1、方法查找
① 汇编快速查找缓存
② C/C++慢速查找:self->super->NSObject->找到换缓存起来
Step 2、动态方法解析: _class_resolveMethod
① _class_resolveInstanceMethod
② _class_resolveClassMethod
Step 3、消息转发
① _forwardingTargetForSelector
② _methodSignatureForSelector
③ _forwardInvocation
④ _doesNotRecognizeSelector

二、为什么C不能hook

同样我们从汇编的角度切入。
敲入代码一些C函数,挂上断点,如图:


运行工程:
会看到断点断到如下汇编:


可以看到每个NSLog对应跳转的地址都是0x10000a010,每个printf对应跳转的地址都是0x10000a184,也就是说每个C的函数都是一一对应着一个真实的地址空间。每次在调用一个C函数的时候都是执行一句汇编bl 0xXXXXXXXX。

所以上面讲述到的消息转发的机会没有了,也就是没有了利用runtime来Hook的机会了。

三、如何利用MachO“玩坏”系统C函数

既然如此,那么是否C函数就真的那么牢不可破,无法对他进行Hook呢?
答案肯定是否定的!
想要从根上理解这个问题,首先要了解:我们的C函数分为系统C函数和我们自定义的C函数。

1、自定义的C函数

在上面的步骤中我们已经了解到所有C函数的调用都是跳转到一个「固定的地址」,那么就可以推断得出这个「固定的地址」其实是在编译期已经被生成好了,所以才能快速、直接的跳转到这个地址,实现函数调用。
C语言被称之为是静态语言也就是这么个理。

2、系统的C函数

在上篇文章不知MachO怎敢说自己懂DYLD已经提到了在dyld启动app的第二个步骤就是加载共享缓存库,共享缓存库包括Foundation框架,NSLog是被包含在Foundation框架的。那么就可以确定一件事情,在我们将自己工程打包出的MachO文件中是不可能预先确定NSLog的地址的。

但是又因为C语言是静态的特性,没法在运行的时候实时获取共享缓存库中NSLog的地址。而共享缓存库的存在好处太大,既能节省大量内存,又能加快启动速度提升性能,不能弃之而不用。

为了解决这个问题,Apple使用了PIC(Position-independent code)技术,在第一次使用对应函数(NSLog)的时候,从系统内存中将对函数(NSLog)的内存地址取出,绑定到APP中对应函数(NSLog)上,就可以实现正常的C函数(NSLog)调用了。

既然有这么个过程,iOS系统可以动态的绑定系统C函数的地址,那么咱们就也能。

四、fishhook源码分析

1、fishhook的总体思路

Facebook的开源库fishhook就可以完美的实现这个任务。
先上一张官网原理图:


总体来说,步骤是这样的:

先找到四张表Lazy Symbol Pointer Table、Indirect Symbol Table、Symbol Table、String Table。
MachO有个规律:Lazy Symbol Pointer Table中第index行代表的函数和Indirect Symbol Table中第index行代表的函数是一样的。
Indirect Symbol Table中value值表示Symbol Table的index。
找到Symbol Table的中对应index的对象,其data代表String Table的偏移值。
用String Table的基值,也就是第一行的pFile值,加上Symbol Table的中取到的偏移值,就能得到Indirect Symbol Table中value(这个value代表函数的偏移值)代表的函数名了。

2、验证NSLog地址

下面就来验证一下在NSLog的地址是不是真的就存在Indirect Symbol Table中。
同样在NSLog处下好断点,打开汇编断点,运行代码。会发现断点断在如下入位置:


注:笔者的工程重新build了,MachO也重新生成,所以此处的截图和上文中断住NSLog的截图的地址不一样,这是正常情况。

可以发现NSLog的地址是0x104d36010,先记住这个值。

然后查看我们APP在内存中的偏移值。
利用image list命令列出所有image,第一个image就是我们APP的偏移值,也就是内存地址。


可以看到APP在内存中的偏移值为0x104d30000。
接着打开MachOView查看MachO中的Indirect Symbol Table中的value,如图:


其值为0x100006010,去除最高位得到的0x6010就是NSLog在MachO中的偏移值。
最后将NSLog在MachO中的偏移值于APP在内存中的偏移值相加就得到NSLog真实的内存地址:
0x6010+0x104d30000=0x104d36010

最终证明,在Indirect Symbol Table的value中的值就是其对应的函数的地址!!!

3、根据MachO的表查找对应的函数名和函数地址

咱们还是用NSLog来距离查找。

1、Indirect Symbol Table

取出其data值0000010A,用10进制表示,结果为266,如图:


2、Symbol Table

在Symbol Table中找到下标(offset)为266的的对象,取出其data0x124,如图:


3、String Table

将在Symbols中得到的偏移值0x124加上String Table的首个地址DC6C,得到值DD90,然后找到pFile为DD90的值,如下两图:



上述就是根据MachO的表查找对应的函数名和函数地址全过程了。

4、源码分析

fishhook的源码总共只有250行左右,所以结合MachO慢慢看,其实一点也不费劲,在笔者的demo中有对其每一句函数的详细注释。当然也有对fishhook使用的demo。

所以笔者就不在此处对fishhook做太过详细的介绍了。只对其中一些关键参数和关键函数做介绍。

1、fishhook为维护一个链表,用来储存需要hook的所有函数

// 给需要rebinding的方法结构体开辟出对应的空间
// 生成对应的链表结构(rebindings_entry),并将新的entry插入头部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)

2、根据linkedit的基值,找到对应的三张表:symbol_table、string_table和indirect_symtab :

// 找到linkedit的头地址
// linkedit_base其实就是MachO的头地址!!!可以通过查看linkedit_base值和image list命令查看验证!!!(文末附有验证图)
/**********************************************************
Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset
MachO地址 = PAGEZERO + ASLR
上面两个公式是已知的 得到下面这个公式
MachO文件地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)
**********************************************************/
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// 获取symbol_table的真实地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// 获取string_table的真实地址
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 获取indirect_symtab的真实地址
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

3、最核心的一个步骤,查找并且替换目标函数:

// 在四张表(section,symtab,strtab,indirect_symtab)中循环查找
// 直到找到对应的rebindings->name,将原先的函数复制给新的地址,将新的函数地址赋值给原先的函数
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab)

五、绑定系统C函数过程验证

上面说了这么多,那么咱们来验证一下系统C函数是不是真的会这样被绑定起来,并且看一看,是在什么时候绑定的。

同样,在第一次敲入NSLog函数的地方加上断点,在第二个NSLog处也加上断点:


运行工程后,使用dis -s命令查看该函数的汇编代码,并且继续查看其中第一次b指令,也就是函数调用的汇编,如图:


从上图就可以看到,在我们第一次调用NSLog的时候,系统确实会默认的调用dyld_stub_binder函数对NSLog进行绑定。

继续跳过这个断点,进入下一个NSLog的汇编断点处,同样利用dis -s命令查看该汇编:


得到答案:
系统确实会在第一次调用系统C函数的时候对其进行绑定!

还记得正文开始的时候的那个问题吗?
那么是不是系统C函数可以hook,而自定义的C函数就绝对不能hook了呢?
很显然,国内外大神那么多,肯定是能做到的,有兴趣的读者可以自行查阅Cydia Substrate。

这篇文章利用了一些LLDB命令行看了许多我们想看的内容,如image list,register read还有dis -s,在我们正向开发中,LLDB就是一把利器,而在我们玩逆向的时候,LLDB就成为了我们某些是后的唯一途径了!所以,在下一篇文章中,笔者将会对LLDB进行更加详细的讲解,让大家看到LLBD的伟大。

1、关于道友AmazingYu的提问:
想问下 linkedit_base 地址与 Text 段的初始地址以及 Data 段的初始地址的关系,这三个段在内存中是挨着的吗,还有就是 linkedit_base 大概在进程内存分布中的哪个地方?

在咨询大佬请叫我Hank后,得到最终答案,在下面问回答中有一些问题,再此纠正一下!
linkedit地址(不是linkedit_base,末尾会介绍linkedit_base到底是什么) 与 Text 段的初始地址以及 Data 段确实是连续的,他们的顺序是:
先是Text 段,然后是Data 段,最后是linkedit_base 地址。从下面三幅图的File Offset和File Size可以看出来,两者相加就能得到下一段的地址:




2、几个名词(pFile 、offset 、File Offset)之前解释的有点问题:
1、首先,这三个都是表示相对于MachO的内存偏移,只不过其含义被细分了。
2、pFile 和 offset含义相近,不过offset更详细,能够对应上具体某一个符号(DATA? TEXT?)。比如文件里面有许多类,类里面有许多的属性,pFile就代表各个类的偏移值,offset代表各个属性的偏移值
3、File Offset 这个存在于Segment的字段中。用于从Segment快速找到其代表的「表」真正的偏移值。
最后说一下linkedit_base:
linkedit_base其实代表的就是MachO的真实内存地址!
可以从下图得到验证


因为

Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset 
MachO地址 = PAGEZERO + ASLR
// 上面两个公式是已知的 所以可以得到下面这个公式
MachO地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)

也就是fishhook中的:

uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

转自:https://www.jianshu.com/p/b6a72aa6c146

收起阅读 »

RunLoop(二):实际应用

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。Timer定时器问题我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的...
继续阅读 »

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。

Timer定时器问题

我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的时候,timer定时器会卡住,停止滚动之后才重新生效。

这个问题比较简单,也是我们经常遇到的。

因为定时器默认是添加在了RunLoop的NSDefaultRunLoopMode模式下,scrollView在滚动的时候会进入UITrackingRunLoopMode,RunLoop在同一时间只能处理一种mode,所以在滚动的时候,自然定时器就没法处理,卡住。

解决方法就是我们创建了timer之后,把他add到RunLoop的NSRunLoopCommonModes,NSRunLoopCommonModes其实并不是一种真实的模式,他只是一个标志,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)。

这个地方多说一句,这个标记为common是啥意思。我们得看回RunLoop结构体的源码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

可以看到里面有一个set类型的变量,CFMutableSetRef _commonModes;,被放到这个set中的mode就等于是被标记为了common。NSDefaultRunLoopMode和UITrackingRunLoopMode都在里面。

下面是我们创建timer的正确姿势 ~

//我们平时可能都是用scheduledTimerWithTimeInterval这个方法创建,这个会默认把timer添加到runloop的defalut模式下,所以我们使用timerWithTimeInterval创建
NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d",++ count);
}];

//NSRunLoopCommonModes 并不是一个真的模式 他只是一个标记,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

线程保活

线程保活并不是所有的项目都用的到,他适应于那种一直有任务需要处理的场景,而且注意,一定要是串行的任务。这种情况下保活一条线程,就可以免去线程创建和销毁的开销,提高性能。

具体怎么保活线程,我下面先直接把我的代码贴出来,然后针对一些点在做一系列的说明。(模拟的项目场景是进入到一个vc中,开一条线程,然后用这条线程来执行任务,当然vc销毁时,线程也要销毁。)

下面是全部代码,大家可以先跳过代码看下面的一些解析。

#import "SecondViewController.h"

@interface MyThread : NSThread
@end
@implementation MyThread
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end

@interface SecondViewController ()
@property (nonatomic, strong) MyThread * thread;
@property (nonatomic, assign, getter=isStopped) BOOL stopped;
@end

@implementation SecondViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];

self.stopped = NO;

UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(40, 100, 100, 40);
btn.backgroundColor = [UIColor blackColor];
[btn setTitle:@"停止" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(stopThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];

__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"--end--");
}];
[self.thread start];

}

- (void)stopThread {
if (!self.thread) return;

// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 执行这个方法必须要在我们自己创建的这个线程中
- (void)__stopThread {
// 标识
self.stopped = YES;
// 停止runloop
CFRunLoopStop(CFRunLoopGetCurrent());
//
self.thread = nil;
}


#pragma mark - 添加touch事件 (每点击一次 让线程处理一次事件)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (!self.thread) return;

[self performSelector:@selector(threadDoSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)threadDoSomething {
NSLog(@"work--%@",[NSThread currentThread]);
}


#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}

@end

最顶部新建了一个继承自NSThread的MyThread类,目的就是为了重写-dealloc方法,在内部有打印内容,方便我调试线程是否被销毁。在我们真是的项目中,可以不需要这部分。

初始化线程,开启RunLoop

__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
//往runloop里面添加source/timer/observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"--end--");
}];
[self.thread start];

这部分是初始化我们的线程,线程的初始化我们一般用的多的是self.thread = [[MyThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];这样的方法,我是觉得这样把self传进线程内部,可能造成一些循环引用问题,最后影响vc和thread的销毁,所以我是用了block的形式。

initWithBlock的意思也就是线程初始化完毕会执行block内的代码。一个子线程默认是没有RunLoop的,RunLoop会在第一次获取的时候创建,所以我们先[NSRunLoop currentRunLoop]获取RunLoop,也就是创建了我们当前线程的RunLoop。

在了解RunLoop底层的时候我们了解到,如果一个RunLoop没有timer、observer、source,就会退出。我们新创建的RunLoop这些都是没有的,如果我们不手动的添加,那我们的RunLoop一跑起来就这就会退出的。所以就等于说我们必须手动给RunLoop添加点事情做。

在代码中我们使用了addPort:forMode这个方法,向当前RunLoop添加一个端口让RunLoop监听。RunLoop有工作做了,自然就不会退出的。

我们在开启线程的时候,用了一个while循环,通过一个属性stopped来控制是否跳出循环,然后循环内部使用了- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;这个方法开启RunLoop。有人有可能会问了,这里的开启RunLoop为什么不直接使用- (void)run;这个方法。这里我稍微解释一下:

查阅一下苹果的文档可以了解到,这个run方法,内部其实也是循环的调用了runMode这个方法的,但是这个循环是永远不会停止的,也就是说我们使用run方法开启的RunLoop是永远都不会停下来的,我们调用了stop之后,也只会停止当前的这一次循环,他还是会继续run起来的。所以文档中也提到,如果我们要创建一个可以停下来的RunLoop,用runMode这个方法。所以我们用这个while循环模拟run的运行原理,但是呢,我们通过stopped这个属性可以控制循环的停止。

while里面的条件weakSelf && !weakSelf.isStopped为什么不仅仅使用stopped判断,而是还要判断weakSelf是否有值?我们下面会提到的。

两个stopThread方法

- (void)stopThread {
if (!self.thread) return;

// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 执行这个方法必须要在我们自己创建的这个线程中
- (void)__stopThread {
// 标识置为YES,跳出while循环
self.stopped = YES;
// 停止runloop的方法
CFRunLoopStop(CFRunLoopGetCurrent());
// RunLoop退出之后,把线程置空释放,因为RunLoop退出之后就没法重新开启了
self.thread = nil;
}

stopThread是给我们的停止button调用的,但是实际的停止RunLoop操作在__stopThread里面。在stopThread中调用__stopThread一定要使用performSelector:onThread:这一类的方法,这样就可以保证在我们指定的线程中执行这个方法。如果我们直接调用__stopThread,就说明是在主线程调用的,那就代表我们把主线程的RunLoop停掉了,那我们的程序就完了。

touch模拟事件处理

我们在touchBegin方法中,让我们self.thread执行-threadDoSomething这个方法,代表每点击一次,我们的线程就要处理一次-threadDoSomething中的打印事件。做这个操作是为了检测看我们每次工作的线程是不是都是我们最开始创建的这一个线程,没有重新开新线程。

其他细节

那我们仔细观察的话会发现一个问题,-threadDoSomething和stopThread这两个方法中都是用下面这个方法来处理线程间通信

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait

但是两次调用传入的wait参数是不一样的。我们要先知道这个waitUntilDone:(BOOL)wait代表什么意思。

如果wait传的是YES,就代表我们在主线程用self调用这个performSelector的时候,主线程会等待我们的self.thread这个线程执行他需要执行的方法,等着self.thread执行完方法之后,主线程再继续往下走。那如果是NO,肯定就是主线程不会等了,主线程继续往下走,然后我们的self.thread去调用自己该调用的方法。

那为什么在stop方法中是用的YES?

有这么一个情形,如果我们push进这个vc,线程初始化,然后RunLoop开启,但是我们不想通过点击停止button来停止,当我们点击导航的back的时候,我也需要销毁线程。

所以我们在vc的-dealloc方法中也调用了stopThread方法。那如果stopThread中使用
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
的时候wait不用YES,而是NO,会出现什么情况,那肯定是crash了。

如果wait是NO,代表我们的主线程不会等待self.thread执行__stopThread方法。

#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}

但是dealloc中主线程调用完stopThread,之后整个dealloc方法就结束了,也就是我们的控制器已经销毁了。但是呢这个时候self.thread还在执行__stopThread方法呢。__stopThread中还要self变量,但是他其实已经销毁了,所以这个地方就会crash了。所以在stopThread中的wait一定要设置为YES。

在当时写代码的时候,这样确实处理了crash的问题,但是我直接返回值后,RunLoop并没有结束,线程没有销毁。这就要讲到上面说的while判断条件是weakSelf && !weakSelf.isStopped的原因了。vc执行了dealloc之后,self被置为nil了,weakSelf.isStopped也是nil,取非之后条件又成立了,while循环还要继续的走,RunLoop又run起来了。所以这里我们加上weakSelf这个判断,也就是self必须不为空。

总结

上面就是我实现的线程保活这一功能的代码和细节分析,当然我们在实际的项目中可能有多个位置需要线程保活这一功能,所以我们应该把这一部分做一下简单的封装,来方便我们在不同的地方调用。大家有兴趣的可以自己封装一下,我在写RunLoop相关的代码时,大多用的是OC层的代码,有兴趣的小伙伴可以尝试一下C语言的API。

RunLoop的应用当前不止这么一点,还可以监控应用卡顿,做性能优化,这些以后研究明白了再继续更博客吧,一起加油。

相关的功能代码和封装已经放到github上面了
https://github.com/Sunxb/RunLoopDemo


链接:https://www.jianshu.com/p/9e0177d40aab

收起阅读 »

音视频学习从零到整-关于视频的一些概念

内容1、视频文件格式2、视频封装格式3、视频编解码方式4、音频编解码方式5、颜色模型一.视频相关概念1.1 视频文件格式文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于...
继续阅读 »

内容

1、视频文件格式
2、视频封装格式
3、视频编解码方式
4、音频编解码方式
5、颜色模型

一.视频相关概念

1.1 视频文件格式

文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于视频来说,
我们常见的文件格式则有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。文件格式通常表现为文件在操作系统上存储时的后缀名,它通常会被操作系统用来与相应的打开程序关联,比如你双击一个 test.doc 文件,系统会调用 Word 去打开它。你双击一个 test.avi 或者 test.mkv 系统会调用视频播放器去打开它。

同样是视频,为什么会有 .mov、.avi、.mpg 等等这么多种文件格式呢?****那是因为它们通过不同的方式实现了视频这件事情,至于这个不同在哪里,那就需要了解一下接下来要说的「视频封装格式」这个概念了。

1.2 视频封装格式

视频封装格式,简称视频格式,相当于一种储存视频信息的容器,它里面包含了封装视频文件所需要的视频信息、音频信息和相关的配置信息(比如:视频和音频的关联信息、如何解码等等)。一种视频封装格式的直接反映就是对应着相应的视频文件格式。


下面我们就列举一些文件封装格式:

1、AVI 格式,对应的文件格式为 .avi,英文全称 Audio Video Interleaved,是由 Microsoft 公司于 1992 年推出。这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。

2、DV-AVI 格式,对应的文件格式为 .avi,英文全称 Digital Video Format,是由索尼、松下、JVC 等多家厂商联合提出的一种家用数字视频格式。常见的数码摄像机就是使用这种格式记录视频数据的。它可以通过电脑的 IEEE 1394 端口传输视频数据到电脑,也可以将电脑中编辑好的的视频数据回录到数码摄像机中。

3、WMV 格式,对应的文件格式是 .wmv、.asf,英文全称 Windows Media Video,是微软推出的一种采用独立编码方式并且可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。

4、MPEG 格式,对应的文件格式有 .mpg、.mpeg、.mpe、.dat、.vob、.asf、.3gp、.mp4 等等,英文全称 Moving Picture Experts Group,是由运动图像专家组制定的视频格式,该专家组于 1988 年组建,专门负责视频和音频标准制定,其成员都是视频、音频以及系统领域的技术专家。MPEG 格式目前有三个压缩标准,分别是 MPEG-1、MPEG-2、和 MPEG-4。MPEG-4 是现在用的比较多的视频封装格式,它为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。

5、Matroska 格式,对应的文件格式是 .mkv,Matroska 是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。

6、Real Video 格式,对应的文件格式是 .rm、.rmvb,是 Real Networks 公司所制定的音频视频压缩规范称为 Real Media。用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。

7、QuickTime File Format 格式,对应的文件格式是 .mov,是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。

8、Flash Video 格式,对应的文件格式是 .flv,是由 Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。

从上面的介绍中,我们大概对视频文件格式以及对应的视频封装方式有了一个概念,接下来则需要了解一下关于视频更本质的东西,那就是视频编解码。

1.3 容器(视频封装格式)
封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.

通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.

常见的视频容器格式:
1、AVI: 是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
2、MOV:是Quicktime封装
3、WMV:微软推出的,作为市场竞争
4、mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
5、flv: 这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
6、MP4:主要应用于mpeg4的封装,主要在手机上使用。

2.1视频编解码方式
视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.

在做视频编解码时,需要考虑以下这些因素的平衡:视频的质量、用来表示视频所需要的数据量(通常称之为码率)、编码算法和解码算法的复杂度、针对数据丢失和错误的鲁棒性(Robustness)、编辑的方便性、随机访问、编码算法设计的完美性、端到端的延时以及其它一些因素。

2.2 常见视频编码方式:

H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。

介绍了上面这些「视频编解码方式」后,我们来说说它和上一节讲的「视频封装格式」的关系。

可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,

但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

3.1 音频编码方式
视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。
MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。
WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

3.2 直播/小视频中的编码格式
视频编码格式

H264编码的优势:
低码率
高质量的图像
容错能力强
网络适应性强
总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
音频编码格式:

AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)
HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)
优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码

适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

4.1 关于H264
H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

图像


H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好。




片(Slice),每一帧图像可以分为多个片

网络提取层单元(NALU, Network Abstraction Layer Unit),

NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

宏块(Macroblock),分片是由宏块组成。

4.2 颜色模型
我们开发场景中使用最多的应该是 RGB 模型


在 RGB 模型中每种颜色需要 3 个数字,分别表示 R、G、B,比如 (255, 0, 0) 表示红色,通常一个数字占用 1 字节,那么表示一种颜色需要 24 bits。那么有没有更高效的颜色模型能够用更少的 bit 来表示颜色呢?

现在我们假设我们定义一个「亮度(Luminance)」的概念来表示颜色的亮度,那它就可以用含 R、G、B 的表达式表示为:

Y = kr*R + kg*G + kb*B

Y 即「亮度」,kr、kg、kb 即 R、G、B 的权重值。

这时,我们可以定义一个「色度(Chrominance)」的概念来表示颜色的差异:

Cr = R – Y
Cg = G – Y
Cb = B – Y

Cr、Cg、Cb 分别表示在 R、G、B 上的色度分量。上述模型就是 YCbCr 颜色模型基本原理。

YCbCr 是属于 YUV 家族的一员,是在计算机系统中应用最为广泛的颜色模型,就比如在本文所讲的视频领域。在 YUV 中 Y 表示的是「亮度」,也就是灰阶值,U 和 V 则是表示「色度」。

YUV 的关键是在于它的亮度信号 Y 和色度信号 U、V 是分离的。那就是说即使只有 Y 信号分量而没有 U、V 分量,我们仍然可以表示出图像,只不过图像是黑白灰度图像。在YCbCr 中 Y 是指亮度分量,Cb 指蓝色色度分量,而 Cr 指红色色度分量。

现在我们从 ITU-R BT.601-7 标准中拿到推荐的相关系数,就可以得到 YCbCr 与 RGB 相互转换的公式

Y = 0.299R + 0.587G + 0.114B
Cb = 0.564(B - Y)
Cr = 0.713(R - Y)
R = Y + 1.402Cr
G = Y - 0.344Cb - 0.714Cr
B = Y + 1.772Cb

这样对于 YCbCr 这个颜色模型我们就有个初步认识了,但是我们会发现,这里 YCbCr 也仍然用了 3 个数字来表示颜色啊,有节省 bit 吗?为了回答这个问题,我们来结合视频中的图像和图像中的像素表示来说明

假设图片有如下像素组成


一副图片就是一个像素阵列.每个像素的 3 个分量的信息是完整的,YCbCr 4:4:4


下图中,对于每个像素点都保留「亮度」值,但是省略每行中偶素位像素点的「色度」值,从而节省了 bit。YCbCr4:2:2


上图,做了更多的省略,但是对图片质量的影响却不会太大.YCbCr4:2:0


转自:https://www.jianshu.com/p/15f28fe89329

收起阅读 »

RunLoop(一):源码与逻辑

简述什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。...
继续阅读 »

简述

什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。

那RunLoop是怎样保持程序的运行状态,到底处理了哪些事件?下面我们就从源码的层面来了解一下RunLoop。

RunLoop

获取runloop对象

NSRunLoop和CFRunLoopRef都代表RunLoop对象,NSRunLoop是对CFRunLoopRef的封装。

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

RunLoop相关类

从源码的代码结构中我们可以找出来一下5个跟RunLoop相关的结构

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
CFRunLoopTimerRef

下面是CFRunLoopRef的结构代码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

变量很多,我们不需要全部看,只需要注意这两个

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

每一个runloop里面有很多mode(存在一个set集合里面),然后之后后一个mode叫做currentMode,也就是说runloop一次只能处理一种mode。

然后我们再看CFRunLoopModeRef的结构,我已经给大家省略了里面那些我们不需要关注的变量

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

根据上面这些我们大概的可以概括出来RunLoop这些相关类的关系。


CFRunLoopModeRef
由上面的源码我们可以稍微总结一下这个CFRunLoopModeRef:

1、CFRunLoopModeRef代表RunLoop的运行模式
2、一个RunLoop包含多个CFRunLoopModeRef,每个CFRunLoopModeRef又包含多个_sources0,_sources1,_observers,_timers。
3、RunLoop每次只能运行一种mode,切换mode的时候,要先退出之前的mode。
4、如果mode中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。
常用的两种Mode

kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行

UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

CFRunLoopObserverRef
源码中给出了可以监听的RunLoop状态

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入RunLoop
kCFRunLoopEntry = (1UL << 0),
// 即将处理timers
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理Sources
kCFRunLoopBeforeSources = (1UL << 2),
// 即将休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出循环
kCFRunLoopExit = (1UL << 7),
// 所有状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

具体的怎么样添加observer来监听RunLoop状态我就不贴代码了,网上一搜有很多的。

RunLoop的运行逻辑

前面我们已经了解了RunLoop相关的结构的源码,知道了RunLoop大概的数据结构,那RunLoop到底是如何工作的呢?它的运行逻辑是什么?

我们了解过了每个mode中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以全部统称是RunLoop要处理的东西,那每一种具体对应我们了解的哪写事件呢?

Source0
触摸事件处理
performSelector:onThread:

Source1
基于系统Port(端口)的线程间通信
系统事件捕捉

Timers
NSTimer定时器
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWating)
Autorelease Pool (BeforWaiting)

注: UI的刷新并不是即时生效,比如说我们改变了view的backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,然后在RunLoop处理完所有的时间,进入休眠之前UI刷新。


这是大神总结的RunLoop的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。

这个图很容易理解,只有从06跳转到08这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第06步,如果存在Source1就直接跳转到08,在代码中使用了goto这个关键字,其实就是跳过了runloop休眠和唤醒这一部分的代码,直接跳转到了处理各种事件的这一部分。

下面我把源码做了一些删减,方便大家可以更清楚的梳理整个过程

// 这个是runloop入口函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */

// 通知Observers 即将进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers 即将退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

下面是核心方法

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

int32_t retVal = 0;
do {

//通知Observers 即将处理Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

//通知Observers 即将处理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

//处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

//处理source0,根据返回值决定在处理一次blocks
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);


// source1相关
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 是否有Source1 有的话跳转到handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}

didDispatchPortLastTime = false;

// 通知Observers: 即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//休眠
__CFRunLoopSetSleeping(rl);


//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);


__CFRunLoopUnsetSleeping(rl);

// 通知Observers: 即将醒来
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 标识标识 !!!!!
handle_msg:;

__CFRunLoopSetIgnoreWakeUps(rl);

//下面根据是什么唤醒的runloop来分别处理

if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}

// 被Timer唤醒
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被Timer唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被GCD唤醒
else if (livePort == dispatchPort) {

// 处理GCD相关
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

} else {
//被Source1唤醒
//处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;

}


//在处理一遍BLocks
__CFRunLoopDoBlocks(rl, rlm);


// 设置返回值 决定是否继续循环
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}

} while (0 == retVal);

return retVal;
}

图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就OK了。

细节补充

第一点

我们都知道RunLoop有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用CPU资源,提高程序性能。

这说明代码在执行到

//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。

举个例子,我们可以使用while(1){};这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是runloop,通过mach_msg使用了一些内核层的API,真的是实现了线程的休眠,让线程不再占用CPU资源。

第二点

RunLoop与线程的关系?

一个线程对应一个RunLoop对象。
RunLoop默认不创建,在第一次获取的时候创建,主线程中的默认存在RunLoop也是因为在底层代码中,提前获取过一次。
RunLoop储存在一个全局的字典中,线程是key,RunLoop是value。(源码中有所体现)
RunLoop会在线程结束时销毁。

链接:https://www.jianshu.com/p/705aa44405c0

收起阅读 »

RAC解析 - 自定义KVO

知识点概述1.KVO实现原理2.runtime使用目的给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。大体思路添加观察者的方法中- (void)SQ_addObserver:(NSObjec...
继续阅读 »

知识点概述

1.KVO实现原理
2.runtime使用

目的

给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。

大体思路

添加观察者的方法中

- (void)SQ_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;

会用runtime的方式手动创建一个其子类,并且将该对象变为该子类。该子类会复写观察方法中keyPath的setter方法,使这个setter被调用时利用runtime去调用observer的回调方法

-(void)observeValueForKeyPath:(NSString *)keyPath 
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;

实现

这里只做KVO的基本功能,当被观察者改变属性的时候通知观察者,所以定义如下方法

NSObject+SQKVO.h

/**
添加观察者

@param observer 观察者
@param keyPath 被观察的属性名
*/
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


/**
当被观察的观察属性改变的时候的回调函数

@param keyPath 所观察被观察者的属性名
@param object 被观察者
@param value 被观察的属性的新值
*/
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value;

@end

因为这里要用到runtime所以需要添加runtime的头文件

#import <objc/message.h>

而且因为用到objc_msgSend所以要改变一下工程的环境变量


一.动态生成子类

在被观察者调用- SQ_addObserver:forKeyPath:时首先动态生成一个其子类。

// 1.生成子类
// 1.1获取名称
Class selfClass = [self class];
NSString *className = NSStringFromClass(selfClass);
NSString *KVOClassName = [className stringByAppendingString:@"_SQKVO"];
const char *KVOClassNameChar = [KVOClassName UTF8String];
// 1.2创建子类
Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0);
// 1.3注册
objc_registerClassPair(KVOClass);

这里可以看到,我们将子类的类名命名为“类名”+“SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。
这里有个注意点,一般为动态创建的类名应尽量复杂一些避免重复。最好加上“”。

二.根据KeyPath动态添加对应的setter

1 确定setter的名字
举个例子,如果用户给的keyPath是name,应该动态添加一个-setName:的方法。而这个setter的名字是 "set" + "把keyPath变为首字母大写" + ":"
所以可以得出

NSString *setterString =
[NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);

2 利用class_addMethod()给子类动态添加方法

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

1、cls:
给哪个类添加方法。即新生成的子类,上面生成的 KVOClass。
2、name:
所添加方法的名称。即上一步生成的字符串 setterString。
3、imp:
所添加方法的实现。即这个方法的C语言实现,首先在下面先写一个C语言的方法。稍后会讲具体实现。

void setValue(id self, SEL _cmd, id newVale) {
}

4、types:
所添加方法的编码类型。setter的返回值是void,参数是一个对象(id)。void用"v"表示,返回值和参数之间用“@:”隔开,对象用"@"表示。最后我们可以得出结果"v@:@"。
具体其他的编码类型可以参考苹果文档。
ps: 这里说下为什么返回值和参数之间用“@:”隔开。“:”代表字符串,所有的OC方法都有两个隐藏参数在参数列表的最前面,“发起者”和 “方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个types其实是imp返回值和参数的编码。因为OC方法中返回值和参数之间必然有“发起者”和“SEL”隔着,所以“@:”自然而然就成了返回值和参数之间的分隔符。
当然我们还可以用@encode来得到我们想要的编码类型

NSString *encodeString =
[NSString stringWithFormat:@"%s%s%s%s",
@encode(void),
@encode(id),
@encode(SEL),
@encode(id)];

3 将当前对象的类变为我们所创建的子类的类型,即更改isa指针

object_setClass(self, KVOClass);

4 将keyPath和观察者关联(associate)到我们的对象上

用下面这个函数可以很方便的将一个对象用键值对的方式绑定到一个目标对象上。
*如果想了解跟多可以查找《Effective Objective-C》的第10条

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

1、object
目标对象

2、key
绑定对象的键,相当于NSDictionary的key
这里的key一般采用下面的方式声明:

static const void *SQKVOObserverKey = &SQKVOObserverKey;
static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;

这样做是因为若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。

3、value
绑定对象,相当于NSDictionary的value

4、policy
绑定对象的缓存策略
@property (nonatomic, weak) :OBJC_ASSOCIATION_ASSIGN
@property (nonatomic, strong) :OBJC_ASSOCIATION_RETAIN_NONATOMIC
@property (nonatomic, copy) :OBJC_ASSOCIATION_COPY_NONATOMIC
@property (atomic, strong) :OBJC_ASSOCIATION_RETAIN
@property (atomic, weak) :OBJC_ASSOCIATION_COPY

最后关联的代码:

objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);

三.setValue()的实现

这个函数的目的主要是:
1.利用objc_msgSend触发原先类的setter
2.利用objc_msgSend触发观察者的回调方法

1. 触发原先的setter方法

// 保存子类
Class KVOClass = [self class];

// 变回原先的类型,去触发setter
object_setClass(self, class_getSuperclass(KVOClass));
NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey);
NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);
objc_msgSend(self, setter, newVale);

2. 调用观察者的回调方法

id observer = objc_getAssociatedObject(self, SQKVOObserverKey);
objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);

3.改回KVO类

object_setClass(self, KVOClass);

四.实现空的回调方法

- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {

}

五.调用自定义的KVO

恭喜你看到这里,并且恭喜你已经成功了!

六.代码

代码下载地址

转自:https://www.jianshu.com/p/eb067f68c2b7

收起阅读 »

Objective-C高级编程笔记一(自动引用计数)

示例代码下载手动引用计数MRC内存管理的思考方式1、自己生成的对象自己持有2、不是自己生成的对象,自己也能持有3、不在需要自己持有的对象时释放4、不是自己持有的对象无法释放对象操作与Objective-C方法的对应实现一个MRCObject类:@impleme...
继续阅读 »

示例代码下载

手动引用计数

MRC内存管理的思考方式

1、自己生成的对象自己持有
2、不是自己生成的对象,自己也能持有
3、不在需要自己持有的对象时释放
4、不是自己持有的对象无法释放

对象操作与Objective-C方法的对应


实现一个MRCObject类:

@implementation MRCObject
- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);

[super dealloc];
}
+ (instancetype)object {
MRCObject *obj = [self allocObject];
[obj autorelease];
return obj;
}

+ (instancetype)allocObject {
MRCObject *obj = [[MRCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);

return obj;
}

@end

自己生成并持有对象:

MRCObject *obj = [MRCObject allocObject];

不是自己生成的对象也能持有:

MRCObject *obj = [MRCObject object];
[obj retain];

不在需要自己持有的对象时释放:

MRCObject *obj = [self allocObject];
[obj release];

无法释放自己没有持有的对象:

MRCObject *obj = [self allocObject];
[obj release];
[obj release];//会奔溃

autorelease

autorelease像c语言的自动变量来对待对象实例,当超出其作用域(相当于变量作用域),对象实例的release方法被调用。与c语言自动变量不同的是,可以autorelease的作用域。

autorelease的使用方法:

1、生成NSAutoreleasePool对象
2、调用已分配对象实例的autorelease方法
3、废弃NSAutoreleasePool对象

在应用程序中,由于主线程的NSRunloop对NSAutoreleasePool对象进行生成、持有和废弃处理。因此开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。如下图:


在大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,autorelease对象就不会被释放,因此会产生内存不足的现象。如下两段代码:

for (int index = 0; index < 1000; index++) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
}
for (int index = 0; index < 1000; index++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
[pool drain];
}

ARC

ARC规则

ARC有效时,id类型和对象类型同c语言其他类型不同,必须添加所有权修饰符。共如下4种所有权修饰符:

1、__strong修饰符
2、__weak修饰符
3、__unsafe_unretained修饰符
4、__outoreleasing修饰符

import "ARCObject.h"

实现一个ARCObject类:

@interface ARCObject ()
{
__strong id _strongObj;
__weak id _weakObj;
}

@end

@implementation ARCObject

- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);
}
+ (instancetype)allocObject {
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
return obj;
}
- (void)setStrongObject:(id)obj {
_strongObj = obj;
}
- (void)setWeakObject:(id)obj {
_weakObj = obj;
}

@end

__strong修饰符

__strong修饰符是所有id类型和对象类型默认的所有权修饰符,表示对对象的强引用,在超出其作用域或被重新赋值时被废弃。

{
ARCObject *obj = [ARCObject allocObject];
NSLog(@"作用域最后一行%@", obj);
}
NSLog(@"作用域已经结束");
ARCObject *obj = [ARCObject allocObject];
NSLog(@"重新赋值前%@", obj);
obj = [ARCObject allocObject];
NSLog(@"重新赋值前后%@", obj);

__strong、__weak、__outoreleasing修饰符的自动变量默认初始化为nil。

__weak修饰符

__weak修饰符与__strong修饰符相反,提供弱引用,弱引用不持有对象实例。

循环引用容易发生内存泄漏,内存泄漏就是应当废弃的对象在超出其生存周期后依然存在。可以使用__weak修饰符来避免。

ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
[aObj setStrongObject:bObj];
[bObj setStrongObject:aObj];
ARCObject *obj = [ARCObject allocObject];
[obj setStrongObject:obj];
ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
ARCObject *cObj = [ARCObject allocObject];
[aObj setWeakObject:bObj];
[bObj setWeakObject:aObj];
[cObj setWeakObject:cObj];

__weak修饰符有一个优点就是:在持有某对象的弱引用时,如果该对象被废弃,则该对象弱引用自动失效且被置为nil。

__unsafe_unretained修饰符

__unsafe_unretained修饰符,正如其名一样是不安全的所有权修饰符。尽管ARC的内存管理是编译器的工作,但是这一点需要注意特别注意,__unsafe_unretained修饰符的变量不属于编译器内存管理的对象。

__unsafe_unretained修饰符和__weak修饰符的变量一样不会持有对象,但是__unsafe_unretained修饰符的变量在销毁时并不会自动置为nil,在其地址被覆盖后就会因为反问垂悬指正而造成奔溃。因此__unsafe_unretained修饰符变量赋值给__strong修饰符变量时要确保对象的真实存在。因为__weak修饰符是在iOS5中实现的,__unsafe_unretained修饰符存在的意义就是在iOS4中代替__weak修饰符的作用。

ARCObject __unsafe_unretained *obj = nil;
{
ARCObject *obj1 = [ARCObject allocObject];
obj = obj1;
}
NSLog(@"%@(%@)", NSStringFromClass(obj.class), obj);

__outoreleasing修饰符

ARC有效时不能使用outorelease方法,也不能使用NSAutoreleasePool类。这样就导致outorelease无法直接使用,但实际上outorelease功能是起作用的。使用@outoreleasepool{}块代码来代替NSAutoreleasePool类对象的生成持有以及废弃。通过赋值给__outoreleasing修饰符的变量来代替调用outorelease方法,也就是说对象被注册到autoreleasepool中。

@autoreleasepool {
ARCObject __autoreleasing *obj1 = [ARCObject allocObject];
NSLog(@"autoreleasepool块最后一行%@", obj1);
}
NSLog(@"autoreleasepool块已经结束");

ARC有效时,cocoa中由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到outoreleasepool中。所以非显示的使用__outoreleasing修饰符也是可以的。

NSMutableArray __weak *array = nil;
NSLog(@"作用域块开始前%@", array);
{
NSMutableArray *arr = [NSMutableArray arrayWithObject:@(1)];
array = arr;
NSLog(@"作用域块最后一行%@", array);
}
NSLog(@"作用域块已经结束%@", array);

打印结果:

2019-03-28 11:56:52.316360+0800 ProfessionalExample[82984:16680615] 作用域块开始前(null)
2019-03-28 11:56:52.316538+0800 ProfessionalExample[82984:16680615] 作用域块最后一行(
1
)
2019-03-28 11:56:52.316627+0800 ProfessionalExample[82984:16680615] 作用域块已经结束(
1
)

id的指针和对象的指针在没有显式指定修饰符时会被附加上__outoreleasing修饰符。

- (BOOL)performOperationWithError:(ARCObject **)obj {
*obj = [ARCObject object];
return NO;
}

调用方法则为如下所示,自动转化为__autoreleasing修饰符:

[self performOperationWithError:<#(ARCObject *__autoreleasing *)#>];

id的指针和对象的指针变量必须指明所有权修饰符,并且赋值的所有权修饰符必须一致:

NSObject **pObj;//编报错,没有所有权修饰符
NSObject *obj = [[NSObject alloc] init];
NSObject *__autoreleasing*pObj = &obj;//编译报错,会更改所有权属性

纠正一个比较普遍的错误认知,for循环中并不是循环结束才释放循环内的局部变量,并不是所有产生大量对象的for循环中都需要加NSAutoreleasePool,而是产生大量autorelease对象时才需要添加。如下示例代码:

for (int index = 0; index < 2; index++) {
if (index == 0) {
NSLog(@"-------------begin");
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
}
if (index == 1) {
NSLog(@"-------------end");
}
}

下面是这段代码的打印内容:

2019-03-28 15:27:19.179194+0800 ProfessionalExample[85692:16955598] -------------begin
2019-03-28 15:27:19.179366+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)生成了
2019-03-28 15:27:19.179449+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)销毁了
2019-03-28 15:27:19.179521+0800 ProfessionalExample[85692:16955598] -------------end

ARC编码规则

1、不能使用retain/release/retainCount/autorelease
2、不能使用NSAllocateObject/NSDeallocateObject
3、须遵守内存管理的方法命名规则
4、不能显式调用dealloc方法
5、使用@autoreleasepool{}代替NSAutoreleasePool
6、不能使用NSZone
7、对象变量不能作为c语言结构体的成员
8、显式转换id和void *

内存管理的方法命名规则

以alloc/new/copy/mutableCopy开头的方法返回对象时,必须返回给调用方应当持有的对象。这在ARC有效时是一样的,不同的是以init开头的方法必须是实例方法且需要返回对象,该返回对象并不注册到autoreleasepool上。

对象变量不能作为c语言结构体的成员

要把对象类型变量加入到结构体中,需强制转为void *或者前面附加__unsafe_unretained修饰符。

显式转换id和void *

可以使用(__bridge)转换void *和OC对象,但是其安全性和赋值给__unsafe_unretained修饰符相近或者更低。如果管理时不注意赋值对象的所有者就会因为垂悬指针而奔溃或者内存泄漏。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
obj = (__bridge NSObject *)p;

__bridge_retained转换可使要赋值的变量持有所赋值的变量。__bridge_transfer则与之相反。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;
obj = (__bridge_transfer NSObject *)p;

NSObject对象与Core Fundation对象之间的相互转换,即免费桥(Toll-Freee-Bridge)转换。CFBridgingRetain函数(等价于__bridge_retained转换),CFBridgingRelease函数(等价于__bridge_transfer)。

NSObject *obj = [[NSObject alloc] init];
CFTypeRef ref = CFBridgingRetain(obj);
obj = CFBridgingRelease(ref);

属性

属性声明的属性与所有权修饰符对应关系


c数组

c静态数组,各修饰符的使用OC对象一样没有区别。

以__strong为例,其初始化为nil,超过作用域销毁:

{
ARCObject *array[2];
array[0] = [ARCObject allocObject];
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[1] = nil;
NSLog(@"array第二个元素:%@", array[1]);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:19:26.697408+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)生成了
2019-03-28 19:19:26.697661+0800 ProfessionalExample[88859:17353905] array第一个元素:<ARCObject: 0x6000005f8500>
2019-03-28 19:19:26.697761+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697845+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697930+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)销毁了
2019-03-28 19:19:26.697995+0800 ProfessionalExample[88859:17353905] 作用域块已经结束

c动态数组,c语言中动态数组声明用指针即id *array(NSObject **array)。需要注意如下几点:

1、_strong/__weak修饰符的OC变量初始化为nil,并不代表其指针初始化为nil。所以分配内存后,需要对其初始化为nil,否则非常危险。calloc函数分配的就是nil初始化后的内存,malloc函数分配内存后必须使用memset将内存填充为0(nil)。
2、必须置空_strong修饰符的态数数组内的元素,使其强引用失效,元素才能释放。因为动态数组的生命周期有开发者管理,编译器不能确定销毁动态数组内元素的时机。

{
ARCObject *__strong *array;
array = (ARCObject *__strong *)calloc(2, sizeof(ARCObject *));
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[0] = [ARCObject allocObject];
array[1] = [ARCObject allocObject];
array[0] = nil;
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
free(array);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:29:26.162245+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.162586+0800 ProfessionalExample[89048:17394552] array第二个元素:(null)
2019-03-28 19:29:26.162763+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)生成了
2019-03-28 19:29:26.162867+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a395c0>)生成了
2019-03-28 19:29:26.162945+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)销毁了
2019-03-28 19:29:26.163011+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.163083+0800 ProfessionalExample[89048:17394552] array第二个元素:<ARCObject: 0x600001a395c0>
2019-03-28 19:29:26.163160+0800 ProfessionalExample[89048:17394552] 作用域块已经结束

转自:https://www.jianshu.com/p/82849c350b0b

收起阅读 »

你确定你会写代码---iOS规范补充

Pod update注意1、先执行pod repo update 公司内部库specs2、再执行pod update --no-repo-update这样就不会update github_specs,速度快JSONSerialization涉及到JSON Ob...
继续阅读 »

Pod update注意

1、先执行pod repo update 公司内部库specs
2、再执行pod update --no-repo-update这样就不会update github_specs,速度快

JSONSerialization

涉及到JSON Object<->NSData数据转换的地方,注意对NSError的处理和JSON Object合法性的校验,如:

BOOL validate = [NSJSONSerialization isValidJSONObject:parament];
if (!validate) {
// 对不是合法的JSON对象错误进行处理
return;
}
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parament options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
// 对数据转换错误进行处理
return;
}

合法JSON对象满足:
1、Top level object is an NSArray or NSDictionary
2、All objects are NSString, NSNumber, NSArray, NSDictionary, or NSNull
3、All dictionary keys are NSStrings
4、NSNumbers are not NaN or infinity

补充一些代码规范、开发约定

写if else语句时可以else不换行紧跟if的}括号,但写if else if时,为了保持条件{}的可读性,务必请换行书写:

// if else
BOOL flag = YES;
if (flag) {

} else {

}

// if else if
BOOL flag = YES;
BOOL elseIfFlag = (1+1-1+2 == 5);
if (flag) {

}
// 这里换行书写
else if(elseIfFlag) {

}

对于@property声明的属性,如果初始化设置复杂,请采用懒加载getters方式,对于简单初始化的,应在.m文件中提供统一的-initData初始化数据的方法。

/// 懒加载方式-内部配置
@property(nonatomic, strong)UIView *redView;
/// 统一初始化
@property(nonatomic, strong)NSMutableArray *dataSourceArray;

/// 统一数据初始化
- (void)initData{
_dataSourceArray = [NSMutableArray new];
}

/// 懒加载
- (UIView *)redView{
if (!_redView) {
_redView = [UIView new];
_redView.backgroundColor = UIColor.redColor;
}
return _redView;
}

对于NSDictionary、NSArray等的初始化,为提高可读性起见,建议采用语法糖的初始化方式:

_dataSourceArray = [@[@"1", @"2"] mutableCopy];
_parameters = [@{@"action": @"add", @"id": @"22"} mutableCopy];

// X: 不推荐这样做
_dataSourceArray = [NSMutableArray new];
[_dataSourceArray addObject:@"1"];
[_dataSourceArray addObject:@"2"];

_parameters = [NSMutableDictionary new];
[_parameters setValue:@"add" forKey:@"action"];
[_parameters setValue:@"22" forKey:@"id"];

对于Category中的对外公有方法,务必采用categoryName_funcName的命名方式,以区别于主类里没有前缀的方法:

// TALPlayer+LogReport.h

/// 加载播放器
- (void)logReport_loadPlayer;
/// 开始播放
- (void)logReport_startPlay;

对于Category里的私有同名方法,可采用下划线方式如_mainClassFuncName以区别.
对于主类里的私有属性,在多个Category访问时,可采用属性中间件的方式,拆出一个独立的MainClass+InternalProperty来提供一些getters方法:

// TALPlayer+InternalProperty.h

- (TALPlayerLogModel *)internalProperty_logModel;
- (TALPlayerStaticsModel *)internalProperty_staticsModel;


// TALPlayer+InternalProperty.m

- (TALPlayerLogModel *)internalProperty_logModel{
// 这里为方便以后调试断点用,建议拆开2行写
id value = [self valueForKey:@"logModel"];
return value;
}

对于需要跟服务器交互的网络请求参数字符串,务必独立出对应Category的DataInfoKeys扩展文件,方便查询、注释、全局引用、修改和拼写纠错:

// TALPlayer+LogReportDataInfoKeys.h

/// action
extern NSString *const LogReportDataInfoActionKey;
/// 心跳
extern NSString *const LogReportDataInfoActionHeartBeatKey;
/// 严重卡顿
extern NSString *const LogReportDataInfoActionSeriousBufferKey;


// TALPlayer+LogReportDataInfoKeys.m

// action
NSString *const LogReportDataInfoActionKey = @"action";
/// 心跳
NSString *const LogReportDataInfoActionHeartBeatKey = @"heartbeat";
/// 严重卡顿
NSString *const LogReportDataInfoActionSeriousBufferKey = @"seriousbuffer";


// 使用
#import "TALPlayerLogReportDataInfoKeys.h"

NSMutableDictionary *info = [NSMutableDictionary new];
info[LogReportDataInfoActionKey] = LogReportDataInfoActionHeartBeatKey;
// info[XXXKey] = value;

对于.h及.m文件中默认#pragma mark的规范,推荐如下:

// XXX.h

#pragma mark - Protocol

#pragma mark - Properties

#pragma mark - Methods

// XXX.m

#pragma mark - Consts

#pragma mark - UI Components

#pragma mark - Data Properties

#pragma mark - Initial Methods

#pragma mark - Lifecycle Methods

#pragma mark - Override Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - XXXDelegate

#pragma mark - Getters

#pragma mark - Setters

如上相关#pragma字符在Xcode中的自动配置,有机会我会单独分享给大家。
Xcode FileTemplate路径:Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class.xctemplate/


对于OC文件中注释规范说明:

//MARK: mark here(类似于#pragma mark,只不过没有横线)

//TODO: todo here

//FIXME: fix me later

//???: What is this shit

//!!!: DO NOT TOUCH MY CODE

说明:
对于单行注释,尽量用//代替/**/格式,,务必请在//后加一个空格,再进行内容补充,如:// 这是单行注释而不要写成//这是单行注释
对于SDK内部私有方法,如果无参数,则采用/// 这是无参数方法注释格式;
对于SDK需要向外暴露的接口方法注释,请务必按照AppleDoc编写,写明@param、@return等:

/**
* 这是一个demo方法
* @param param1 第一个参数
* @param param2 第二个参数
* @return BOOL值
*/
- (BOOL)thisIsADemoFuncWithParam1: (NSString *)param1
param2: (NSInteger)param2{
return NO;
}

1、对于@property申明的SDK公开属性,务必写成/* 这是SDK公开属性注释 */,方便调用时Xcode提示;

2、对于SDK内部使用的属性,最好写成/// 这是属性注释而不是/**/;

3、另外,务必让对于>=2个参数的方法,各个参数折行对齐,务必保持.h和.m方法参数格式一致;

4、对于方法名的统一性说明:

  4.1 如果方法是功能性的,处理某些事件、计算、数据处理等的私有方法,则可定义方法名为handleXXX:,如-handleRedBtnClick:、-handleResponsedData:、-handlePlayerEvent:等;

  4.2 对于一些需要暴露的公有方法,则命名最好按照n的功能命名,如对于一个TALPlayer它可以play、stop、resume等;

  4.3 对于可以switch两种状态切换的状态方法,最好命名为toggleXXX:(BOOL)on,如- (void)toggleMute:(BOOL)on;

5、对于一些状态描述性的属性,可以用needs、is、should、has+adj/vb组合形式,如needsAutoDisplay、shouldAutoRotateOrientation、isFinished、hasData或hasNoData等;

对于一些NS_ENUM枚举定义,务必遵循统一前缀方式:

typedef enum : NSUInteger {
TALPlayerEventA = 0,
TALPlayerEventB,
TALPlayerEventC,
} TALPlayerEvent;
// 或者
typedef NS_ENUM(NSUInteger, MyEnum) {
MyEnumValueA,
MyEnumValueB,
MyEnumValueC,
};

6、对于一些全局宏的定义,务必SDK前缀全大写_NAME_组合如TALPLAYER_GLOBAL_MACRO_NAME,对于const类型的常量,务必加上k前缀,命名为kConstValue;

7、对于一些typedef的Block,命名最好指明Block的类别+用途,如TALPlayerLogReportHandler,如果有功能性区分的话,则可以定义为TALPlayerLogReportCompletionHandler、TALPlayerLogReportSuccessHandler、TALPlayerLogReportFailureHandler注意是名词组合形式;

8、调用Block时,一定要对block对象进行nil值判断,防止崩溃handler ? handler() : nil;

9、所有对于NSString校验的地方,都应该校验其length > 0,而不是!str;

10、所有对于NSURL校验的地方,都应该校验其[URL.scheme.lowercaseString isEqualToString: @"https"]方式,而不是!URL;

链接:https://www.jianshu.com/p/deb117eca9ea

收起阅读 »

在iOS中运用React Component的思路,效率更高的开发UI,更好的复用UI组件

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺...
继续阅读 »

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺带着把ES6的一些新特性也了解了一下,更好的函数调用方式,Class的引入,Promise的运用等等,其实最吸引我的还是在用了Weex之后,感受到了Component带来的UI复用,高效开发的快感。Weex是运用Vue.js来调用,渲染native控件,来达到one code, run everywhere。不管是Vue.js,还是React,最终都是朝着W3C WebComponent的标准走了(今年会发布的Vue 3.0在组件上的语法基本上跟React一样了)。这篇就来讲讲我对React Component的理解,还有怎么把这个标准也能在native上面做运用

demo源码

iOS UI开发的痛点

对iOS开发来说,最常用的UI组件就是UICollectionView了,就是所谓的一个列表页,现在的app大部分页面都是由一个列表来呈现内容的。对iOS开发者来说,我们可以封装每个UICollectionViewCell,从而可以在每个页面的UICollectionView中能够复用,但是痛点是,这个复用仅仅是UI上的复用,在每写一个新的页面(UIViewController)的时候,还是需要新建一个UICollectionView,然后再把UICollectionView的DataSource和Delegate方法再实现一遍,把这些Cell再在这些方法里重新生成一遍,才能让列表展现出来。比方说我们首页列表底部有猜你喜欢的cell,个人中心页面底部也有猜你喜欢的cell,这两个页面,都需要在自己拥有的UICollectionView中注册这个猜你喜欢的cell,返回这个猜你喜欢cell的高度,设置这个cell的model并刷新数据,如果有Header或者Footer的话,还得重新设置这些Header跟Footer。所以新写一个列表页面,对iOS开发者来说,还是很麻烦。

使用Weex或者RN开发原生列表页

使用Weex开发列表页的时候,我们组内的小伙伴都觉得很爽,很高效,基本上几行代码就能绘制出一个列表页,举个RN和weex的例子

// React
render() {
const cells = this.state.items.map((item, index) => {
if (item.cellType === 'largeCell') {
return <LargeCell cellData={item.entity}></LargeCell>
} else if (item.cellType === 'mediumCell') {
return <MediumCell cellData={item.entity}></MediumCell>
} else if (item.cellType === 'smallCell') {
return <SmallCell cellData={item.entity}></SmallCell>
}
});

return(
<Waterfall>
{ cells }
</Waterfall>
);
}

// Vue
<template>
<waterfall>
<cell v-for="(item, index) in itemsArray" :key="index">
<div class="cell" v-if="item.cellType === 'largeCell'">
<LargeCell :cellData="item.entity"></LargeCell>
</div>
<div class="cell" v-if="item.cellType === 'mediumCell'">
<MediumCell :cellData="item.entity"></MediumCell>
</div>
<div class="cell" v-if="item.cellType === 'smallCell'">
<SmallCell :cellData="item.entity"></SmallCell>
</div>
</cell>
</waterfall>
</template>

const

waterfall对应的就是iOS中的UICollectionView,waterfall这个组件中有cell的子组件,这些cell的子组件可以是我们自己定义的不同类型样式的cell组件。LargeCell,MediumCell,SmallCell对应的就是原生中的我们自定义的UICollectionViewCell。这些Cell子组在任何waterfall组件下面都可以使用,在一个waterfall组件下面,我们只需要把我们把在这个列表中需要展示的cell放进来,通过props把数据传到cell组件中即可。这种方式对iOS开发者来说,真的是太舒服了。在觉得开发很爽的同时,我也在思考,既然这种Component的方式用起来很爽,那么能不能也运用到原生开发中呢?毕竟我们大部分的业务需求还是基于原生来开发的。

React的核心思想

1、先来解释下React中的React Element和React Component
1.1、React Elements

const element = <div id='login-button>Login</div>

这段JSX表达式返回的就是一个React Element,React element描述了用户将在屏幕上看到的那个UI,跟DOM elements不一样的是,React elements是一个单纯的对象,仅仅是对将要呈现到屏幕上的UI的一个描述,并不是真正渲染好的UI,创建一个React element开销是极其小的,渲染的事情是由背后的React DOM来处理的。上面的那段代码相当于:

const element = React.createElement(
'div',
{id: 'login-button'},
'Login'
)

返回的React element对象相当于 =>

{
type: 'div',
props: {
children: 'Login',
id: 'login-button'
}
}

1.2 React Components

React中最核心的一个思想就是Component了,官方的解释是Component允许我们将UI拆分为独立可复用的代码片段,组件中可以包含多个其他组件,这样将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性(Readability)、可维护性(Maintainability)、可复用性(Reusability)和可测试性(Testability)。这也是 React 里用 Component 抽象所有 UI 的意义所在。

class Button extends React.Component {
render() {
const element = <div id='login-button>{ this.props.title }</div>
return (
<div>
{ element }
</div>
)
}

这段代码中Button就是一个React Component,这个component接受一个叫props的参数,返回描述UI的React element。

2、可以看出React Component接受props是一个对象,也就是所谓的一种数据结构,返回React Element也是一种对象,所谓的另外一种数据结构,所以我认为的React Component其实就是一个function,这个function的主要功能就是将一种数据结构(描述原始数据)转换成另外一种数据结构(描述UI)。React element仅仅是一个描述UI的对象,可以认为是一个中间状态,我们可以用最小的开销来创建或者销毁element对象。

3、React的核心思想总结下来就是这样的一个流程
1、原始数据到UI数据的转化 props -> React Component -> React Element
2、React Element的作用是将Component的创建跟描述状态分离,Component内部主要负责这个Component的构建,React Element主要用来做描述这个Component的状态
3、多个Component返回的多个Elements,这个流程是进行UI组合
4、React Element并不是一个渲染结果,React DOM的作用是将UI的状态(即Element)和UI的渲染分离,React DOM负责element的渲染
5、最后一个流程就是UI渲染了
上述这几个流程基本上代表了React的核心概念

怎么在iOS中运用React Component概念

说了这么多,其实iOS中缺少的就是这个Component概念,iOS原生的流程是原始数据到UI布局,再到UI绘制。复用的只是UI绘制结果的那个view(e.g. UICollectionViewCell)

在使用UICollectionView的时候,我们的数据都是通过DataSource方法返回给UICollectionView,UICollectionView拿到这些数据之后,就直接去绘制UICollectionViewCell了。所以每个列表页都得重新建一个UICollectionView,再引入自定义的UICollectionViewCell来绘制列表,所有的DataSource跟Delegate方法都得走一遍。所以我在想,我们可以按照React的那种方式来绘制列表么?将一个个UI控件抽象成一个个组件,再将这些组件组合到一起,绘制出最后的页面,React或者Weex的绘制列表其实就是waterfall这个列表component里面按照列表顺序插入自定义的cell component(组合)。那么我们其实可以在iOS中也可以有这个waterfall的component,这个component支持一个insertChildComponent:的方法,这个方法里就是插入自定义的CellComponent到waterfall这个组件中,并通过传入props来创建这个component。所以我就先定义了一个组件的基类BaseComponent

@protocol ComponentProtocol <NSObject>

/**
* 绘制组件
*
* @param view 展示该组件的view
*/
- (void)drawComponentInView:(UIView *)view withProps:(id)props;

/**
* 组件的尺寸
*
* @param props 该component的数据model
* @return 该组件的size
*/
+ (CGSize)componentSize:(id)props;

@end

@interface BaseComponent : NSObject <ComponentProtocol>

- (instancetype)initWithProps:(id)props;

@property (nonatomic, strong, readonly) id props;

所有的Component的创建都是通过传入props参数,来返回一个组件实例,每个Component还遵守一个ComponentProtocol的协议,协议里两个方法:

1、- (void)drawComponentInView:(UIView *)view withProps:(id)props; 每个component通过这个方法来进行native控件的绘制,参数中view是将会展示该组件的view,比方说WaterfallComponent中的该方法view为UIViewController的view,因为UIViewController的view会用来展示WaterfallComponent这个组件,'props'是该组件创建时传入的参数,这个参数用来告诉组件应该怎样绘制UI
2、+ (CGSize)componentSize:(id)props; 来描述组件的尺寸。

有了这个Component概念之后,我们原生的绘制流程就变成

1、创建Component,传入参数props
2、Component内部执行创建代码,保存props
3、当页面需要绘制的时候(React中的render命令),component内部会执行- (void)drawComponentInView:(UIView *)view withProps:(id)props;方法来描述并绘制UI

原生代码中想实现React element,其实不是一件简单的事情,因为原生没有类似JSX这种语言来生成一套只用来描述UI,并不绘制UI的中间状态的对象(可以做,比方说自己定义一套语法来描述UI),所以目前我的做法是在component内部,等到绘制命令来了之后,通过在- (void)drawComponentInView:(UIView *)view withProps:(id)props方法中,调用原生自定义的UIKit控件,通过props来绘制该UIKit

所以将通过封装component的方式,我们之前UIKit代表的UI组件转换成组件,把这些组件一个个单独抽离出来,再通过搭积木的方式,将各种组件一个个组合到一起,怎么绘制交给component内部去描述,而不是交给每个页面对应的UIViewController

Demo

Demo中,我会创建一个WaterfallComponent组件,还有多个CellComponent来绘制列表页,每个不一样列表页面(UIViewController)都可以创建一个WaterfallComponent组件,然后将不一样的CellComponent按照顺序插入到WaterfallComponent组件中,即可完成绘制列表,不需要每个页面再去处理UICollectionView的DataSource,Delegate方法。


WaterfallComponent内部会有一个UICollectionView,WaterfallComponent的insertChildComponent方法中,会创建一个dataController来管理数据源,并用来跟UICollectionView的DataSource方法进行交互从而绘制出列表页,最终UIViewController中绘制列表的方法如下:

self.waterfallComponent = [[WaterfallComponent alloc] initWithProps:nil];

for (NSDictionary *props in datas) {
if ([props[@"type"] isEqualToString:@"1"]) {
FirstCellComponent *cellComponent = [[FirstCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
} else if ([props[@"type"] isEqualToString:@"2"]) {
SecondCellComponent *cellComponent = [[SecondCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
}
}
[self.waterfallComponent drawComponentInView:self.view withProps:nil];

这样,每个我们自定义的Cell就可以以CellComponent的形式,被按照随意顺序插入到WaterfallComponent,从而做到了真正意义上的复用,Demo已上传到GitHub上,有兴趣的可以看

总结

React的核心思想是将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性、可维护性、可复用性和可测试性。这也是 React 里用 Component 抽象所有 UI 的意义所在。
原生开发中,使用Component的概念,用Component去抽象UIKit控件,也能达到同样的效果,这样也能统一每个开发使用UICollectionView时候的规范,也能统一对所有列表页的数据源做一些统一处理,比方说根据一个逻辑,统一在所有列表页,插入一个广告cell,这个逻辑完全可以在WaterfallComponent里统一处理。

思考

目前我们只用到了Component这个概念,其实React中,React Element的概念也是非常核心的,React Element隔离了UI描述跟UI绘制的逻辑,通过JSX来描述UI,并不去生成,绘制UI,这样我们能够以最小的代价来生成或者销毁React Elements,然后在交付给系统绘制elements里描述的UI,那么如果原生里也有这一套模板语言,那么我们就能真正做到在Component里,传入props,返回一个element描述UI,然后再交给系统去绘制,这样还能省去cell的创建,只创建CellComponent即可。其实我们可以通过定义一套语义去描述UI布局,然后通过解析这套语义,通过Core Text去做绘制,这一套还是值得我再去思考的。

链接:https://www.jianshu.com/p/bc4b13a0d312

收起阅读 »

Swift 5.0 值得关注的特性:增加 Result<T, E: Error> 枚举类型

HackingSwift: What’s new in Swift 5.0Result<T> 还是 Result<T, E: Error>背景在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 er...
继续阅读 »

HackingSwift: What’s new in Swift 5.0
Result<T> 还是 Result<T, E: Error>

背景

在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 error。因为数据可能获取成功,也可能失败。因此回调中的数据和错误都是 optional 类型。
比如 CloudKit 中保存数据的一个函数就是这样:

func save(_ record: CKRecord, completionHandler: @escaping (CKRecord?, Error?) -> Void)

这种形式的缺点是没有体现出两种结果的互斥关系:如果数据成功获取到了,那么 error 一定为空。如果 error 有值,数据一定是获取失败了。

Swift 中枚举的能力相比 OC 有着很大的进步,每个枚举值除了可以是常规的基础类型,还可以是一个关联的类型。有了这样的特性后用枚举来优化返回结果的数据结构显得水到渠成:

enum Result<Success, Failure> where Failure : Error {

/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)
}

基本用法

定义异步返回结果是 Int 类型的函数:

func fetchData(_ completionHandler: @escaping (Result<Int, Error>) -> Void) {
DispatchQueue.global().async {
let isSuccess = true
if isSuccess {
let resultValue = 6
return completionHandler(.success(resultValue))
} else {
let error = NSError(domain: "custom error", code: -1, userInfo: nil)
return completionHandler(.failure(error))
}
}
}

返回值的类型通过泛型进行约束,Result 第一个泛型类型表示返回值的类型,第二个类型表示错误的类型。对 Result 赋值和常规的枚举一样:

let valueResult: Result<Int, CustomError> = Result.success(4)

// 因为 swift 中会进行类型推断,编译器在确认返回的是 `Result` 类型后,可以省略枚举类型的声明
let errorResult = .failure(CustomError.inputNotValid)

取出 Result 值和获取普通的关联类型枚举是一样的:

fetchData { (result) in
switch result {
case .success(let value):
print(value)
case .failure(let error)
print(error.localizedDescription)
}
}

如果你只想要获取其中一项的值,也可以直接用 if case 拆包:

fetchDate { (result) in
if case .success(let value) = result {
print(value)
}
}

可以判等

Enum 是一个值类型,是一个值就应该可以判断是否相等。如果 Result 的成功和失败的类型都是 Equatable,那么 Result就可以判等,源码如下:

extension Result : Equatable where Success : Equatable, Failure : Equatable { }

类似的,如果是成功和失败的类型都是 Hashable,那么 Result 也是 Hashable:

extension Result : Hashable where Success : Hashable, Failure : Hashable { }

如果实现了 Hashable ,可以用来当做字典的 key。

辅助的 API

map、mapError
与 Dictionary 类似,Swift 为 Result 提供了几个 map value 和 error 的方法。

let intResult: Result<Int, Error> = Result.success(4)
let stringResult = x.map { (value) -> Result<String, Error> in
return .success("map")
}

let originError = NSError(domain: "origin error", code: -1, userInfo: nil)
let errorResult: Result<Int, Error> = .failure(originError)
let newErrorResult = errorResult.mapError { (error) -> Error in
let newError = NSError(domain: "new error", code: -2, userInfo: nil)
return newError
}

flatMap、flatMapError
map 返回的是具体的结果和错误, flatMap 闭包中返回的是 Result 类型。如果 Result 中包含的是数据,效果和 map 一致,替换数据;如果 Result 中包含的是错误,那么不替换结果。

let intResult: Result<Int, Error> = Result.success(4)

// 替换成功
let flatMapResult = intResult.flatMap { (value) -> Result<String, Error> in
return .success("flatMap")
}

// 没有执行替换操作,flatMapIntResult 值还是 intResult
let flatMapIntResult = intResult.flatMap { (value) -> Result<String, Error> in
return .failure(NSError(domain: "origin error", code: -1, userInfo: nil))
}

get
很多时候只关心 Result 的值,Swift 提供了 get() 函数来便捷的直接获取值,需要注意的是这个函数被标记为 throws,使用时语句前需要加上 try:

let intResult: Result<Int, Error> = Result.success(4)

let value = try? intResult.get()

可抛出异常的闭包初始化器

很多时候获取返回值的闭包中可能会发生异常代表获取失败的错误,基于这个场景 Swift 提供了一个可抛出异常的闭包初始化器:

enum CustomError: Error, Equatable {
case inputNotValid
}

let fetchInt = { () -> Int in
if true {
return 4
} else {
throw CustomError.inputNotValid
}
}

let result: Result<Int, Error> = Result { try fetchInt() }

需要提醒是通过这种方式声明的 Result 的 error 类型只能是 Error,不能指定特定的 Error。

转自:https://www.jianshu.com/p/a3712edc9367

收起阅读 »

运行时Hook所有Block方法调用的技术实现

1.方法调用的几种Hook机制iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。当我们想Hook一...
继续阅读 »

1.方法调用的几种Hook机制

iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。

当我们想Hook一个OC类的某些具体的方法时可以通过Method Swizzling技术来实现、当我们想Hook动态库中导出的某个C函数时可以通过修改导入函数地址表中的信息来实现(可以使用开源库fishhook来完成)、当我们想Hook所有OC类的方法时则可以通过替换objc_msgSend系列函数来实现。。。

那么对于Block方法呢而言呢?

2.Block的内部实现原理和实现机制简介

这里假定你对Block内部实现原理和运行机制有所了解,如果不了解则请参考文章《深入解构iOS的block闭包实现原理》或者自行通过搜索引擎搜索。

源程序中定义的每个Block在编译时都会转化为一个和OC类对象布局相似的对象,每个Block也存在着isa这个数据成员,根据isa指向的不同,Block分为__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三种类型。也就是说从某种程度上Block对象也是一种OC对象。下面的类图描述了Block类的层次结构。


Block类以及其派生类在CoreFoundation.framework中被定义和实现,并且没有对外公开。

每个Block对象在内存中的布局,也就是Block对象的存储结构被定义如下(代码出自苹果开源出来的库实现libclosure中的文件Block_private.h):

//需要注意的是下面两个只是模板,具体的每个Block定义时总是按这个模板来定义的。

//Block描述,每个Block一个描述并定义在全局数据段
struct Block_descriptor_1 {
uintptr_t reserved; //记住这个变量和结构体,它很重要!!
uintptr_t size;
};

//Block对象的内存布局
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1 *descriptor;
// imported variables,这里是每个block对象的特定数据成员区域
};

这里要关注一下struct Block_descriptor_1中的reserved这个数据成员,虽然系统没有用到它,但是下面就会用到它而且很重要!

在了解了Block对象的类型以及Block对象的内存布局后,再来考察一下一个Block从定义到调用是如何实现的。就以下面的源代码为例:

int main(int argc, char *argv[])
{
//定义
int a = 10;
void (^testblock)(void) = ^(){
NSLog(@"Hello world!%d", a);
};

//执行
testblock();

return 0;
}

在将OC代码翻译为C语言代码后每个Block的定义和调用将变成如下的伪代码:

//testblock的描述信息
struct Block_descriptor_1_fortestblock {
uintptr_t reserved;
uintptr_t size;
};

//testblock的布局存储结构体
struct Block_layout_fortestblock {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1_fortestblock *descriptor;
int m_a; //外部的传递进来的数据。
};

//testblock函数的实现。
void main_invoke_fortestblock(struct Block_layout_fortestblock *cself)
{
NSLog(@"Hello world!%d", cself->m_a);
}

//testblock对象描述的实例,存储在全局内存区
struct Block_descriptor_1_fortestblock _testblockdesc = {0, sizeof(struct Block_layout_fortestblock)};

int main(int argc, char *argv[])
{
//定义部分
int a = 10;
struct Block_layout_fortestblock testblock = {
.isa = __NSConcreteStackBlock,
.flags =0,
.reserved = 0,
.invoke = main_invoke_fortestblock,
.descriptor = & _testblockdesc,
.m_a = a
};

//调用部分
testblock.invoke();

return 0;
}

可以看出Block对象的生成和调用都是在编译期间就已经固定在代码中了,它不像其他OC对象调用方法时需要通过runtime来执行间接调用。并且线上程序中所有关于Block的符号信息都会被strip掉。所以上述的所介绍的几种Hook方法都无法Hook住一个Block对象的函数调用。

如果想要Hook住系统的所有Block调用,需要解决如下几个问题:
a. 如何在运行时将所有的Block的invoke函数替换为一个统一的Hook函数。
b. 这个统一的Hook函数如何调用原始Block的invoke函数。
c. 如何构建这个统一的Hook函数。

3.实现Block对象Hook的方法和原理

一个OC类对象的实例通过引用计数来管理对象的生命周期。在MRC时代当对象进行赋值和拷贝时需要通过调用retain方法来实现引用计数的增加,而在ARC时代对象进行赋值和拷贝时就不再需要显示调用retain方法了,而是系统内部在编译时会自动插入相应的代码来实现引用计数的添加和减少。不管如何只要是对OC对象执行赋值拷贝操作,最终内部都会调用retain方法。

Block对象也是一种OC对象!!

每当一个Block对象在需要进行赋值或者拷贝操作时,也会激发对retain方法的调用。因为Block对象赋值操作一般是发生在Block方法执行之前,因此我们可以通过Method Swizzling的机制来Hook 类的retain方法,然后在重写的retain方法内部将Block对象的invoke数据成员替换为一个统一的Hook函数!

通过考察__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三个类的实现发现这三个类都重载了NSObject的retain方法,这样在执行Method Swizzling时就不需要对NSObject的retain方法执行替换,而只要对上述三个类的retain执行替换即可。

你可以说出为什么这三个派生类都会对retain方法进行重载吗?答案可以从这三种Block的类型定义以及所表示的意义中去寻找。

Block技术不仅可以用在OC语言中,LLVM对C语言进行的扩展也能使用Block,比如gcd库中大量的使用了Block。在C语言中如果对一个Block进行赋值或者拷贝系统需要通过C库函数:

//函数声明在Block.h头文件汇总
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

来实现,这个函数定义在libsystem_blocks.dylib库中,并且库实现已经开源:libclosure。因此可以借助fishhook库来对__Block_copy这个函数进行替换处理,然后在替换的函数函数中将一个Block的原始的invoke函数替换为统一的Hook函数。

另外一个C语言函数objc_retainBlock,也是实现了对Block进行赋值时的引用计数增加,这个函数内部就是简单的调用__Block_copy方法。因此我们也可以添加对objc_retainBlock的替换处理。

解决了第一个问题后,接下来再解决第二个问题。还记得上面提到过的struct Block_descriptor_1中的reserved这个数据成员吗? 当我们通过上述的方法对所有Block对象的invoke成员替换为一个统一的Hook函数前,可以将Block对象的原始invoke函数保存到这个保留字段中去。然后就可以在统一的Hook函数内部读取这个保留字段中的保存的原始invoke函数来执行真实的方法调用了。

因为一个Block对象函数的第一个参数其实是一个隐藏的参数,这个隐藏的参数就是Block对象本身,因此很容易就可以从隐藏的参数中来获取到对应的保留字段。

下面的代码将展示通过方法交换来实现Hook处理的伪代码

struct Block_descriptor {
void *reserved;
uintptr_t size;
};

struct Block_layout {
void *isa;
int32_t flags; // contains ref count
int32_t reserved;
void *invoke;
struct Block_descriptor *descriptor;
};

//统一的Hook函数,这里以伪代码的形式提供
void blockhook(void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}
//模拟器下如果返回类型是结构体并且大于16字节那么第一个参数是返回值保存的内存地址,block对象变为第二个参数
void blockhook_stret(void *pret, void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}

//执行Block对象的方法替换处理
void replaceBlockInvokeFunction(const void *blockObj)
{
struct Block_layout *layout = (struct Block_layout*)blockObj;
if (layout != NULL && layout->descriptor != NULL){
int32_t BLOCK_USE_STRET = (1 << 29); //如果模拟器下返回的类型是一个大于16字节的结构体,那么block的第一个参数为返回的指针,而不是block对象。
void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
if (layout->invoke != hookfunc){
layout->descriptor->reserved = layout->invoke;
layout->invoke = hookfunc;
}
}
}

void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSStackBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSStackBlock_retain_old(obj, cmd);
}

void *(*__NSMallocBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSMallocBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSMallocBlock_retain_old(obj, cmd);
}

void *(*__NSGlobalBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSGlobalBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSGlobalBlock_retain_old(obj, cmd);
}

int main(int argc, char *argv[])
{
//因为类名和方法名都不能直接使用,所以这里都以字符串的形式来转换获取。
__NSStackBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSStackBlock"), sel_registerName("retain"), (IMP)__NSStackBlock_retain_new, nil);
__NSMallocBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSMallocBlock"), sel_registerName("retain"), (IMP)__NSMallocBlock_retain_new, nil);
__NSGlobalBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSGlobalBlock"), sel_registerName("retain"), (IMP)__NSGlobalBlock_retain_new, nil);

return 0;
}

解决了第二个问题后,就需要解决第三个问题。上面的统一Hook函数blockhook和block_stret只是伪代码实现,因为任何一个Block中的函数的参数类型和个数是不一样的,而且统一Hook函数也需要在适当的时候调用原始的默认Block函数实现,并且不能破坏参数信息。为了解决这些问题就使得这个统一的Hook函数不能用高级语言来实现,而只能用汇编语言来实现。下面就是在arm64位体系下的实现代码:

.text
.align 5
.private_extern _blockhook
_blockhook:
//为了不破坏原有参数,这里将所有参数压入栈中
stp q6, q7, [sp, #-0x20]!
stp q4, q5, [sp, #-0x20]!
stp q2, q3, [sp, #-0x20]!
stp q0, q1, [sp, #-0x20]!
stp x6, x7, [sp, #-0x10]!
stp x4, x5, [sp, #-0x10]!
stp x2, x3, [sp, #-0x10]!
stp x0, x1, [sp, #-0x10]!
stp x8, x30, [sp, #-0x10]!

//这里可以添加任意逻辑来进行hook处理。

//这里将所有参数还原
ldp x8, x30, [sp], #0x10
ldp x0, x1, [sp], #0x10
ldp x2, x3, [sp], #0x10
ldp x4, x5, [sp], #0x10
ldp x6, x7, [sp], #0x10
ldp q0, q1, [sp], #0x20
ldp q2, q3, [sp], #0x20
ldp q4, q5, [sp], #0x20
ldp q6, q7, [sp], #0x20

ldr x16, [x0, #0x18] //将block对象的descriptor数据成员取出
ldr x16, [x16] //获取descriptor中的reserved成员
br x16 //执行reserved中保存的原始函数指针。
LExit_blockhook:

对于x86_64/arm32位系统来说,如果block函数的返回是一个结构体并且长度超过16字节(arm32是8字节)。那么block对象里面的flags属性就会设置为BLOCK_USE_STRET。而x86_64/arm32位系统对于这种返回类型的函数就会将返回值存放到第一个参数所指向的内存中,同时会把原本的block对象变化为第二个参数,因此需要对这种情况进行特殊处理。

关于在运行时Hook所有Block方法调用的技术实现原理就介绍到这里了。当然一个完整的系统可能需要其他一些能力:

1、如果你只想Hook可执行程序中定义的Block,那么请参考我的文章:深入iOS系统底层之映像操作API介绍 中的内容来实现Hook函数的过滤处理。
2、如果你不想借助Block_descriptor中的reserved来保存原始的invoke函数,那么可以参考我的文章:Thunk程序的实现原理以及在iOS中的应用(二)中介绍的技术来实现统一Hook函数以及完成对原始invoke函数的调用技术。

具体完整的代码可以访问我的github中的项目:YSBlockHook。这个项目以AOP的形式实现了真机arm64位模式下对可执行程序中所有定义的Block进行Hook的方法,Hook所做的事情就是在所有Block调用前,打印出这个Block的符号信息。

链接:https://www.jianshu.com/p/0a3d00485c7f

收起阅读 »

iOS逆向(8)-Monkey、Logos

由于最近微信大佬发飙,罚了红包外挂5000万大洋,这就让人很慌了,别说罚我5000万,5000块我都吃不消。所以笔者决定以后不用微信做例子了。换成优酷了😈。本文会对优酷的设置页面增加一个开启/关闭屏蔽广告的Cell(仅UI)。效果可见下文配图。在之前的几篇文章...
继续阅读 »

由于最近微信大佬发飙,罚了红包外挂5000万大洋,这就让人很慌了,别说罚我5000万,5000块我都吃不消。所以笔者决定以后不用微信做例子了。换成优酷了😈。

本文会对优酷的设置页面增加一个开启/关闭屏蔽广告的Cell(仅UI)。效果可见下文配图。

在之前的几篇文章里已经介绍了APP重签名,代码注入,Hook原理,可以发现,将工程建好,脚本写好,我们就可以以代价非常小的方式对一个第三方的APP进行分析。
那么是否一种工具,可以将重签名,代码注入,Hook源代码,class-dump,Cydia Substrate,甚至是恢复符号表这些功能,集成在一个工程里面,让真正的逆向小白也能享受逆向的乐趣呢?
答案是肯定的,Monkey就是这样的一个非越狱插件开发集成神器!

老规矩,片头先上福利:点击下载demo
这篇文章会用到的工具有:

1、MonkeyDev
2、博主自己砸壳的优酷ipa包 提取码: xtua
3、砸壳后的SimpleAppDemo.ipa 提取码: afnc

一、Monkey

什么是Monkey?
原有iOSOpenDev的升级,非越狱插件开发集成神器!

可以使用Xcode开发CaptainHook Tweak、Logos Tweak 和 Command-line Tool,在越狱机器开发插件,这是原来iOSOpenDev功能的迁移和改进。

1、只需拖入一个砸壳应用,自动集成class-dump、restore-symbol、* Reveal、Cycript和注入的动态库并重签名安装到非越狱机器。
2、支持调试自己编写的动态库和第三方App
3、支持通过CocoaPods第三方应用集成SDK以及非越狱插件,简单来说就是通过CocoaPods搭建了一个非越狱插件商店。

环境要求

使用工具前确保如下几点:

1、安装最新的theos

sudo git clone --recursive https://github.com/theos/theos.git /opt/theos

2、安装ldid(如安装theos过程安装了ldid,跳过)

brew install ldid

安装

你可以通过以下命令选择指定的Xcode进行安装:

sudo xcode-select -s /Applications/Xcode-beta.app

默认安装的Xcode为:

xcode-select -p

执行安装命令:

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-install)"

卸载

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-uninstall)"

更新
如果没有发布特殊说明,使用如下命令更新即可:

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-update)"

安装/更新之后重启下Xcode再新建项目。如果看到如下选项,即代表安装成功,如果没有,重复上面步骤再来一遍。


二、Logos

Logos是Thoes开发的一套组件,可非常方便用于的Hook OC代码。

接下来我们就介绍下Logos的简单用法,最后运用Monkey和Logos给优酷增加一点UI。

1、创建一个简单的工程
创建工程SimpleAppDemo,里面只有一个按钮,点击按钮弹出一个Alert。 点击下载:SimpleAppDemo
按钮对应的方法为:

- (IBAction)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来啦" message:@"老弟😁😁😁" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

2、砸壳
对SimpleAppDemo参数的ipa文件进行砸壳,砸壳过程就不在这详细描述了,这里有笔者已经砸壳好的ipa:SimpleAppDemo.ipa 提取码: afnc

3、新建一个Monkey工程
取名LogosDemo,将下面下载好的SimpleAppDemo.ipa,放到工程对应的目录下:


配好证书(随意一个能在手机上运行的证书即可),Run。运行成功~

4、玩转Logos
在上一步建好的Monkey工程中,可以发现在目录有一个Logos目录:


默认有两个文件LogosDemoDylib.xm和LogosDemoDylib.mm。
其中Logos语句就是写在LogosDemoDylib.xm中的,LogosDemoDylib.mm是根据LogosDemoDylib.xm中的内容自动生成的。
接下来,咱们根据几个需求来介绍Logos的一些常用的用法。

1、更改点击按钮的弹框内容(hook)
由于需要更改弹窗,所以首先导入UIKit框架。

#import <UIKit/UIKit.h>

由于咱们手上有源码,所以可以直接跳过动态分析的这一步,直接就知道按钮所处的页面是叫做ViewController,按钮的响应方法是:

- (IBAction)tapAction:(id)sender

利用hook命令:

#import <UIKit/UIKit.h>

// hook + 类名
%hook ViewController
// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

%end

运行项目,发现按钮已经被成功hook了。


2、调用原方法(orig)

#import <UIKit/UIKit.h>

%hook ViewController

- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
// 调用原方法
%orig;
}

%end

3、新增一个方法,并且调用(new)

由于在Monkey工程里面是编译不到源码的,所以无论是新增的方法,还是调用原工程中的方法,都是无法通过编译的,所以都需要使用interface申明每一个方法。

#import <UIKit/UIKit.h>

// 这里只是为了申明
@interface ViewController

- (void)newFunC;

@end

%hook ViewController

// 新增方法关键字new
%new
- (void)newFunC{
NSLog(@"newFunC");
}

// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
[self newFunC];
// 调用原方法关键字orig
%orig;
}

%end

文中所有的Demo都在这可以下载到:Dmoe

Logos除了以上hook,end,orig,new这几种关键字,还有:
%subclass:增加一个类
%log:打印,类似NSLog
%group: 给代码分组,可以用于在不同环境加载不同的代码,比如iOS8加载group1,iOS9加载group2,如果部分中,默认所有代码在名为「_ungrouped」的隐藏分组中。
...

所有的Logos语法都可以在官方文档中查询得到。

5、给优酷加UI

首先在这里下载笔者自己砸壳后优酷ipa包(arm64架构的):优酷(砸壳).ipa 提取码: xtua

Step 1、新建工程YouKu

同样的新建一个Monkey工程,取名YouKu,将下载好的ipa包放入工程对应的TargetApp目录下。Run。同样是重签名成功。

在上面的Demo中,我们是对我们直接的工程进行HOOK,由于我们手上有源码,所以我们越过了最难的一个步骤:动态分析。
而我们现在要对优酷进行Hook,但我们手上是没有优酷的源码的,所以此时此刻就需要对其进行动态分析了。
下面我将结合Xcode和class dump对优酷的设置页面简单的进行分析。

Step 2、class dump

class-dump is a command-line utility for examining the Objective-C segment of Mach-O files. It generates declarations for the classes, categories and protocols. This is the same information provided by using 'otool -ov', but presented as normal Objective-C declarations.
简单说就是一个可以导出一个MachO文件的所有头文件信息(包括Extension)

在文首有提到Monkey除了重签名,还集成了class dump的功能,所以我们需要做的就仅仅是开启这个功能:


Run!成功之后可以发现在工程目录下多了一个文件夹Youkui4Phone_Headers,其中就是优酷的所有的头文件了。


Step 3、分析优酷设置页面

工程Run成功后,点击进入设置页面(不用登录),如下图:


我们现在要做的就是在这个页面的TableView的最后一行加上Cell,里面有个Switch,用于打开/关闭屏蔽广告功能(只是UI,这篇文章不牵扯到屏蔽广告的具体实现,如果你需要,点个小心心,持续关注我哦😀😀😀)。

利用伟大的Xcode我们可以非常清晰的看到,设置页面的DataSource和Delegate都是在SettingViewController中,


咱们就找到Hook的类名:SettingViewController
需要Hook的方法自然就是TableView的那些DataSource和Delegate了。

这里需要额外提到的一点是,在文章开始的时候就说了Monkey已经将Cydia Substrate集成进去了,所以我们可以直接使用Cydia Substrate的相关功能了。

在这里我们需要拿到这个页面TableView的对应的变量,我们就需要使用到Cydia Substrate的功能了。打开上文中获取到优酷的所有的头文件,所有SettingViewController,发现其只有一个TableView变量:_tabview。
那么毫无疑问,就是他了!
而获取它的方法是:

MSHookIvar <UITableView *>(self,"_tabview")

一个reloadData的简单使用:

[MSHookIvar <UITableView *>(self,"_tabview") reloadData];

其他的UI代码在这里就不一一解释了,全部代码如下,当然在Demo中也是有的,其中包括了数据的简单持久化功能:

#import <UIKit/UIKit.h>
#define FYDefaults [NSUserDefaults standardUserDefaults]
#define FYSwitchUserDefaultsKey @"FYSwitchUserDefaultsKey"

@interface SettingViewController
- (long long)numberOfSectionsInTableView:(id)arg1;
@end

%hook SettingViewController

%new
-(void)switchChangeAction:(UISwitch *)switchView{
[FYDefaults setBool:switchView.isOn forKey:FYSwitchUserDefaultsKey];
[FYDefaults synchronize];
[MSHookIvar <UITableView *>(self,"_tabview") reloadData];
}

//多少组
- (long long)numberOfSectionsInTableView:(id)arg1{
UITableView * tableView = MSHookIvar <UITableView *>(self,"_tabview");
NSLog(@"fy_numberOfSectionsInTableView:");
// 额外增加一个
return %orig+1;
}

//每组多少行
- (long long)tableView:(UITableView *)tableView numberOfRowsInSection:(long long)section{
NSLog(@"fy_numberOfRowsInSection:");
//定位设置界面,并且是最后一个
if(section == [self numberOfSectionsInTableView:tableView]-1){
return 1;
}
else{
return %orig;
}
}

//返回高度
- (double)tableView:
(UITableView *)tableView heightForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_heightForRowAtIndexPath:");
//定位设置界面,并且是最后一个
if([indexPath section] ==[self numberOfSectionsInTableView:tableView]-1){
return 44;
}
else{
return %orig;
}
}


//每一个Cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_cellForRowAtIndexPath:");
//定位设置界面,并且是最后一组
if([indexPath section] == [self numberOfSectionsInTableView:tableView]-1){
UITableViewCell * cell = nil;
if([indexPath row] == 0){
static NSString *swCell = @"SwCellIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:swCell];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:nil];
}
cell.textLabel.text = @"免广告";
// 免广告开关
UISwitch *switchView = [[UISwitch alloc] init];
switchView.on = [FYDefaults boolForKey:FYSwitchUserDefaultsKey];
[switchView addTarget:self action:@selector(switchChangeAction:) forControlEvents:(UIControlEventValueChanged)];
cell.accessoryView = switchView;
cell.imageView.image = [UIImage imageNamed:([FYDefaults boolForKey:FYSwitchUserDefaultsKey] == 1) ? @"unlocked" : @"locked"];
}
cell.backgroundColor = [UIColor whiteColor];
return cell;

}else{
return %orig;
}
}

%end

最后的效果


6、为什么Monkey这么牛逼

查看重新编译后的app文件,可以发现其中的Framework多了很多东西:


从这可以得知,原来Monkey其实也是通过将诸多的动态库(包括自己的工程)注入的形式,实现了这些功能。

三、总结

在这片文章中主要介绍了Monkey的一些用法已经Logos的基本语法。而在上一篇其实留了一个小尾巴,就是Cycript,笔者将要在下一篇文章中重点讲解Cycript的安装,基础用法和高级用法。之所以放在下一篇,是因为Cycript配合Monkey将会有事半功倍的效果。

链接:https://www.jianshu.com/p/da6cb32a1416

收起阅读 »

iOS开发你不知道的事-编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。

编译流程分析
现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

1、预处理(Prepressing)
2、编译(Compilation)
3、汇编(Assembly)
4、链接(Linking)


预编译
首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i

还可以下面的表达

$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

1、将所有的#define删除,并展开所有的宏定义
2、处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
3、处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
4、删除所有的注释//和/**/
5、添加行号和文件名标识,比如#2 “hello.c” 2。
6、保留所有的#pragma编译器指令
截图个大家看看效果


经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)
编译过程就是把预处理完的文件进行一系列的:词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o

链接(linking)
链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

链接相应的库


下面在贴出我们的写出的源代码是如何变成目标代码的流程图:


主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器
iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

1.LLVM核心库:
LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)。LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

3.LLDB项目:
LLDB项目以LLVM和Clang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST和表达式解析器,LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

4.libc和libc++:
libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

5.lld项目:
lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。


编译器前端的任务是进行:

1、语法分析
2、语义分析
3、生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


以上图解内容所做的是事情和gcc编译一模模一样样!

iOS程序-详细编译过程
1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
4.链接文件:将项目中的多个可执行文件合并成一个文件;
5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
6.编译 storyboard 文件:storyboard 文件也是会被编译的;
7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
10.生成 .app 包
11.将 Swift 标准库拷贝到包中
12.对包进行签名
13.完成打包

链接:https://www.jianshu.com/p/b60612c4d9ca

收起阅读 »

测试 View Controllers

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。让事情保持简单测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动...
继续阅读 »

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。

让事情保持简单

测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动开发(简称 TDD),有些人喜欢它,有些人则不喜欢。我们在这里不深入讨论,只是如果用 TDD,你得在写代码之前先写好测试。如果你好奇的话,可以去找 Wikipedia 上的文章看看。同时,我们也认为重构和测试可以很好地结合在一起。

测试 UI 部分通常很麻烦,因为它们包含太多活动部件。通常,view controller 需要和大量的 model 和 view 类交互。为了使 view controller 便于测试,我们要让任务尽量分离。

幸好,我们在更轻量的 view controller 这篇文章中的阐述的技术可以让测试更加简单。通常,如果你发现有些地方很难做测试,这就说明你的设计出了问题,你应该重构它。你可以重新参考更轻量的 view controller 这篇文章来获得一些帮助。总的目标就是有清晰的关注点分离。每个类只做一件事,并且做好。这样就可以让你只测试这件事。

记住:测试越多,回报的增长趋势越慢。首先你应该做简单的测试。当你觉得满意时,再加入更多复杂的测试。

Mocking

当你把一个整体拆分成小零件(比如更小的类)时,我们可以针对每个小的类来进行测试。但由于我们测试的类会和其他类交互,这里我们用一个所谓的 mock 或 stub 来绕开它。把 mock 对象看成是一个占位符,我们测试的类会跟这个占位符交互,而不是真正的那个对象。这样,我们就可以针对性地测试,并且保证不依赖于应用程序的其他部分。

在示例程序中,我们有个包含数组的 data source 需要测试。这个 data source 会在某个时候从 table view 中取出(dequeue)一个 cell。在测试过程中,还没有 table view,但是我们传递一个 mock 的 table view,这样即使没有 table view,也可以测试 data source,就像下面你即将看到的。起初可能有点难以理解,多看几次后,你就能体会到它的强大和简单。

Objective-C 中有个用来 mocking 的强大工具叫做 OCMock。它是一个非常成熟的项目,充分利用了 Objective-C 运行时强大的能力和灵活性。它使用了一些很酷的技巧,让通过 mock 对象来测试变得更加有趣。

本文后面有 data source 测试的例子,它更加详细地展示了这些技术如何工作在一起。

SenTestKit

编者注 这一节有一些过时了。在 Xcode 5 中 SenTestingKit 已经被 XCTest 完全取代,不过两者使用上没有太多区别,我们可以通过 Xcode 的 Edit -> Refactor -> Convert to XCTest 选项来切换到新的测试框架

我们将要使用的另一个工具是一个测试框架,开发者工具的一部分:Sente 的 SenTestingKit。这个上古神器从 1997 年起就伴随在 Objective-C 开发者左右,比第一款 iPhone 发布还早 10 年。现在,它已经集成到 Xcode 中了。SenTestingKit 会运行你的测试。通过 SenTestingKit,你将测试组织在类中。你需要给每一个你想测试的类创建一个测试类,类名以 Tests 结尾,它反应了这个类是干什么的。

这些测试类里的方法会做具体的测试工作。方法名必须以 test 开头来作为触发一个测试运行的条件。还有特殊的 -setUp 和 -tearDown 方法,你可以重载它们来设置各个测试。记住,你的测试类就是个类而已:只要对你有帮助,可以按需求在里面加 properties 和辅助方法。

做测试时,为测试类创建基类是个不错的模式。把通用的逻辑放到基类里面,可以让测试更简单和集中。可以通过示例程序中的例子来看看这样带来的好处。我们没有使用 Xcode 的测试模板,为了让事情简单有效,我们只创建了单独的 .m 文件。通过把类名改成以 Tests 结尾,类名可以反映出我们在对什么做测试。

编者注 Xcode 5 中 默认的测试模板也不再会自动创建 .h 文件了

与 Xcode 集成

测试会被 build 成一个 bundle,其中包含一个动态库和你选择的资源文件。如果你要测试某些资源文件,你得把它们加到测试的 target 中,Xcode 就会将它们打包到一个 bundle 中。接着你可以通过 NSBundle 来定位这些资源文件,示例项目实现了一个 -URLForResource:withExtension: 方法来方便的使用它。

Xcode 中的每个 scheme 定义了相应的测试 bundle 是哪个。通过 ⌘-R 运行程序,⌘-U 运行测试。

测试的运行依附于程序的运行,当程序运行时,测试 bundle 将被注入(injected)。测试时,你可能不想让你的程序做太多的事,那样会对测试造成干扰。可以把下面的代码加到 app delegate 中:

static BOOL isRunningTests(void) __attribute__((const));

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (isRunningTests()) {
return YES;
}

//
// Normal logic goes here
//

return YES;
}

static BOOL isRunningTests(void)
{
NSDictionary* environment = [[NSProcessInfo processInfo] environment];
NSString* injectBundle = environment[@"XCInjectBundle"];
return [[injectBundle pathExtension] isEqualToString:@"octest"];
}

编辑 Scheme 给了你极大的灵活性。你可以在测试之前或之后运行脚本,也可以有多个测试 bundle。这对大型项目来说很有用。最重要的是,可以打开或关闭个别测试,这对调试测试非常有用,只是要记得之后再把它们重新全部打开。

还要记住你可以为测试代码下断点,当测试执行时,调试器会在断点处停下来。

测试 Data Source
好了,让我们开始吧。我们已经通过拆分 view controller 让测试工作变得更轻松了。现在我们要测试 ArrayDataSource。首先我们新建一个空的,基本的测试类。我们把接口和实现都放到一个文件里;也没有哪个地方需要包含 @interface,放到一个文件会显得更加漂亮和整洁。

#import "PhotoDataTestCase.h"

@interface ArrayDataSourceTest : PhotoDataTestCase
@end

@implementation ArrayDataSourceTest
- (void)testNothing;
{
STAssertTrue(YES, @"");
}
@end

这个类没做什么事,只是展示了基本的设置。当我们运行这个测试时,-testNothing 方法将会运行。特别地,STAssert宏将会做琐碎的检查。注意,前缀 ST 源自于 SenTestingKit。这些宏和Xcode 集成,会把失败显示到侧边面板的Issues导航栏中。

第一个测试

我们现在把 testNothing 替换成一个简单、真正的测试:

- (void)testInitializing;
{
STAssertNil([[ArrayDataSource alloc] init], @"Should not be allowed.");
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){};
id obj1 = [[ArrayDataSource alloc] initWithItems:@[]
cellIdentifier:@"foo"
configureCellBlock:block];
STAssertNotNil(obj1, @"");
}

接着,我们想测试ArrayDataSource实现的方法:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath;

为此,我们创建一个测试方法:

- (void)testCellConfiguration;

首先,创建一个 data source:

__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
configuredCell = a;
configuredObject = b;
};
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:block];

注意,configureCellBlock 除了存储对象以外什么都没做,这可以让我们可以更简单地测试它。

然后,我们为 table view 创建一个 mock 对象:

id mockTableView = [OCMockObject mockForClass:[UITableView class]];

Data source 将在传进来的 table view 上调用 -dequeueReusableCellWithIdentifier:forIndexPath: 方法。我们将告诉 mock object 当它收到这个消息时要做什么。首先创建一个 cell,然后设置 mock。

UITableViewCell *cell = [[UITableViewCell alloc] init];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[[mockTableView expect] andReturn:cell]
dequeueReusableCellWithIdentifier:@"foo"
forIndexPath:indexPath];

第一次看到它可能会觉得有点迷惑。我们在这里所做的,是让 mock 记录特定的调用。Mock 不是一个真正的 table view;我们只是假装它是。-expect 方法允许我们设置一个 mock,让它知道当这个方法调用时要做什么。

另外,-expect 方法也告诉 mock 这个调用必须发生。当我们稍后在 mock 上调用 -verify 时,如果那个方法没有被调用过,测试就会失败。相应地,-stub 方法也用来设置 mock 对象,但它不关心方法是否被调用过。

现在,我们要触发代码运行。我们就调用我们希望测试的方法。

NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
id result = [dataSource tableView:mockTableView
cellForRowAtIndexPath:indexPath];

然后我们测试是否一切正常:

STAssertEquals(result, cell, @"Should return the dummy cell.");
STAssertEquals(configuredCell, cell, @"This should have been passed to the block.");
STAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
[mockTableView verify];

STAssert宏测试值的相等性。注意,前两个测试,我们通过比较指针来完成;我们不使用-isEqual:,是因为我们实际希望测试的是result,cell和configuredCell都是同一个对象。第三个测试要用 -isEqual:,最后我们调用 mock的 -verify方法。

注意,在示例程序中,我们是这样设置 mock 的:

id mockTableView = [self autoVerifiedMockForClass:[UITableView class]];

这是我们测试基类中的一个方便的封装,它会在测试最后自动调用 -verify 方法。

测试 UITableViewController
下面,我们转向PhotosViewController。它是个 UITableViewController 的子类,它使用了我们刚才测试过的 data source。View controller 剩下的代码已经相当简单了。

我们想测试点击 cell 后把我们带到详情页面,即一个 PhotoViewController的实例被 push 到 navigation controller 里面。我们再次使用 mocking 来让测试尽可能不依赖于其他部分。

首先我们创建一个 UINavigationController 的 mock:

id mockNavController = [OCMockObject mockForClass:[UINavigationController class]];

接下来,我们要使用部分 mocking。我们希望 PhotosViewController 实例的navigationController 返回 mockNavController。我们不能直接设置 navigation controller,所以我们简单地用 stub 来替换掉 PhotosViewController实例这个方法,让它返回mockNavController 就可以了。

PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
id photosViewControllerMock = [OCMockObject partialMockForObject:photosViewController];
[[[photosViewControllerMock stub] andReturn:mockNavController] navigationController];

现在,任何时候对 photosViewController 调用 -navigationController 方法,都会返回 mockNavController。这是个强大的技巧,OCMock 就有这样的本领。

接下来,我们要告诉 navigation controller mock 我们调用的期望,即,一个 photo 不为 nil 的 detail view controller。

UIViewController* viewController = [OCMArg checkWithBlock:^BOOL(id obj) {
PhotoViewController *vc = obj;
return ([vc isKindOfClass:[PhotoViewController class]] &&
(vc.photo != nil));
}];
[[mockNavController expect] pushViewController:viewController animated:YES];

现在,我们触发 view 加载,并且模拟一行被点击:

UIView *view = photosViewController.view;
STAssertNotNil(view, @"");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[photosViewController tableView:photosViewController.tableView
didSelectRowAtIndexPath:indexPath];

最后我们验证 mocks 上期望的方法被调用过:

[mockNavController verify];
[photosViewControllerMock verify];

现在我们有了一个测试,用来测试和 navigation controller 的交互,以及正确 view controller 的创建。

又一次地,我们在示例程序中使用了便捷的方法:

- (id)autoVerifiedMockForClass:(Class)aClass;
- (id)autoVerifiedPartialMockForObject:(id)object;

于是,我们不需要记住调用-verify。

进一步探索

就像你从上面看到的那样,部分 mocking 非常强大。如果你看看 -[PhotosViewController setupTableView]方法的源码,你就会看到它是如何从 app delegate 中取出 model 对象的。

NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;

上面的测试依赖于这行代码。打破这种依赖的一种方式是再次使用 部分 mocking,让 app delegate 返回预定义的数据,就像这样:

id storeMock; // 假设我们已经设置过了
id appDelegate = [AppDelegate sharedDelegate]
id appDelegateMock = [OCMockObject partialMockForObject:appDelegate];
[[[appDelegateMock stub] andReturn:storeMock] store];

现在,无论何时调用[AppDelegate sharedDelegate].store,它将返回 storeMock。将这个技术使用好的话,可以确保让你的测试恰到好处地在保持简单和应对复杂之间找到平衡。

需要记住的事

部分 mock 技术将会在 mocks 的存在期间替换并保持被 mocking 的对象,并且一直有效。你可以通过提前调用[aMock stopMocking]来终于这种行为。大多数时候,你希望 部分 mock 在整个测试期间都保持有效。如果要提前终止,请确保在测试方法最后放置[aMock verify]。否则 ARC 会过早释放这个 mock,这样你就不能 -verify了,这不太可能是你想要的结果。

测试 NIB 加载

PhotoCell设置在一个 NIB 中,我们可以写一个简单的测试来检查 outlets 设置得是否正确。我们来回顾一下PhotoCell类:

@interface PhotoCell : UITableViewCell

+ (UINib *)nib;

@property (weak, nonatomic) IBOutlet UILabel* photoTitleLabel;
@property (weak, nonatomic) IBOutlet UILabel* photoDateLabel;

@end

我们的简单测试的实现看上去是这样:

@implementation PhotoCellTests

- (void)testNibLoading;
{
UINib *nib = [PhotoCell nib];
STAssertNotNil(nib, @"");

NSArray *a = [nib instantiateWithOwner:nil options:@{}];
STAssertEquals([a count], (NSUInteger) 1, @"");
PhotoCell *cell = a[0];
STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

// 检查 outlet 是否正确设置
STAssertNotNil(cell.photoTitleLabel, @"");
STAssertNotNil(cell.photoDateLabel, @"");
}

@end

非常基础,但是能出色完成工作。

值得一提的是,当有发生改变时,我们需要同时更新测试以及相应的类或 nib 。这是事实。你需要考虑改变类或者 nib 文件时可能会打破原有的 outlets 连接。如果你用了 .xib 文件,你可能要注意了,这是经常发生的事。

关于 Class 和 Injection
我们已经从与 Xcode 集成得知,测试 bundle 会注入到应用程序中。省略注入的如何工作的细节(它本身是个巨大的话题),简单地说:注入是把待注入的 bundle(我们的测试 bundle)中的 Objective-C 类添加到运行的应用程序中。这很好,因为这样允许我们运行测试了。

还有一件事会很让人迷惑,那就是如果我们同时把一个类添加到应用程序和测试 bundle中。如果在上面的示例程序中,我们(不小心)把 PhotoCell 类同时添加到测试 bundle 和应用程序里的话,在测试 bundle 中调用 [PhotoCell class]会返回一个不同的指针(你应用程序中的那个类)。于是我们的测试将会失败:

STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

再一次声明:注入很复杂。你应该确认的是:不要把应用程序中的 .m 文件添加到测试 target 中。否则你会得到预想不到的行为。

额外的思考

如果你使用一个持续集成 (CI) 的解决方案,让你的测试启动和运行是一个好主意。详细的描述超过了本文的范围。这些脚本通过 RunUnitTests 脚本触发。还有个 TEST_AFTER_BUILD 环境变量。

另一种有趣的选择是创建单独的测试 bundle 来自动化性能测试。你可以在测试方法里做任何你想做的。定时调用一些方法并使用 STAssert 来检查它们是否在特定阈值里面是其中一种选择。

链接:https://www.jianshu.com/p/733fa8bbca95

收起阅读 »

iOS标准库中常用数据结构和算法之cache

📝缓存Cache缓存是以键值对的形式进行数据的存储和检索,内部采用哈希表实现。当系统出现内存压力时则会释放掉部分缓存的键值对。 iOS系统提供了一套基于OC语言的高级缓存库NSCache,同时也提供一套基于C语言实现的缓存库libcache.dylib,其中N...
继续阅读 »

📝缓存Cache

缓存是以键值对的形式进行数据的存储和检索,内部采用哈希表实现。当系统出现内存压力时则会释放掉部分缓存的键值对。 iOS系统提供了一套基于OC语言的高级缓存库NSCache,同时也提供一套基于C语言实现的缓存库libcache.dylib,其中NSCache是基于libcache.dylib实现的高级类库,并且这两个库都是线程安全的。 本文主要介绍基于C语言的缓存库的各种API函数。

头文件: #include <cache.h>, #include <cache_callbacks.h>
平台: iOS系统

一、缓存对象的创建和关闭

功能:创建或者销毁一个缓存对象。

函数签名:

int cache_create(const char *name, cache_attributes_t *attrs, cache_t **cache_out);
int cache_destroy(cache_t *cache);

参数:

name:[in] 创建缓存时用来指定缓存的字符串名称,不能为空。
attrs: [in] 设置缓存的属性。不能为空。
cache_out: [out] 返回创建的缓存对象。
return: [out] 成功操作返回0,否则返回非0

描述:

缓存对象是一个容器对象,其缓存的内容是一个个键值对,至于这些键值对是什么类型的数据,如何控制键值对的数据的生命周期,如何判断两个键是否是相同的键等等这些信息缓存对象本身是无法得知,因此需要我们明确的告诉缓存对象如何去操作这些键值信息。这也就是为什么在创建缓存对象时需要指定属性这个参数了。属性的参数类型是一个cache_attributes_t结构体。这个结构体的大部分数据成员都是函数指针,这些函数指针就是用来实现对键值进行操作的各种策略。

struct cache_attributes_s {
uint32_t version; //缓存对象的版本信息
cache_key_hash_cb_t key_hash_cb; //对键执行hash计算的函数,不能为空
cache_key_is_equal_cb_t key_is_equal_cb; //判断两个键是否相等的函数,不能为空

cache_key_retain_cb_t key_retain_cb; //键加入缓存时调用,用于增加键的引用计数或者进行内存拷贝。
cache_release_cb_t key_release_cb; //键的释放处理函数,用于对键的内存管理使用。
cache_release_cb_t value_release_cb; //值的释放处理函数,用于对值的内存管理使用。

cache_value_make_nonpurgeable_cb_t value_make_nonpurgeable_cb; //当值的引用计数从0变为1时对值内存进行非purgeable的处理函数。
cache_value_make_purgeable_cb_t value_make_purgeable_cb; //当值的引用计数变为0时对值进行purgeable的处理函数。这个函数的作用是为了解决当内存吃紧时自动释放所分配的内存。

void *user_data; //附加数据,这个附加数据会在所有的这些回调函数中出现。

// Added in CACHE_ATTRIBUTES_VERSION_2
cache_value_retain_cb_t value_retain_cb; //值增加引用计数的函数,用于对值的内存管理使用。
};
typedef struct cache_attributes_s cache_attributes_t;

上述的各种回调函数的格式都在cache.h中有明确的定义,因此这里就不再展开介绍了。一般情况下我们通常都会将字符串或者整数来作为键使用,因此当你采用字符串或者整数作为键时,系统预置了一系列缓存对象属性的默认实现函数。这些函数的声明在cache_callbacks.h文件中

/*
* Pre-defined callback functions.
*/

//用于键进行哈希计算的预置函数
CACHE_PUBLIC_API uintptr_t cache_key_hash_cb_cstring(void *key, void *unused);
CACHE_PUBLIC_API uintptr_t cache_key_hash_cb_integer(void *key, void *unused);
//用于键进行相等比较的预置函数
CACHE_PUBLIC_API bool cache_key_is_equal_cb_cstring(void *key1, void *key2, void *unused);
CACHE_PUBLIC_API bool cache_key_is_equal_cb_integer(void *key1, void *key2, void *unused);

//键值进行释放的函数,这函数默认实现就是调用free函数,因此如果采用这个函数进行释放处理则键值需要从堆中进行内存分配。
CACHE_PUBLIC_API void cache_release_cb_free(void *key_or_value, void *unused);

//对值进行purgeable处理的预置函数。
CACHE_PUBLIC_API void cache_value_make_purgeable_cb(void *value, void *unused);
CACHE_PUBLIC_API bool cache_value_make_nonpurgeable_cb(void *value, void *unused);

示例代码:

//下面代码用于创建一个以字符串为键的缓存对象,其中的缓存对象的属性中的各个成员函数采用的是系统默认预定的函数。
#include <cache.h>
#include <cache_callbcaks.h>

cache_t *im_cache;
cache_attributes_t attrs = {
.version = CACHE_ATTRIBUTES_VERSION_2,
.key_hash_cb = cache_key_hash_cb_cstring,
.key_is_equal_cb = cache_key_is_equal_cb_cstring,
.key_retain_cb = my_copy_string,
.key_release_cb = cache_release_cb_free,
.value_release_cb = cache_release_cb_free,
};
cache_create("com.acme.im_cache", &attrs, &im_cache);

二、缓存对象中键值对的设置和获取以及删除

功能:用于处理键值对在缓存中的添加、获取和删除操作。
函数签名:

//将键值对添加到缓存,或者替换掉原有的键值对。
int cache_set_and_retain(cache_t *cache, void *key, void *value, size_t cost);

//从缓存中根据键获取值
int cache_get_and_retain(cache_t *cache, void *key, void **value_out);

//将缓存中的值引用计数减1,当引用计数为0时则清理值分配的内存或者销毁值分配的内存。
int cache_release_value(cache_t *cache, void *value);

//从缓存中删除键值。
int cache_remove(cache_t *cache, void *key);

参数:
cache:[in] 缓存对象。
key:[in] 添加或者获取或者删除时的键。
cost:[in] 添加缓存时的成本代价,值越大键值在缓存中保留的时间就越长久。
value:[in] 添加时的值。
value_out: [out] 用于值获取时的输出。

描述:
1、cache_set_and_retain 函数用于将键值对放入缓存中,并指定cost值。当将一个键添加到缓存时,系统内部分别会调用缓存属性cache_attributes_t结构体中的key_retain_cb来实现对键的内存的管理,如果这个函数设置为NULL的话那就表明我们需要自己负责键的生命周期的管理。因为缓存对象内部是通过哈希表来进行数据的存储和检索的,所以在将键值对加入缓存时,还需要提供对键进哈希计算和比较的属性函数key_hash_cb,key_is_equal_cb。 而对于值来说,当值加入缓存时系统会将值的引用计数设置为1,如果我们想自行处理值在缓存中的内存保存则需要指定缓存属性中的value_retain_cb来实现。加入缓存中的值是可以为NULL的。最后的cost参数用于指定这个键值对的成本值,值越小在缓存中保留的时间就越少,反之亦然。

2、cache_get_and_retain函数用来根据键获取对应的值,如果缓存中没有保存对应的键值对,或者键值对被丢弃,或者值所分配的内存被清除则value_out返回NULL,并且函数返回特殊的值ENOENT。每调用一次值的获取,缓存对象都会增加值的引用计数。因此当我们不再需要访问返回的值时则需要调用手动调用cache_release_value函数来减少缓存对象中值的引用计数。而当值的引用计数变为0时则值分配的内存会设置为可清理(purgeable)或者被销毁掉。

3、cache_remove函数用于删除缓存中的键值对。当删除缓存中的键值对时,缓存对象会调用属性结构体中的key_release_cb函数进行键的内存销毁,以及如果值的引用计数变为0时则会调用value_release_cb函数进行值的内存销毁。

示例代码:

#include <cache.h>
#include <cache_callbacks.h>

void main()
{
cache_attributes_t attr;
attr.key_hash_cb = cache_key_hash_cb_cstring;
attr.key_is_equal_cb = cache_key_is_equal_cb_cstring;
attr.key_retain_cb = NULL;
attr.key_release_cb = cache_release_cb_free;
attr.version = CACHE_ATTRIBUTES_VERSION_2;
attr.user_data = NULL;
attr.value_retain_cb = NULL;
attr.value_release_cb = cache_release_cb_free;
attr.value_make_purgeable_cb = cache_value_make_purgeable_cb;
attr.value_make_nonpurgeable_cb = cache_value_make_nonpurgeable_cb;

//创建缓存
cache_t *cache = NULL;
int ret = cache_create("com.test", &attr, &cache);

//将键值对放入缓存
char *key = malloc(4);
strcpy(key, "key");
char *val = malloc(4);
strcpy(val, "val");
ret = cache_set_and_retain(cache, key, val, 0);
ret = cache_release_value(cache, val);
//获取键值对,使用后要释放。
char *val2 = NULL;
ret = cache_get_and_retain(cache, key, (void**)&val2);
ret = cache_release_value(cache, val2);

//删除键值
cache_remove(cache, key);

//销毁缓存。
cache_destroy(cache);
}

三、缓存中键值对的清理策略和值的访问策略

缓存的作用是会对保存在里面的键值对进行丢弃,这取决于当前内存的使用情况以及其他一些场景。在调用cache_set_and_retain将键值对添加到缓存时还会指定一个cost值,值越大被丢弃的可能性就越低。在上面的介绍中有说明缓存会对键值对中的值进行引用计数管理。当调用cache_set_and_retain时值引用计数将设置为1,调用cache_get_and_retain函数获取值时如果键值对在缓存中则会增加值的引用计数。而不需要访问和操作值时我们需要调用cache_release_value函数来将引用计数减1。当值的引用计数变为0时就会立即或者以后发生如下的事情:

1、如果我们在缓存的属性结构体中设置了value_make_purgeable_cb函数则会调用这个函数表明此时值是可以被清理的。被清理的意思是说为值分配的物理内存随时有可能会被回收。因此当值被设置为可被清理状态时就不能继续去直接访问值所分配的内存了。
2、如果在此之前键值对因为函数cache_remove的调用而被从缓存中删除,则会调用属性结构体中的value_release_cb函数来执行值内存的销毁处理。
3、如果因为系统内存的压力导致需要丢弃缓存中的键值对时,就会把值引用计数为0的键值对丢弃掉!注意:只有值引用计数为0时才会缓存被丢弃。

每次对缓存中的值的访问都需要通过cache_get_and_retain函数来执行,当调用cache_get_and_retain函数时会发生如下事情:

1、判断当前键值对是否在缓存中,如果不再则值返回NULL。
2、如果键值对在缓存中。并且值的引用计数为0,就会判断缓存的结构体属性中是否存在value_make_nonpurgeable_cb函数,如果存在则会调用value_make_nonpurgeable_cb函数将值的内存设置为不可清理,如果设置为不可清理返回为false则表明此时值的内存已经被清理了,这时候键值对将会从缓存中丢弃,并且cache_get_and_retain函数将返回值为NULL。当然如果value_make_nonpurgeable_cb函数为空则不会发生这一步。
3、增加值的引用计数,并返回值。

链接:https://www.jianshu.com/p/2f58f165bf1a

收起阅读 »

iOS标准库中常用数据结构和算法之内存池

⛲️内存池内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当...
继续阅读 »

⛲️内存池

内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当不需要用时你可以将这块内存放入内存池中,供其他地方在申请时进行复用,而当你再次需要时则只需要重新申请即可。内存池提供了内存分配编号而且设置脏标志的概念,当你把分配的内存放入内存池并设置脏标志后,系统就会在适当的时候将这块内存的内容写回到磁盘,这样当你再次根据内存编号来访问内存时,系统就又会从磁盘中将内容读取到内存中去。

功能:在iOS中提供了一套内存池管理的API,你可以用这套API来实现上述的功能,而且系统内部很多功能也是借助内存池来实现内存复用和磁盘存储的。
头文件: #include <mpool.h>, #include <db.h>
平台: BSD系统,linux系统

一、内存池的创建、同步和关闭

功能:创建和关闭一个内存池对象并和磁盘文件绑定以便进行同步操作。
函数签名:

//创建一个内存池对象
MPOOL * mpool_open(void *key, int fd, pgno_t pagesize, pgno_t maxcache);

//将内存池中的脏数据同步写回到磁盘文件中
int mpool_sync(MPOOL *mp);

//关闭和销毁内存池对象。
int mpool_close(MPOOL *mp);

参数:

key:[in] 保留字段,暂时没有用处,传递NULL即可。
fd:[in] 内存池关联的磁盘文件句柄,文件句柄需要用open函数来打开。
pagesize:[in] 内存池中每次申请和分配的内存的尺寸大小,单位是字节。
maxcache:[in] 内存池中内存页的最大缓存数量。如果池中缓存的内存数量超过了最大缓存的数量就会复用已经存在的内存,而不是每次都分配新的内存。
return:[out] 返回一个新创建的内存池对象,其他两个函数成功返回0,失败返回非0.

描述:

1、内存池中的内存的分配和获取是以页为单位进行的,每次分配的页的尺寸大小由pagesize指定,同时内存池也指定了最大的缓存页数量maxcache。每次从内存池中分配一页内存时,除了会返回分配的内存地址外,还会返回这一页内存的编号。这个编号对于内存池中的内存页来说是唯一的。因为内存池中的内存是可以被复用的,因此就有可能是不同的编号的内存页所得到的内存地址是相同的。

2、每一个内存池对象都会要和一个文件关联起来,以便能够实现内存数据的永久存储和内存的复用。文件句柄必须用open函数来打开,比如下面的例子:

int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

3、当我们不需要使用某个内存页时或者内存页的内容有改动则我们需要将这个内存页放入回内存池中,并将页标志为脏(DIRTY)。这样系统就会在适当的时候将此内存页的数据写回到磁盘文件中,同时此内存页也会在后续被重复利用。

4、当我们想将所有设置为脏标志的内存页立即写入磁盘时则需要调用mpool_sync函数进行同步处理。

5、当我们不再需要内存池时,则可以通过mpool_close来关闭内存池对象,需要注意的是关闭内存池并不会将内存中的数据回写到磁盘中去。

二、内存池中内存的获取

功能: 从内存池中申请分配一页新的内存或者获取现有缓存中的内存。
函数签名:

//从内存池中申请分配一页新的内存
void * mpool_new(MPOOL *mp, pgno_t *pgnoaddr);
//根据内存编号页获取对应的内存。
void * mpool_get(MPOOL *mp, pgno_t pgno, u_int flags);

参数:

mp:[in] 内存池对象。
pgnoaddr:[out] 用于mpool_new函数,用于保存新分配的内存页编号。
pngno:[in] 用于mpool_get函数,指定要获取的内存页的编号。
flags:[in] 此参数暂时无用。
return:[out] 返回分配或者获取的内存地址。如果分配或者获取失败则返回NULL。

描述:

1、无论是new还是get每次从内存池里面分配或者获取的内存页的大小都是由上述mpool_open函数中的pagesize参数指定的大小。
2、系统内部分配的内存是用calloc函数实现的,但是我们不需要手动调用free来对内存进行释放处理。
3、每个内存页都有一个唯一的页编号,而且每次分配的页编号也会一直递增下去。
4、mpool_new函数申请分配新的内存时,如果当前缓存中的内存页小于maxcache数量则总是分配新的内存,只有当缓存数量大于maxcache时才会从现有的缓存中寻找一页可以被重复利用的内存页,如果没有可以重复利用的页面,则会继续分配新的内存页。
5、mpool_get函数则根据内存页的编号获取对应的内存页。如果编号不存在则返回NULL。需要注意的是一般在获取了某一页内存后,不要进行重复获取操作,否则在DEBUG状态下会返回异常。另外一个情况是有可能相同的页编号下两次获取的内存地址是不一样的,因为系统实现内部有内存复用的机制。

三、内存池中内存的放回

功能:将分配或者申请的内存页放回到内存池中去,以便进行重复利用。
函数签名:

int  mpool_put(MPOOL *mp, void *pgaddr, u_int flags);

参数:

mp: [in] 内存池对象。
pgaddr:[in] 要放入缓存的内存页地址。这个地址由mpool_get/new两个函数返回。
flags:[in] 放回的属性,一般设置为0或者MPOOL_DIRTY。
return:[in] 函数调用成功返回0,失败返回非0

描述:

1、这个函数用来将内存页放入回内存池缓存中,以便对内存进行重复利用。当将某个内存地址放入回缓存后,将不能再次访问这个内存地址了。如果要想继续访问内存中的数据则需要借助上述的mpool_get/new函数来重新获取。
2、flags:属性如果指定为0时,表明放弃这次内存中的内容的修改,系统不会将内存中的内容写入到磁盘中,而只是将内存放入缓存中供其他地方重复使用。而如果设置为MPOOL_DIRTY时,则表明将这页内存中的数据设置为脏标志,除了同样将内存放入缓存中重复利用外,则会在适当的时候将内存中的数据写入到磁盘中,以便下次进行读取。

四、内存池磁盘读写通知

功能:注册回调函数,当某页内存要写回到磁盘或者要从磁盘中读取时就会调用指定的回调函数。
函数签名:

void mpool_filter(MPOOL *mp, void (*pgin)(void *, pgno_t, void *),
void (*pgout)(void *, pgno_t, void *), void *pgcookie);

参数:

mp:[in] 内存池对象.
pgin: [in]: 回调函数,当某个内存页的数据需要从磁盘读取时,会在读取完成后调用这个回调函数。
pgout:[in]: 回调函数,当某个内存页的数据要到磁盘时,会在写入完成后调用这个回调函数。
pgcookie: [in] 上述两个回调函数的附加参数。

描述:

因为内存池中的内存页会进行复用,以及会在适当的时候将内容同步到磁盘中,或者从磁盘中将内容读取到内存中,因此可以借助这个函数来监控这些磁盘文件和内存之间的读写操作。pgin和pgout函数的格式定义如下:

//pgin和pgout回调函数的格式。
//pgcookie:是mpool_filter函数中传入的参数。
//pgno: 要进行读写的内存页编号
//pageaddr: 要进行读写的内存地址。
void (*pgcallback)(void *pgcookie, pgno_t pgno, void *pageaddr);

五、实例代码

#include <mpool.h>
#include <db.h>

//创建并打开一个文件。
int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

//创建一个内存池对象,每页的内存100个字节,最大的缓存数量为4
MPOOL *pool = mpool_open(NULL, fd, 100, 4);


//从内存池中分配一个新的内存页,这里对返回的内存填写数据。
pgno_t pidx1, pidx2 = 0;
char *mem1 = (char*)mpool_new(pool, &pidx1);
memcpy(mem1, "aaa", 4);

char *mem2 = (char*)mpool_new(pool, &pidx2);
memcpy(mem2, "bbb", 4);

//将分配的内存mem1放回内存池中,但是内容不保存到磁盘
mpool_put(pool, mem1, 0);
//将分配的内存mem2放回内存池中,但是内容保存到磁盘。
mpool_put(pool, mem2, MPOOL_DIRTY);

//经过上面的操作后mem1,mem2将不能继续再访问了,需要访问时需要再次调用mpool_get。
mem1 = (char*)mpool_get(pool, pidx1, 0);
mem2 = (char*)mpool_get(pool, pidx2, 0);

//上面的mem1和mem2可能和前面的new返回的地址是不一样的。因此在内存池中不能通过地址来做唯一比较,而应该将编号来进行比较。

//将所有设置为脏标志的内存也写回到磁盘中去。
mpool_sync(pool);

mpool_close(pool); //关闭内存池。

close(fd); //关闭文件。

内存池为iOS系统底层开发提供了一个非常重要的能力,我们可以好好利用内存池来对内存进行管理,以及一些需要进行持久化的数据也可以借助内存池来进行保存,通过内存池提高内存的重复利用率。

转自:https://www.jianshu.com/p/34bd3e5c5b4e

收起阅读 »

iOS WKWebView实现JS与Objective-C交互(一) 附Demo

前言: 根据需求有时候需要用到JS与Objective-C交互来实现一些功能, 本文介绍实现交互的一种方式, 使用WKWebView的新特性MessageHandler, 来实现JS调用原生, 原生调用JS.一. 基础说明WKWebView 初始化时,有一个参...
继续阅读 »

前言: 根据需求有时候需要用到JS与Objective-C交互来实现一些功能, 本文介绍实现交互的一种方式, 使用WKWebView的新特性MessageHandler, 来实现JS调用原生, 原生调用JS.

一. 基础说明

WKWebView 初始化时,有一个参数叫configuration,它是WKWebViewConfiguration类型的参数,而WKWebViewConfiguration有一个属性叫userContentController,它又是WKUserContentController类型的参数。WKUserContentController对象有一个方法- addScriptMessageHandler:name:,我把这个功能简称为MessageHandler。- addScriptMessageHandler:name:有两个参数,第一个参数是userContentController的代理对象,第二个参数是JS里发送postMessage的对象。
所以要使用MessageHandler功能,就必须要实现WKScriptMessageHandler协议。

二. 在JS中使用方法


1. js文件代码实例

function locationClick() {
/// "showMessage". 为我们和前端开发人员的约定
window.webkit.messageHandlers.showMessage.postMessage(null);
}

2. 在ViewController 我们需要做哪些事情

2.1 对WKWebView进行初始化以及设置

/// 创建网页配置对象
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
/// 创建设置对象
WKPreferences *preference = [[WKPreferences alloc]init];
/// 最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
preference.minimumFontSize = 40.0;
/// 设置是否支持javaScript 默认是支持的
preference.javaScriptEnabled = YES;
/// 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
preference.javaScriptCanOpenWindowsAutomatically = YES;
config.preferences = preference;
/// 这个类主要用来做native与JavaScript的交互管理

_wkwebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) configuration:config];
[self.view addSubview:_wkwebView];
/// Load WebView
#if 0
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://m.benlai.com/huanan/zt/1231cherry"]];
[self.wkwebView loadRequest:request];
#endif

#if 1
NSString *bundleStr = [[NSBundle mainBundle] pathForResource:@"summerxx-test" ofType:@"html"];
[self.wkwebView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:bundleStr]]];
#endif

// UI代理
_wkwebView.UIDelegate = self;
// 导航代理
_wkwebView.navigationDelegate = self;
// 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
_wkwebView.allowsBackForwardNavigationGestures = YES;

// 添加监测网页加载进度的观察者
[self.wkwebView addObserver:self
forKeyPath:@"estimatedProgress"
options:0
context:nil];
// 添加监测网页标题title的观察者
[self.wkwebView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];

2.2 在合理地方进行注册

[self.wkwebView.configuration.userContentController addScriptMessageHandler:self name:@"showMessage"];

2.3 接收JS给我们传递消息, 这里我做了一个简单的弹窗提示

#pragma mark - WKScriptMessageHandler
/// 通过接收JS传出消息的name进行捕捉的回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"showMessage"]) {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"title" message:@"messgae" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"同意" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"虽然我同意了你, 但是答应我别骄傲."];
[self.wkwebView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@",result, error);
}];
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"cancel");

}];
UIAlertAction *errorAction = [UIAlertAction actionWithTitle:@"拒绝" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"虽然我拒绝了你, 但是继续爱我好吗"];
[self.wkwebView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@",result, error);
}];
}];
[alertController addAction:errorAction];
[alertController addAction:okAction];
[alertController addAction:cancelAction];

// 出现
[self presentViewController:alertController animated:YES completion:^{

}];

}
}

2.4 销毁

- (void)dealloc {
/// Remove removeObserver
[_wkwebView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
[_wkwebView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(title))];
WKUserContentController *userCC = self.wkwebView.configuration.userContentController;
[userCC removeScriptMessageHandlerForName:@"showMessage"];
}

Demo: 演示步骤, 点击获取定位 Objective-C获取到JS消息
点击拒绝, JS获取到Objective-C传递的消息

如图:



总结: 脑壳疼
备注: 如果遇到跨域问题, 主要还是前端和服务端改一下就好了.

参照 : https://www.jianshu.com/p/433e59c5a9eb

demo: https://github.com/summerxx27/JS_ObjectiveC_MessageHandler

完~
文/夏天然后

转自:https://www.jianshu.com/p/d798786e99eb

收起阅读 »

整洁的 Table View 代码

Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相...
继续阅读 »

Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相关代码的整洁和良好组织的技术。

UITableViewController vs. UIViewController

Apple 提供了 UITableViewController 作为 table views 专属的 view controller 类。Table view controllers 实现了一些非常有用的特性,来帮你避免一遍又一遍地写那些死板的代码!但是话又说回来,table view controller 只限于管理一个全屏展示的 table view。大多数情况下,这就是你想要的,但如果不是,还有其他方法来解决这个问题,就像下面我们展示的那样。

Table View Controllers 的特性

Table view controllers 会在第一次显示 table view 的时候帮你加载其数据。另外,它还会帮你切换 table view 的编辑模式、响应键盘通知、以及一些小任务,比如闪现侧边的滑动提示条和清除选中时的背景色。为了让这些特性生效,当你在子类中覆写类似 viewWillAppear: 或者 viewDidAppear: 等事件方法时,需要调用 super 版本。

Table view controllers 相对于标准 view controllers 的一个特别的好处是它支持 Apple 实现的“下拉刷新”。目前,文档中唯一的使用 UIRefreshControl 的方式就是通过 table view controller ,虽然通过努力在其他地方也能让它工作(见此处),但很可能在下一次 iOS 更新的时候就不行了。

这些要素加一起,为我们提供了大部分 Apple 所定义的标准 table view 交互行为,如果你的应用恰好符合这些标准,那么直接使用 table view controllers 来避免写那些死板的代码是个很好的方法。

Table View Controllers 的限制

Table view controllers 的 view 属性永远都是一个 table view。如果你稍后决定在 table view 旁边显示一些东西(比如一个地图),如果不依赖于那些奇怪的 hacks,估计就没什么办法了。

如果你是用代码或 .xib 文件来定义的界面,那么迁移到一个标准 view controller 将会非常简单。但是如果你使用了 storyboards,那么这个过程要多包含几个步骤。除非重新创建,否则你并不能在 storyboards 中将 table view controller 改成一个标准的 view controller。这意味着你必须将所有内容拷贝到新的 view controller,然后再重新连接一遍。

最后,你需要把迁移后丢失的 table view controller 的特性给补回来。大多数都是 viewWillAppear: 或 viewDidAppear: 中简单的一条语句。切换编辑模式需要实现一个 action 方法,用来切换 table view 的 editing 属性。大多数工作来自重新创建对键盘的支持。

在选择这条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:

使用 Child View Controllers

和完全抛弃 table view controller 不同,你还可以将它作为 child view controller 添加到其他 view controller 中(关于此话题的文章)。这样,parent view controller 在管理其他的你需要的新加的界面元素的同时,table view controller 还可以继续管理它的 table view。

- (void)addPhotoDetailsTableView
{
DetailsViewController *details = [[DetailsViewController alloc] init];
details.photo = self.photo;
details.delegate = self;
[self addChildViewController:details];
CGRect frame = self.view.bounds;
frame.origin.y = 110;
details.view.frame = frame;
[self.view addSubview:details.view];
[details didMoveToParentViewController:self];
}

如果你使用这个解决方案,你就必须在 child view controller 和 parent view controller 之间建立消息传递的渠道。比如,如果用户选择了一个 table view 中的 cell,parent view controller 需要知道这个事件来推入其他 view controller。根据使用习惯,通常最清晰的方式是为这个 table view controller 定义一个 delegate protocol,然后到 parent view controller 中去实现。

@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end

@interface PhotoViewController ()
@end

@implementation PhotoViewController
// ...
- (void)didSelectPhotoAttributeWithKey:(NSString *)key
{
DetailViewController *controller = [[DetailViewController alloc] init];
controller.key = key;
[self.navigationController pushViewController:controller animated:YES];
}
@end

就像你看到的那样,这种结构为 view controller 之间的消息传递带来了额外的开销,但是作为回报,代码封装和分离非常清晰,有更好的复用性。根据实际情况的不同,这既可能让事情变得更简单,也可能会更复杂,需要读者自行斟酌和决定。

分离关注点(Separating Concerns)

当处理 table views 的时候,有许多各种各样的任务,这些任务穿梭于 models,controllers 和 views 之间。为了避免让 view controllers 做所有的事,我们将尽可能地把这些任务划分到合适的地方,这样有利于阅读、维护和测试。

这里描述的技术是文章更轻量的 View Controllers 中的概念的延伸,请参考这篇文章来理解如何重构 data source 和 model 的逻辑。结合 table views,我们来具体看看如何在 view controllers 和 views 之间分离关注点。

搭建 Model 对象和 Cells 之间的桥梁

有时我们需要将想显示的 model 层中的数据传到 view 层中去显示。由于我们同时也希望让 model 和 view 之间明确分离,所以通常把这个任务转移到 table view 的 data source 中去处理:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PhotoCell"];
Photo *photo = [self itemAtIndexPath:indexPath];
cell.photoTitleLabel.text = photo.name;
NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
cell.photoDateLabel.text = date;
}

但是这样的代码会让 data source 变得混乱,因为它向 data source 暴露了 cell 的设计。最好分解出来,放到 cell 类的一个 category 中。

@implementation PhotoCell (ConfigureForPhoto)

- (void)configureForPhoto:(Photo *)photo
{
self.photoTitleLabel.text = photo.name;
NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
self.photoDateLabel.text = date;
}

@end

有了上述代码后,我们的 data source 方法就变得简单了。

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
[cell configureForPhoto:[self itemAtIndexPath:indexPath]];
return cell;
}

在我们的示例代码中,table view 的 data source 已经分解到单独的类中了,它用一个设置 cell 的 block 来初始化。这时,这个 block 就变得这样简单了:

TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {
[cell configureForPhoto:photo];
};

让 Cells 可复用

有时多种 model 对象需要用同一类型的 cell 来表示,这种情况下,我们可以进一步让 cell 可以复用。首先,我们给 cell 定义一个 protocol,需要用这个 cell 显示的对象必须遵循这个 protocol。然后简单修改 category 中的设置方法,让它可以接受遵循这个 protocol 的任何对象。这些简单的步骤让 cell 和任何特殊的 model 对象之间得以解耦,让它可适应不同的数据类型。

在 Cell 内部控制 Cell 的状态

如果你想自定义 table views 默认的高亮或选择行为,你可以实现两个 delegate 方法,把点击的 cell 修改成我们想要的样子。例如:

- (void)tableView:(UITableView *)tableView
didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}

- (void)tableView:(UITableView *)tableView
didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = nil;
}

然而,这两个 delegate 方法的实现又基于了 view controller 知晓 cell 实现的具体细节。如果我们想替换或重新设计 cell,我们必须改写 delegate 代码。View 的实现细节和 delegate 的实现交织在一起了。我们应该把这些细节移到 cell 自身中去。

@implementation PhotoCell
// ...
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
[super setHighlighted:highlighted animated:animated];
if (highlighted) {
self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
} else {
self.photoTitleLabel.shadowColor = nil;
}
}
@end

总的来说,我们在努力把 view 层和 controller 层的实现细节分离开。delegate 肯定得清楚一个 view 该显示什么状态,但是它不应该了解如何修改 view 结构或者给某些 subviews 设置某些属性以获得正确的状态。所有这些逻辑都应该封装到 view 内部,然后给外部提供一个简单的 API。

控制多个 Cell 类型

如果一个 table view 里面有多种类型的 cell,data source 方法很快就难以控制了。在我们示例程序中,photo details table 有两种不同类型的 cell:一种用于显示几个星,另一种用来显示一个键值对。为了划分处理不同 cell 类型的代码,data source 方法简单地通过判断 cell 的类型,把任务派发给其他指定的方法。

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *key = self.keys[(NSUInteger) indexPath.row];
id value = [self.photo valueForKey:key];
UITableViewCell *cell;
if ([key isEqual:PhotoRatingKey]) {
cell = [self cellForRating:value indexPath:indexPath];
} else {
cell = [self detailCellForKey:key value:value];
}
return cell;
}

- (RatingCell *)cellForRating:(NSNumber *)rating
indexPath:(NSIndexPath *)indexPath
{
// ...
}

- (UITableViewCell *)detailCellForKey:(NSString *)key
value:(id)value
{
// ...
}

编辑 Table View

Table view 提供了易于使用的编辑特性,允许你对 cell 进行删除或重新排序。这些事件都可以让 table view 的 data source 通过 delegate 方法得到通知。因此,通常我们能在这些 delegate 方法中看到对数据的进行修改的操作。

修改数据很明显是属于 model 层的任务。Model 应该为诸如删除或重新排序等操作暴露一个 API,然后我们可以在 data source 方法中调用它。这样,controller 就可以扮演 view 和 model 之间的协调者,而不需要知道 model 层的实现细节。并且还有额外的好处,model 的逻辑也变得更容易测试,因为它不再和 view controllers 的任务混杂在一起了。

总结

Table view controllers(以及其他的 controller 对象!)应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。你应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单的样板代码。

这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单的 API,最终,它变得更加容易理解,也更利于团队协作。

转自:https://www.jianshu.com/p/22df7a214b49

收起阅读 »

Core Image 和视频

在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。两个例...
继续阅读 »

在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。两个例子的完整源代码,请点击这里

总览

当涉及到处理视频的时候,性能就会变得非常重要。而且了解黑箱下的原理 —— 也就是 Core Image 是如何工作的 —— 也很重要,这样我们才能达到足够的性能。在 GPU 上面做尽可能多的工作,并且最大限度的减少 GPU 和 CPU 之间的数据传送是非常重要的。之后的例子中,我们将看看这个细节。

想对 Core Image 有个初步认识的话,可以读读 Warren 的这篇文章 Core Image 介绍。我们将使用 Swift 的函数式 API 中介绍的基于 CIFilter 的 API 封装。想要了解更多关于 AVFoundation 的知识,可以看看本期话题中 Adriaan 的文章,还有话题 #21 中的 iOS 上的相机捕捉。

优化资源的 OpenGL ES

CPU 和 GPU 都可以运行 Core Image,我们将会在 下面 详细介绍这两个的细节。在这个例子中,我们要使用 GPU,我们做如下几样事情。

我们首先创建一个自定义的 UIView,它允许我们把 Core Image 的结果直接渲染成 OpenGL。我们可以新建一个 GLKView 并且用一个 EAGLContext 来初始化它。我们需要指定 OpenGL ES 2 作为渲染 API,在这两个例子中,我们要自己触发 drawing 事件 (而不是在 -drawRect: 中触发),所以在初始化 GLKView 的时候,我们将 enableSetNeedsDisplay 设置为 false。之后我们有可用新图像的时候,我们需要主动去调用 -display。

在这个视图里,我们保持一个对 CIContext 的引用,它提供一个桥梁来连接我们的 Core Image 对象和 OpenGL 上下文。我们创建一次就可以一直使用它。这个上下文允许 Core Image 在后台做优化,比如缓存和重用纹理之类的资源等。重要的是这个上下文我们一直在重复使用。

上下文中有一个方法,-drawImage:inRect:fromRect:,作用是绘制出来一个 CIImage。如果你想画出来一个完整的图像,最容易的方法是使用图像的 extent。但是请注意,这可能是无限大的,所以一定要事先裁剪或者提供有限大小的矩形。一个警告:因为我们处理的是 Core Image,绘制的目标以像素为单位,而不是点。由于大部分新的 iOS 设备配备 Retina 屏幕,我们在绘制的时候需要考虑这一点。如果我们想填充整个视图,最简单的办法是获取视图边界,并且按照屏幕的 scale 来缩放图片 (Retina 屏幕的 scale 是 2)。

完整的代码示例在这里:CoreImageView.swift

从相机获取像素数据

对于 AVFoundation 如何工作的概述,请看 Adriaan 的文章 和 Matteo 的文章 iOS 上的相机捕捉。对于我们而言,我们想从镜头获得 raw 格式的数据。我们可以通过创建一个 AVCaptureDeviceInput 对象来选定一个摄像头。使用 AVCaptureSession,我们可以把它连接到一个 AVCaptureVideoDataOutput。这个 data output 对象有一个遵守 AVCaptureVideoDataOutputSampleBufferDelegate 协议的代理对象。这个代理每一帧将接收到一个消息:

func captureOutput(captureOutput: AVCaptureOutput!,
didOutputSampleBuffer: CMSampleBuffer!,
fromConnection: AVCaptureConnection!) {

我们将用它来驱动我们的图像渲染。在我们的示例代码中,我们已经将配置,初始化以及代理对象都打包到了一个叫做 CaptureBufferSource 的简单接口中去。我们可以使用前置或者后置摄像头以及一个回调来初始化它。对于每个样本缓存区,这个回调都会被调用,并且参数是缓冲区和对应摄像头的 transform:

source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
(buffer, transform) in
...
}

我们需要对相机返回的数据进行变换。无论你如何转动 iPhone,相机的像素数据的方向总是相同的。在我们的例子中,我们将 UI 锁定在竖直方向,我们希望屏幕上显示的图像符合照相机拍摄时的方向,为此我们需要后置摄像头拍摄出的图片旋转 -π/2。前置摄像头需要旋转 -π/2 并且加一个镜像效果。我们可以用一个 CGAffineTransform 来表达这种变换。请注意如果 UI 是不同的方向 (比如横屏),我们的变换也将是不同的。还要注意,这种变换的代价其实是非常小的,因为它是在 Core Image渲染管线中完成的。

接着,要把 CMSampleBuffer转换成 CIImage,我们首先需要将它转换成一个 CVPixelBuffer。我们可以写一个方便的初始化方法来为我们做这件事:

extension CIImage {
convenience init(buffer: CMSampleBuffer) {
self.init(CVPixelBuffer: CMSampleBufferGetImageBuffer(buffer))
}
}

现在我们可以用三个步骤来处理我们的图像。首先,把我们的 CMSampleBuffer 转换成 CIImage,并且应用一个形变,使图像旋转到正确的方向。接下来,我们用一个 CIFilter 滤镜来得到一个新的 CIImage 输出。我们使用了 Florian 的文章 提到的创建滤镜的方式。在这个例子中,我们使用色调调整滤镜,并且传入一个依赖于时间而变化的调整角度。最终,我们使用之前定义的 View,通过 CIContext 来渲染 CIImage。这个流程非常简单,看起来是这样的:

source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
[unowned self] (buffer, transform) in
let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
let filter = hueAdjust(self.angleForCurrentTime)
self.coreImageView?.image = filter(input)
}

当你运行它时,你可能会因为如此低的 CPU 使用率感到吃惊。这其中的奥秘是 GPU 做了几乎所有的工作。尽管我们创建了一个 CIImage,应用了一个滤镜,并输出一个 CIImage,最终输出的结果是一个 promise:直到实际渲染才会去进行计算。一个 CIImage 对象可以是黑箱里的很多东西,它可以是 GPU 算出来的像素数据,也可以是如何创建像素数据的一个说明 (比如使用一个滤镜生成器),或者它也可以是直接从 OpenGL 纹理中创建出来的图像。

从影片中获取像素数据

我们可以做的另一件事是通过 Core Image 把这个滤镜加到一个视频中。和实时拍摄不同,我们现在从影片的每一帧中生成像素缓冲区,在这里我们将采用略有不同的方法。对于相机,它会推送每一帧给我们,但是对于已有的影片,我们使用拉取的方式:通过 display link,我们可以向 AVFoundation 请求在某个特定时间的一帧。

display link 对象负责在每帧需要绘制的时候给我们发送消息,这个消息是按照显示器的刷新频率同步进行发送的。这通常用来做 自定义动画,但也可以用来播放和操作视频。我们要做的第一件事就是创建一个 AVPlayer和一个视频输出:

player = AVPlayer(URL: url)
videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferDict)
player.currentItem.addOutput(videoOutput)

接下来,我们要创建 display link。方法很简单,只要创建一个 CADisplayLink 对象,并将其添加到 run loop。

let displayLink = CADisplayLink(target: self, selector: "displayLinkDidRefresh:")
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

现在,唯一剩下的就是在 displayLinkDidRefresh: 调用的时候获取视频每一帧。首先,我们获取当前的时间,并且将它转换成当前播放项目里的时间比。然后我们询问 videoOutput,如果当前时间有一个可用的新的像素缓存区,我们把它复制一下并且调用回调方法:

func displayLinkDidRefresh(link: CADisplayLink) {
let itemTime = videoOutput.itemTimeForHostTime(CACurrentMediaTime())
if videoOutput.hasNewPixelBufferForItemTime(itemTime) {
let pixelBuffer = videoOutput.copyPixelBufferForItemTime(itemTime, itemTimeForDisplay: nil)
consumer(pixelBuffer)
}
}

我们从一个视频输出获得的像素缓冲是一个 CVPixelBuffer,我们可以把它直接转换成 CIImage。正如上面的例子,我们会加上一个滤镜。在这个例子里,我们将组合多个滤镜:我们使用一个万花筒的效果,然后用渐变遮罩把原始图像和过滤图像相结合,这个操作是非常轻量级的。

创意地使用滤镜

大家都知道流行的照片效果。虽然我们可以将这些应用到视频,但 Core Image 还可以做得更多。

Core Image 里所谓的滤镜有不同的类别。其中一些是传统的类型,输入一张图片并且输出一张新的图片。但有些需要两个 (或者更多) 的输入图像并且混合生成一张新的图像。另外甚至有完全不输入图片,而是基于参数的生成图像的滤镜。

通过混合这些不同的类型,我们可以创建意想不到的效果。

混合图片

在这个例子中,我们使用这些东西:


上面的例子可以将图像的一个圆形区域像素化。

它也可以创建交互,我们可以使用触摸事件来改变所产生的圆的位置。

Core Image Filter Reference 按类别列出了所有可用的滤镜。请注意,有一部分只能用在 OS X。

生成器和渐变滤镜可以不需要输入就能生成图像。它们很少自己单独使用,但是作为蒙版的时候会非常强大,就像我们例子中的 CIBlendWithMask 那样。

混合操作和 CIBlendWithAlphaMask 还有 CIBlendWithMask 允许将两个图像合并成一个。

CPU vs. GPU

我们在话题 #3 的文章,绘制像素到屏幕上里,介绍了 iOS 和 OS X 的图形栈。需要注意的是 CPU 和 GPU 的概念,以及两者之间数据的移动方式。

在处理实时视频的时候,我们面临着性能的挑战。

首先,我们需要能在每一帧的时间内处理完所有的图像数据。我们的样本中采用 24 帧每秒的视频,这意味着我们有 41 毫秒 (1/24 秒) 的时间来解码,处理以及渲染每一帧中的百万像素。

其次,我们需要能够从 CPU 或者 GPU 上面得到这些数据。我们从视频文件读取的字节数最终会到达 CPU 里。但是这个数据还需要移动到 GPU 上,以便在显示器上可见。

避免转移

一个非常致命的问题是,在渲染管线中,代码可能会把图像数据在 CPU 和 GPU 之间来回移动好几次。确保像素数据仅在一个方向移动是很重要的,应该保证数据只从 CPU 移动到 GPU,如果能让数据完全只在 GPU 上那就更好。

如果我们想渲染 24 fps 的视频,我们有 41 毫秒;如果我们渲染 60 fps 的视频,我们只有 16 毫秒,如果我们不小心从 GPU 下载了一个像素缓冲到 CPU 里,然后再上传回 GPU,对于一张全屏的 iPhone 6 图像来说,我们在每个方向将要移动 3.8 MB 的数据,这将使帧率无法达标。

当我们使用 CVPixelBuffer 时,我们希望这样的流程:


CVPixelBuffer 是基于 CPU 的 (见下文),我们用 CIImage 来包装它。构建滤镜链不会移动任何数据;它只是建立了一个流程。一旦我们绘制图像,我们使用了基于 EAGL 上下文的 Core Image 上下文,而这个 EAGL 上下文也是 GLKView 进行图像显示所使用的上下文。EAGL 上下文是基于 GPU 的。请注意,我们是如何只穿越 GPU-CPU 边界一次的,这是至关重要的部分。

工作和目标

Core Image 的图形上下文可以通过两种方式创建:使用 EAGLContext 的 GPU 上下文,或者是基于 CPU 的上下文。

这个定义了 Core Image 工作的地方,也就是像素数据将被处理的地方。与工作区域无关,基于 GPU 和基于 CPU 的图形上下文都可以通过执行 createCGImage(…),render(, toBitmap, …) 和 render(, toCVPixelBuffer, …),以及相关的命令来向 CPU 进行渲染。

重要的是要理解如何在 CPU 和 GPU 之间移动像素数据,或者是让数据保持在 CPU 或者 GPU 里。将数据移过这个边界是需要很大的代价的。

缓冲区和图像

在我们的例子中,我们使用了几个不同的缓冲区和图像。这可能有点混乱。这样做的原因很简单,不同的框架对于这些“图像”有不同的用途。下面有一个快速总览,以显示哪些是以基于 CPU 或者基于 GPU 的:


结论

Core Image 是操纵实时视频的一大利器。只要你适当的配置下,性能将会是强劲的 —— 只要确保 CPU 和 GPU 之间没有数据的转移。创意地使用滤镜,你可以实现一些非常炫酷的效果,神马简单色调,褐色滤镜都弱爆啦。所有的这些代码都很容易抽象出来,深入了解下不同的对象的作用区域 (GPU 还是 CPU) 可以帮助你提高代码的性能。

转自:https://www.jianshu.com/p/6c4118290a56

收起阅读 »

GCD你会用吗?GCD扫盲之dispatch_semaphore

本文是GCD多线程编程中dispatch_semaphore内容的小结,通过本文,你可以了解到:1、信号量的基本概念与基本使用2、信号量在线程同步与资源加锁方面的应用3、信号量释放时的小陷阱今天我来讲解一下dispatch_semaphore在我们平常开发中的...
继续阅读 »

本文是GCD多线程编程中dispatch_semaphore内容的小结,通过本文,你可以了解到:

1、信号量的基本概念与基本使用
2、信号量在线程同步与资源加锁方面的应用
3、信号量释放时的小陷阱

今天我来讲解一下dispatch_semaphore在我们平常开发中的一些基本概念与基本使用,dispatch_semaphore俗称信号量,也称为信号锁,在多线程编程中主要用于控制多线程下访问资源的数量,比如系统有两个资源可以使用,但同时有三个线程要访问,所以只能允许两个线程访问,第三个应当等待资源被释放后再访问,这时我们就可以使用dispatch_semaphore。

与dispatch_semaphore相关的共有3个方法,分别是dispatch_semaphore_create,dispatch_semaphore_wait,dispatch_semaphore_signal下面我们逐一了解一下这三个方法。

测试代码在这

semaphore的三个方法

dispatch_semaphore_create

/*!
* @function dispatch_semaphore_create
*
* @abstract
* Creates new counting semaphore with an initial value.
*
* @discussion
* Passing zero for the value is useful for when two threads need to reconcile
* the completion of a particular event. Passing a value greater than zero is
* useful for managing a finite pool of resources, where the pool size is equal
* to the value.
*
* @param value
* The starting value for the semaphore. Passing a value less than zero will
* cause NULL to be returned.
*
* @result
* The newly created semaphore, or NULL on failure.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_semaphore_t
dispatch_semaphore_create(long value);

dispatch_semaphore_create方法用于创建一个带有初始值的信号量dispatch_semaphore_t。

对于这个方法的参数信号量的初始值,这里有2种情况:

1、信号量初始值为0时:这种情况主要用于两个线程需要协调特定事件的完成时,即线程同步。
2、信号量初始值为大于0时:这种情况主要用于管理有限的资源池,其中池大小等于这个值,即资源加锁。
上面的2种情况(线程同步、资源加锁),我们在后续的使用篇中会详细讲解。

dispatch_semaphore_wait

/*!
* @function dispatch_semaphore_wait
*
* @abstract
* Wait (decrement) for a semaphore.
*
* @discussion
* Decrement the counting semaphore. If the resulting value is less than zero,
* this function waits for a signal to occur before returning.
*
* @param dsema
* The semaphore. The result of passing NULL in this parameter is undefined.
*
* @param timeout
* When to timeout (see dispatch_time). As a convenience, there are the
* DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
*
* @result
* Returns zero on success, or non-zero if the timeout occurred.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

dispatch_semaphore_wait这个方法主要用于等待或减少信号量,每次调用这个方法,信号量的值都会减一,然后根据减一后的信号量的值的大小,来决定这个方法的使用情况,所以这个方法的使用同样也分为2种情况:

1、当减一后的值小于0时,这个方法会一直等待,即阻塞当前线程,直到信号量+1或者直到超时。
2、当减一后的值大于或等于0时,这个方法会直接返回,不会阻塞当前线程。
上面2种方式,放到我们日常的开发中就是下面2种使用情况:

· 当我们只需要同步线程时,我们可以使用dispatch_semaphore_create(0)初始化信号量为0,然后使用dispatch_semaphore_wait方法让信号量减一,这时就属于第一种减一后小于0的情况,这时就会阻塞当前线程,直到另一个线程调用dispatch_semaphore_signal这个让信号量加1的方法后,当前线程才会被唤醒,然后执行当前线程中的代码,这时就起到一个线程同步的作用。

· 当我们需要对资源加锁,控制同时能访问资源的最大数量(假设为n)时,我们就需要使用dispatch_semaphore_create(n)方法来初始化信号量为n,然后使用dispatch_semaphore_wait方法将信号量减一,然后访问我们的资源,然后使用dispatch_semaphore_signal方法将信号量加一。如果有n个线程来访问这个资源,当这n个资源访问都还没有结束时,就会阻塞当前线程,第n+1个线程的访问就必须等待,直到前n个的某一个的资源访问结束,这就是我们很常见的资源加锁的情况。

dispatch_semaphore_signal

/*!
* @function dispatch_semaphore_signal
*
* @abstract
* Signal (increment) a semaphore.
*
* @discussion
* Increment the counting semaphore. If the previous value was less than zero,
* this function wakes a waiting thread before returning.
*
* @param dsema The counting semaphore.
* The result of passing NULL in this parameter is undefined.
*
* @result
* This function returns non-zero if a thread is woken. Otherwise, zero is
* returned.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);

dispatch_semaphore_signal方法用于让信号量的值加一,然后直接返回。如果先前信号量的值小于0,那么这个方法还会唤醒先前等待的线程。

semaphore使用篇

线程同步

这种情况在我们的开发中也是挺常见的,当主线程中有一个异步网络任务,我们需要等这个网络请求成功拿到数据后,才能继续做后面的处理,这时我们就可以使用信号量这种方式来进行线程同步。

我们首先看看完整测试代码:

- (IBAction)threadSyncTask:(UIButton *)sender {

NSLog(@"threadSyncTask start --- thread:%@",[NSThread currentThread]);

//1.创建一个初始值为0的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

//2.定制一个异步任务
//开启一个异步网络请求
NSLog(@"开启一个异步网络请求");
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url =
[NSURL URLWithString:[@"https://www.baidu.com/" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"%@", [error localizedDescription]);
}
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
NSLog(@"%@", dict);
}
NSLog(@"异步网络任务完成---%@",[NSThread currentThread]);
//4.调用signal方法,让信号量+1,然后唤醒先前被阻塞的线程
NSLog(@"调用dispatch_semaphore_signal方法");
dispatch_semaphore_signal(semaphore);
}];
[dataTask resume];

//3.调用wait方法让信号量-1,这时信号量小于0,这个方法会阻塞当前线程,直到信号量等于0时,唤醒当前线程
NSLog(@"调用dispatch_semaphore_wait方法");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"threadSyncTask end --- thread:%@",[NSThread currentThread]);
}

运行之后的log如下:

2019-04-27 17:24:27.050077+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}
2019-04-27 17:24:27.050227+0800 GCD(四) dispatch_semaphore[34482:6102243] 开启一个异步网络请求
2019-04-27 17:24:27.050571+0800 GCD(四) dispatch_semaphore[34482:6102243] 调用dispatch_semaphore_wait方法
2019-04-27 17:24:27.105069+0800 GCD(四) dispatch_semaphore[34482:6105851] (null)
2019-04-27 17:24:27.105262+0800 GCD(四) dispatch_semaphore[34482:6105851] 异步网络任务完成---<NSThread: 0x6000028c6ec0>{number = 6, name = (null)}
2019-04-27 17:24:27.105401+0800 GCD(四) dispatch_semaphore[34482:6105851] 调用dispatch_semaphore_signal方法
2019-04-27 17:24:27.105550+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}

从log中我们可以看出,wait方法会阻塞主线程,直到异步任务完成调用signal方法,才会继续回到主线程执行后面的任务。

资源加锁

当一个资源可以被多个线程读取修改时,就会很容易出现多线程访问修改数据出现结果不一致甚至崩溃的问题。为了处理这个问题,我们通常使用的办法,就是使用NSLock,@synchronized给这个资源加锁,让它在同一时间只允许一个线程访问资源。其实信号量也可以当做一个锁来使用,而且比NSLock还有@synchronized代价更低一些,接下来我们来看看它的基本使用

第一步,定义2个宏,将wait与signal方法包起来,方便下面的使用

#ifndef ZED_LOCK
#define ZED_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif

#ifndef ZED_UNLOCK
#define ZED_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif

第二步,声明与创建共享资源与信号锁

/* 需要加锁的资源 **/
@property (nonatomic, strong) NSMutableDictionary *dict;

/* 信号锁 **/
@property (nonatomic, strong) dispatch_semaphore_t lock;
//创建共享资源
self.dict = [NSMutableDictionary dictionary];
//初始化信号量,设置初始值为1
self.lock = dispatch_semaphore_create(1);

第三步,在即将使用共享资源的地方添加ZED_LOCK宏,进行信号量减一操作,在共享资源使用完成的时候添加ZED_UNLOCK,进行信号量加一操作。

- (IBAction)resourceLockTask:(UIButton *)sender {

NSLog(@"resourceLockTask start --- thread:%@",[NSThread currentThread]);

//使用异步执行并发任务会开辟新的线程的特性,来模拟开辟多个线程访问贡献资源的场景

for (int i = 0; i < 3; i++) {

NSLog(@"异步添加任务:%d",i);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

ZED_LOCK(self.lock);
//模拟对共享资源处理的耗时
[NSThread sleepForTimeInterval:1];
NSLog(@"i:%d --- thread:%@ --- 将要处理共享资源",i,[NSThread currentThread]);
[self.dict setObject:@"semaphore" forKey:@"key"];
NSLog(@"i:%d --- thread:%@ --- 共享资源处理完成",i,[NSThread currentThread]);
ZED_UNLOCK(self.lock);

});
}

NSLog(@"resourceLockTask end --- thread:%@",[NSThread currentThread]);
}

在这一步中,我们使用异步执行并发任务会开辟新的线程的特性,来模拟开辟多个线程访问贡献资源的场景,同时使用了线程休眠的API来模拟对共享资源处理的耗时。这里我们开辟了3个线程来并发访问这个共享资源,代码运行的log如下:

2019-04-27 18:36:25.275060+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask start --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:25.275312+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:0
2019-04-27 18:36:25.275508+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:1
2019-04-27 18:36:25.275680+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:2
2019-04-27 18:36:25.275891+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask end --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:26.276757+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 将要处理共享资源
2019-04-27 18:36:26.277004+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 共享资源处理完成
2019-04-27 18:36:27.282099+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 将要处理共享资源
2019-04-27 18:36:27.282357+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 共享资源处理完成
2019-04-27 18:36:28.283769+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 将要处理共享资源
2019-04-27 18:36:28.284041+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 共享资源处理完成

从多次log中我们可以看出:

添加信号锁之后,每个线程对于共享资源的操作都是有序的,并不会出现2个线程同时访问锁中的代码区域。

我把上面的实现代码简化一下,方便分析这种锁的实现原理:

//step_1
ZED_LOCK(self.lock);
//step_2
NSLog(@"执行任务");
//step_3
ZED_UNLOCK(self.lock);

1、信号量初始化的值为1,当一个线程过来执行step_1的代码时,会调用信号量的值减一的方法,这时,信号量的值为0,它会直接返回,然后执行step_2的代码去完成去共享资源的访问,然后再使用step_3中的signal方法让信号量加一,信号量的值又会回归到初始值1。这就是一个线程过来访问的调用流程。

2、当线程1过来执行到step_2的时候,这时又有一个线程2它也从step_1处来调用这段代码,由于线程1已经调用过step_1的wait方法将信号量的值减一,这时信号量的值为0。同时线程2进入然后调用了step_1的wait方法又将信号量的值减一,这时的信号量的值为-1,由于信号量的值小于0时会阻塞当前线程(线程2),所以,线程2就会一直等待,直到线程1执行完step_3中的方法,将信号量加一,才会唤醒线程2,继续执行下面的代码。这就是为什么信号量可以对共享资源加锁的原因,如果我们可以允许n个线程同时访问,我们就需要在初始化这个信号量时把信号量的值设为n,这样就限制了访问共享资源的线程数。

通过上面的分析,我们可以知道,如果我们使用信号量来进行线程同步时,我们需要把信号量的初始值设为0,如果要对资源加锁,限制同时只有n个线程可以访问的时候,我们就需要把信号量的初始值设为n。

semaphore的释放

在我们平常的开发过程中,如果对semaphore使用不当,就会在它释放的时候遇到奔溃问题。

首先我们来看2个例子:

- (IBAction)crashScene1:(UIButton *)sender {

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

//在使用过程中将semaphore置为nil
semaphore = nil;
}
- (IBAction)crashScene2:(UIButton *)sender {

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

//在使用过程中对semaphore进行重新赋值
semaphore = dispatch_semaphore_create(3);
}

我们打开测试代码,找到semaphore对应的target,然后运行一下代码,然后点击后面2个按钮调用一下上面的代码,然后我们可以发现,代码在运行到semaphore = nil;与semaphore = dispatch_semaphore_create(3);时奔溃了。然后我们使用lldb的bt命令查看一下调用栈。

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
frame #0: 0x0000000111c31309 libdispatch.dylib`_dispatch_semaphore_dispose + 59
frame #1: 0x0000000111c2fb06 libdispatch.dylib`_dispatch_dispose + 97
* frame #2: 0x000000010efb113b GCD(四) dispatch_semaphore`-[ZEDDispatchSemaphoreViewController crashScene1:](self=0x00007fdcfdf0add0, _cmd="crashScene1:", sender=0x00007fdcfdd0a3d0) at ZEDDispatchSemaphoreViewController.m:117
frame #3: 0x0000000113198ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
frame #4: 0x0000000112bd40bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
frame #5: 0x0000000112bd43da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
frame #6: 0x0000000112bd331e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
frame #7: 0x00000001131d40a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
frame #8: 0x00000001131d57a0 UIKitCore`-[UIWindow sendEvent:] + 4080
frame #9: 0x00000001131b3394 UIKitCore`-[UIApplication sendEvent:] + 352
frame #10: 0x00000001132885a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
frame #11: 0x000000011328b1cb UIKitCore`__handleEventQueueInternal + 5948
frame #12: 0x0000000110297721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #13: 0x0000000110296f93 CoreFoundation`__CFRunLoopDoSources0 + 243
frame #14: 0x000000011029163f CoreFoundation`__CFRunLoopRun + 1263
frame #15: 0x0000000110290e11 CoreFoundation`CFRunLoopRunSpecific + 625
frame #16: 0x00000001189281dd GraphicsServices`GSEventRunModal + 62
frame #17: 0x000000011319781d UIKitCore`UIApplicationMain + 140
frame #18: 0x000000010efb06a0 GCD(四) dispatch_semaphore`main(argc=1, argv=0x00007ffee0c4efc8) at main.m:14
frame #19: 0x0000000111ca6575 libdyld.dylib`start + 1
frame #20: 0x0000000111ca6575 libdyld.dylib`start + 1
(lldb)

从上面的调用栈我们可以看出,奔溃的地方都处于libdispatch库调用dispatch_semaphore_dispose方法释放信号量的时候,为什么在信号量使用过程中对信号量进行重新赋值或置空操作会crash呢,这个我们就需要从GCD的源码层面来分析了,GCD的源码库libdispatch在苹果的开源代码库可以下载,我在自己的Github也放了一份libdispatch-187.10版本的,下面的源码分析都是基于这个版本的。

首先我们来看一下dispatch_semaphore_t的结构体dispatch_semaphore_s的结构体定义

struct dispatch_semaphore_s {
DISPATCH_STRUCT_HEADER(dispatch_semaphore_s, dispatch_semaphore_vtable_s);
long dsema_value; //当前的信号值
long dsema_orig; //初始化的信号值
size_t dsema_sent_ksignals;
#if USE_MACH_SEM && USE_POSIX_SEM
#error "Too many supported semaphore types"
#elif USE_MACH_SEM
semaphore_t dsema_port; //当前mach_port_t信号
semaphore_t dsema_waiter_port; //休眠时mach_port_t信号
#elif USE_POSIX_SEM
sem_t dsema_sem;
#else
#error "No supported semaphore type"
#endif
size_t dsema_group_waiters;
struct dispatch_sema_notify_s *dsema_notify_head;//链表头部
struct dispatch_sema_notify_s *dsema_notify_tail;//链表尾部
};

这里我们需要关注2个值的变化,dsema_value与dsema_orig,它们分别代表当前的信号值与初始化时的信号值。

当我们调用dispatch_semaphore_create方法创建信号量时,这个方法内部会把传入的参数存储到dsema_value(当前的value)和dsema_orig(初始value)中,条件是value的值必须大于或等于0。

dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
dispatch_semaphore_t dsema;

// If the internal value is negative, then the absolute of the value is
// equal to the number of waiting threads. Therefore it is bogus to
// initialize the semaphore with a negative value.
if (value < 0) {//初始值不能小于0
return NULL;
}

dsema = calloc(1, sizeof(struct dispatch_semaphore_s));//申请信号量的内存

if (fastpath(dsema)) {//信号量初始化赋值
dsema->do_vtable = &_dispatch_semaphore_vtable;
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
dsema->do_ref_cnt = 1;
dsema->do_xref_cnt = 1;
dsema->do_targetq = dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dsema->dsema_value = value;//当前的值
dsema->dsema_orig = value;//初始值
#if USE_POSIX_SEM
int ret = sem_init(&dsema->dsema_sem, 0, 0);//内存空间映射
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif
}

return dsema;
}

然后调用dispatch_semaphore_wait与dispatch_semaphore_signal时会对dsema_value做加一或减一操作。当我们对信号量置空或者重新赋值操作时,会调用dispatch_semaphore_dispose释放信号量,我们来看看对应的源码

static void
_dispatch_semaphore_dispose(dispatch_semaphore_t dsema)
{
if (dsema->dsema_value < dsema->dsema_orig) {//当前的信号值如果小于初始值就会crash
DISPATCH_CLIENT_CRASH(
"Semaphore/group object deallocated while in use");
}

#if USE_MACH_SEM
kern_return_t kr;
if (dsema->dsema_port) {
kr = semaphore_destroy(mach_task_self(), dsema->dsema_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
if (dsema->dsema_waiter_port) {
kr = semaphore_destroy(mach_task_self(), dsema->dsema_waiter_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
#elif USE_POSIX_SEM
int ret = sem_destroy(&dsema->dsema_sem);
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif

_dispatch_dispose(dsema);
}

从源码中我们可以看出,当dsema_value小于dsema_orig时,即信号量还在使用时,会直接调用DISPATCH_CLIENT_CRASH让APP奔溃。

所以,我们在使用信号量的时候,不能在它还在使用的时候,进行赋值或者置空的操作。

如果文中有错误的地方,或者与你的想法相悖的地方,请在评论区告知我,我会继续改进,如果你觉得这个篇文章总结的还不错,麻烦动动小手,给我的文章与Git代码样例点个✨

链接:https://www.jianshu.com/p/7981e3357fe9

收起阅读 »

探究iOS鲜为人知的小秘密一一__attribute__运用

Clang Attributes是Clang提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程,一般以attribute(xxx)的形式出现在代码中;为...
继续阅读 »

Clang Attributes是Clang提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程,一般以attribute(xxx)的形式出现在代码中;为方便使用,一些常用属性也被Cocoa定义成宏,比如在系统头文件中经常出现的NS_CLASS_AVAILABLE_IOS(9_0)就是attribute(availability(...))这个属性的简单写法。

※unavailable
#define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable))

可以加在方法声明的后面,告诉编译器该方法不可用

Swift中

@available(*, unavailable)

func foo() {}

@available(iOS, unavailable, message="you can't call this")

func foo2() {}

※availability

#define NS_DEPRECATED_IOS(_iosIntro,_iosDep,...) CF_DEPRECATED_IOS(_iosIntro,_iosDep,__VA_ARGS__)

展开看

#define CF_DEPRECATED_IOS(_iosIntro, _iosDep, ...) __attribute__((availability(ios,introduced=_iosIntro,deprecated=_iosDep,message="" __VA_ARGS__)))

再展开看

__attribute__((availability(ios,introduced=2_0,deprecated=7_0,message=""__VA_ARGS__)))

iOS即是iOS平台

introduced从哪个版本开始使用

deprecated从哪个版本开始弃用

message警告的信息

其实还可以再加一个参数例子:

void f(void) __attribute__((availability(macosx,introduced=10.4,deprecated=10.6,obsoleted=10.7)));

obsoleted完全禁止使用的版本

NS_AVAILABLE_IOS(7_0) iOS7.0或之后才能使用

NS_DEPRECATED_IOS(2_0,6_0) iOS2.0开始使用6.0废弃

Swift中:

@available(iOS 6.0, *)

public var minimumScaleFactor: CGFloat

@available(OSX, introduced=10.4, deprecated=10.6, obsoleted=10.10)

@available(iOS, introduced=5.0, deprecated=7.0)

func foo3() {}

※objc_subclassing_restricted

一句话就是使用这个属性可以定义一个不可被继承的类

__attribute__((objc_subclassing_restricted))

@interface Eunuch : NSObject

@end

@interface Child : Eunuch//如果继承它会报错

@end

在Swift中对原来的很多attribute的支持现在还缺失中,为了达到类似的目的,我们可以使用一个final关键词


※objc_requires_super

使用这个属性标志子类继承这个方法时需要调用super,否则给出编译警告,来让父类中有一些关键代码是在被继承重写后必须执行

#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))


在Switf中也还是可以用final的方法来达到这个目的


Swift equivalent to __attribute((objc_requires_super))?(stackoverflow)

关于Swift中的final的详细讲解

※overloadable

用于C函数,可以定义若干个相同函数名,但参数不同的方法,调用时编译器会自动根据参数选择函数去调用

__attribute__((overloadable))

void logAnything(id obj) {

NSLog(@"%@", obj);

}

__attribute__((overloadable)) void logAnything(int number) {

NSLog(@"%@", @(number));

}

__attribute__((overloadable)) void logAnything(CGRect rect) {

NSLog(@"%@", NSStringFromCGRect(rect));

}

// Tests

logAnything(@[@"1", @"2"]);

logAnything(233);

logAnything(CGRectMake(1, 2, 3, 4));

有兴趣的可以写一个自定义的Log免去用NSLog要写@""等格式的麻烦

※cleanup

__attribute__((cleanup(...))),用于修饰一个变量,在它的作用域结束时可以自动执行一个指定的方法,个人感觉这个真的很方便


一个对象可以这样用,那么block实际也是一个对象,这样就可以写一个宏,实际上就是ReactiveCocoa中神奇的@onExit

#define onExit\

__strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^

有时候我们需要用到锁这个东西,或者一些CoreFoundation的对象有时候最后需要释放,用这个宏就很方便了


为了好看用这个宏的时候加个@就加个释放池就可以了


sunnyxx这篇博客讲的很清楚

Swift中:可以用defer


还有一些format, const, noreturn, aligned , packed, objc_boxable, constructor / destructor, enable_if, objc_runtime_name可以看这两篇文章:

神奇的attribute

Clang Attributes黑魔法小记

链接:https://www.jianshu.com/p/33d7d0596028

收起阅读 »

iOS-拍照后裁剪,不可拖动照片的问题

问题在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewController遇到一点问题:1、使用拍照功能,进行截取时(allowEditing = YES)时,拍照完成的图片无法拖动,没有办法进行选择性的截取图片2、如...
继续阅读 »

问题

在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewController遇到一点问题:

1、使用拍照功能,进行截取时(allowEditing = YES)时,拍照完成的图片无法拖动,没有办法进行选择性的截取图片
2、如果使用选择相册功能,进入裁剪界面,图片是可以拖动的,唯独拍照之后进入裁剪界面无法拖动
3、微信头像更换拍照好像也无法拖动,初步推测可能使用的系统自带的裁剪界面
所以想来仔细研究一下UIImagePickViewController的属性和使用方法

UIImagePickViewController

UIImagePickerController是iOS系统提供的和系统的相册和相机交互的一个类,可以用来获取相册的照片,也可以调用系统的相机拍摄照片或者视频。该类的继承结构是:

UIImagePickerController-->UINavigationController-->UIViewController-->UIResponder-->NSObject

官方文档中对于该类的说明是:

该类只支持竖屏模式,为了保证该类被原样使用,它不支持子类,并且它的视图层次是私有的不能被修改,只支持自定义cameraOverlayView属性来展示更多信息以及和用户的交互。

由于该类继承自UINavgationController,所以在使用过程中一般实现UIImagePickerControllerDelegate和UINavigationControllerDelegate这两个代理,可以利用navgation的push 和pop操作自定义界面实现更复杂的交互效果。

下面具体分析该类的一些方法和属性.

UIImagePickViewController之常用属性

@property (nullable, nonatomic, weak) id <UINavigationControllerDelegate, UIImagePickerControllerDelegate> delegate;

该对象的代理需要实现UINavigationControllerDelegate和UIImagePickerControllerDelegate协议,nullable是xcode6.3之后引入的nullability annotations特性,主要用于在OC和swift之间的转换。这一特性主要包含两个新的类型注释nullable和nonnull,用于表示对象是否可以是NULL或nil

@property (nonatomic) UIImagePickerControllerSourceType sourceType; // default value is UIImagePickerControllerSourceTypePhotoLibrary.

sourceType用于指定要访问的系统的媒体类型。UIImagePickerControllerSourceType支持以下3种枚举类型,默认值是图片库

typedef NS_ENUM(NSInteger, UIImagePickerControllerSourceType) { UIImagePickerControllerSourceTypePhotoLibrary,//照片库模式。图像选取控制器以该模式显示时会浏览系统照片库的根目录。 UIImagePickerControllerSourceTypeCamera, //相机模式,图像选取控制器以该模式显示时可以进行拍照或摄像。 UIImagePickerControllerSourceTypeSavedPhotosAlbum //相机胶卷模式,图像选取控制器以该模式显示时会浏览相机胶卷目录。};

1、PhotoLibrary代表系统照片应用对应的相薄,包含照片流和其它自定义的相册
2、PhotosAlbum则对应系统照片应用的照片,包含用设备拍摄的所有照片流。
3、Camera则代表相机的摄像头。

@property (nonatomic, copy) NSArray<NSString *> *mediaTypes;

mediaTypes用于设置相机支持的功能,拍照或者是视频,返回值类型可以是kUTTypeMovie视频和kUTTypeImage拍照

1、kUTTypeMovie包含

const CFStringRef kUTTypeAudiovisualContent ;抽象的声音视频
const CFStringRef kUTTypeMovie ;抽象的媒体格式(声音和视频)
const CFStringRef kUTTypeVideo ;只有视频没有声音
const CFStringRef kUTTypeAudio ;只有声音没有视频
const CFStringRef kUTTypeQuickTimeMovie ;
const CFStringRef kUTTypeMPEG ;
const CFStringRef kUTTypeMPEG4 ;
const CFStringRef kUTTypeMP3 ;
const CFStringRef kUTTypeMPEG4Audio ;
const CFStringRef kUTTypeAppleProtectedMPEG4Audio;

2、kUTTypeImage包含

const CFStringRef kUTTypeImage ;抽象的图片类型
const CFStringRef kUTTypeJPEG ;
const CFStringRef kUTTypeJPEG2000 ;
const CFStringRef kUTTypeTIFF ;
const CFStringRef kUTTypePICT ;
const CFStringRef kUTTypeGIF ;
const CFStringRef kUTTypePNG ;
const CFStringRef kUTTypeQuickTimeImage ;
const CFStringRef kUTTypeAppleICNS const CFStringRef kUTTypeBMP;
const CFStringRef kUTTypeICO;
@property (nonatomic) BOOL showsCameraControls NS_AVAILABLE_IOS(3_1);
@property (nonatomic) BOOL allowsEditing NS_AVAILABLE_IOS(3_1); // replacement for -allowsImageEditing; default value is NO.
@property (nonatomic) BOOL allowsImageEditing NS_DEPRECATED_IOS(2_0, 3_1);

1、showsCameraControls用于指定拍照时下方的工具栏是否显示
2、allowImageEditing在iOS3.1就已废弃,取而代之的是allowEditing,
3、allowEditing表示拍完照片或者从相册选完照片后,是否跳转到编辑模式对图片裁剪,只有在showsCameraControls为YES时才有效果。

@property (nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode NS_AVAILABLE_IOS(4_0); // default is UIImagePickerControllerCameraCaptureModePhoto
@property (nonatomic) UIImagePickerControllerCameraDevice cameraDevice NS_AVAILABLE_IOS(4_0); // default is UIImagePickerControllerCameraDeviceRear
@property (nonatomic) UIImagePickerControllerCameraFlashMode cameraFlashMode
@property (nonatomic) CGAffineTransform cameraViewTransform NS_AVAILABLE_IOS(3_1); // set the transform of the preview view.
@property (nullable, nonatomic,strong) __kindof UIView *cameraOverlayView NS_AVAILABLE_IOS(3_1); // set a view to overlay the preview view.

当sourceType是camera的时候,这几个属性有效,否则抛出异常。

· cameraCaptureMode捕捉模式指定的是相机是拍摄照片还是视频,它的枚举类型如下:

NS_ENUM(NSInteger, UIImagePickerControllerCameraCaptureMode) { 
UIImagePickerControllerCameraCaptureModePhoto,//photo
UIImagePickerControllerCameraCaptureModeVideo//video
};

· cameraDevice指定拍摄的摄像头位置,是使用前置摄像头还是后置摄像头,它的枚举类型有:

typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraDevice) { 
UIImagePickerControllerCameraDeviceRear,//后摄像头(默认)
UIImagePickerControllerCameraDeviceFront//前摄像头
};

· cameraFlashMode用于指定闪光灯模式,它的枚举类型如下:

typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraFlashMode) { 
UIImagePickerControllerCameraFlashModeOff = -1,//关闭闪关灯
UIImagePickerControllerCameraFlashModeAuto = 0,//自动
UIImagePickerControllerCameraFlashModeOn = 1//开启闪关灯
};

· cameraViewTransform该结构体可以用于指定拍摄时View的一些形变属性,如旋转缩放等。当showsCameraControls为NO,系统的工具栏隐藏时,我们可以自定义背景View赋值给cameraOverlayView添加到拍摄时的预览视图之上。

@property(nonatomic) NSTimeInterval videoMaximumDuration NS_AVAILABLE_IOS(3_1); // default value is 10 minutes.
@property(nonatomic) UIImagePickerControllerQualityType videoQuality NS_AVAILABLE_IOS(3_1);

· videoMaximumDuration用于设置视频拍摄模式下最大拍摄时长,默认值是10分钟。
· videoQuality表示拍摄的视频质量设置,默认是Medium即表示中等质量。 videoQuality支持的枚举类型如下:

typedef NS_ENUM(NSInteger, UIImagePickerControllerQualityType) { 
UIImagePickerControllerQualityTypeHigh = 0, // 高清模式
UIImagePickerControllerQualityTypeMedium = 1, //中等质量,适于WIFI传播
UIImagePickerControllerQualityTypeLow = 2, //低等质量,适于蜂窝网络传输
UIImagePickerControllerQualityType640x480 NS_ENUM_AVAILABLE_IOS(4_0) = 3, // VGA 质量
UIImagePickerControllerQualityTypeIFrame1280x720 NS_ENUM_AVAILABLE_IOS(5_0) = 4,//1280*720的分辨率
UIImagePickerControllerQualityTypeIFrame960x540 NS_ENUM_AVAILABLE_IOS(5_0) = 5,//960*540分辨率
};

UIImagePickViewController之类方法

@interface UIImagePickerController : UINavigationController <NSCoding>
+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType; // returns YES if source is available (i.e. camera present)
+ (nullable NSArray<NSString *> *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType; // returns array of available media types (i.e. kUTTypeImage)
+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0); // returns YES if camera device is available
+ (BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0); // returns YES if camera device supports flash and torch.
+ (nullable NSArray<NSNumber *> *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0);

isSourceTypeAvailable用于判断当前设备是否支持指定的sourceType,可以是照片库/相册/相机.
isCameraDeviceAvailable判断当前设备是否支持前置摄像头或者后置摄像头
isFlashAvailableForCameraDevice是否支持前置摄像头闪光灯或者后置摄像头闪光灯
availableMediaTypesForSourceType方法返回所特定的媒体如相册/图片库/相机所支持的媒体类型数组,元素值可以是kUTTypeImage类型或者kUTTypeMovie类型的静态字符串,所以是NSString类型的数组
availableCaptureModesForCameraDevice返回特定的摄像头(前置摄像头/后置摄像头)所支持的拍摄模式数值数组,元素值可以是UIImagePickerControllerCameraCaptureMode枚举里面的video或者photo,所以是NSNumber类型的数组

UIImagePickViewController之对象方法

- (void)takePicture NS_AVAILABLE_IOS(3_1);
- (BOOL)startVideoCapture NS_AVAILABLE_IOS(4_0);
- (void)stopVideoCapture NS_AVAILABLE_IOS(4_0);

1、takePicture可以用来实现照片的连续拍摄,需要自己自定义拍摄的背景视图来赋值给cameraOverlayView,结合自定义overlayView实现多张照片的采集,在收到代理的didFinishPickingMediaWithInfo方法之后可以启动额外的捕捉。
2、startVideoCapture用来判断当前是否可以开始录制视频,当视频正在拍摄中,设备不支持视频拍摄,磁盘空间不足等情况,该方法会返回NO.该方法结合自定义overlayView可以拍摄多部视频
3、stopVideoCapture当你调用此方法停止视频拍摄时,它会调用代理的imagePickerController:didFinishPickingMediaWithInfo:方法

UIImagePickViewController之代理方法

@protocol UIImagePickerControllerDelegate<NSObject>
@optional
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo NS_DEPRECATED_IOS(2_0, 3_0);
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
@end

1、imagePickerController:didFinishPickingImage:editingInfo:在iOS3.0中已废弃,不再使用。
2、当用户取消选取的内容时会调用DidCancel方法,默认实现销毁弹出的视图。
3、当完成内容的选取时会调用didFinishPickingMediaWithInfo方法,默认info字典的key值可以是以下类型:

UIKIT_EXTERN NSString *const UIImagePickerControllerMediaType; //指定用户选择的媒体类型
UIKIT_EXTERN NSString *const UIImagePickerControllerOriginalImage; // 原始图片
UIKIT_EXTERN NSString *const UIImagePickerControllerEditedImage; // 修改后的图片
UIKIT_EXTERN NSString *const UIImagePickerControllerCropRect; // 裁剪尺寸
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaURL; // 媒体的URL
UIKIT_EXTERN NSString *const UIImagePickerControllerReferenceURL NS_AVAILABLE_IOS(4_1); // 原件的URL
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaMetadata //当数据来源是相机的时候获取到的静态图像元数据,可以使用phtoho框架进行处理

UIImagePickViewController之C语言函数(保存图片或视频)

UIKIT_EXTERN void UIImageWriteToSavedPhotosAlbum(UIImage *image, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo);
UIKIT_EXTERN BOOL UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) NS_AVAILABLE_IOS(3_1);
UIKIT_EXTERN void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo) NS_AVAILABLE_IOS(3_1);

1、UIImageWriteToSavedPhotosAlbum用来保存照片到相册,seletor应该设置为- (void)image:(UIImage )image didFinishSavingWithError:(NSError )error contextInfo:(void )contextInfo;当照片保存到相册完成时,会调用该方法通知你。
2、UIVideoAtPathIsCompatibleWithSavedPhotosAlbum会返回布尔类型的值判断该路径下的视频能否保存到相册,视频需要先存储到沙盒文件再保存到相册,而照片是可以直接从代理完成的回调info字典里面获取到。
3、UISaveVideoAtPathToSavedPhotosAlbum用来保存视频到相册,seletor应该设置为- (void)video:(NSString )videoPath didFinishSavingWithError:(NSError )error contextInfo:(void )contextInfo;当视频保存到相册或出错时会调用该方法通知你。
这三个方法一般是在代理的完成方法didFinishPickingMediaWithInfo里面配合使用。
以上便是系统UIImagePickViewController的所有属性和方法的简单介绍

问题解决方案

由于使用设置allowEditing = YES属性后,开启摄像头拍照后直接进入系统自定义的裁剪界面,但是在这个界面,从摄像头拍摄过来的照片无法拖动裁剪,仅仅从相册选择过来的照片是可以拖动进行裁剪的,那么我们就决定裁剪界面不使用系统自带的,而自定义一个裁剪界面.

在UIImagePickViewController的代理方法里

#pragma UIImagePickerController Delegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *portraitImg = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
YYImageClipViewController *imgClipVC = [[YYImageClipViewController alloc] initWithImage:portraitImg cropFrame:CGRectMake(0, 100.0f, self.view.frame.size.width, self.view.frame.size.width) limitScaleRatio:3.0];
imgClipVC.delegate = self;
[picker pushViewController:imgClipVC animated:NO];
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[picker dismissViewControllerAnimated:YES completion:nil];
}

HZImageCropperViewController是我自定义的一个裁剪界面,通过这个控制器的两个代理方法,得到裁剪后的照片

- (void)imageClip:(YYImageClipViewController *)clipViewController didFinished:(UIImage *)editedImage 
{
//保存图片
NSString *imageFilePath = [UIImage saveImage:editedImage];
//上传七牛服务器
[self upImageFilePath:imageFilePath];
//隐藏裁剪界面
[clipViewController dismissViewControllerAnimated:YES completion:nil];
}

- (void)imageClipDidCancel:(YYImageClipViewController *)clipViewController
{
[clipViewController dismissViewControllerAnimated:YES completion:nil];
}

下面附上一个demo,里面包含裁剪界面的源代码,可供大家参考
自定义裁剪界面demo

转自:https://www.jianshu.com/p/9474ca73e269

收起阅读 »

【环信大学】深入浅出Runtime(2)

逻辑图终端代码及源码clang 命令行及源码.zip


逻辑图


终端代码及源码

clang 命令行及源码.zip




iOS 详解socket编程[oc]粘包、半包处理

在做socket编程时,如果是做tcp连接,那就不可避免的会遇到粘包与半包的问题。粘包 就是多组数据被一并接收了,粘在了一起,无法做划分;半包 就是有数据接收不完整,无法处理。要解决粘包、半包的问题,一般在设计数据(消息)格式时会约定好一个字段专门用于描述数据...
继续阅读 »

在做socket编程时,如果是做tcp连接,那就不可避免的会遇到粘包与半包的问题。

粘包 就是多组数据被一并接收了,粘在了一起,无法做划分;

半包 就是有数据接收不完整,无法处理。

要解决粘包、半包的问题,一般在设计数据(消息)格式时会约定好一个字段专门用于描述数据包的长度,这样就使数据有了边界,依靠这个边界,就能把每组数据划分出来,数据不完整时也能获知数据的缺失。

(当然也可以把数据设计成定长数据,但这样不够灵活;或者用\n,\r这类字符作为数据划分依据,但不直观、不明确,同时也不灵活)

举个栗子:

消息=消息头+消息体。消息头用于描述消息本身的基本信息,消息体则为消息的具体内容


如上图所示,假如我们的一个消息是这么定义的

消息头 = msgId(4B)+version(2B)+len(4B),共占用10字节

消息体 =  len中描述的16字节长

所以这条消息的长度就是 26字节

可以看到,要想知道一条完整数据的边界,关键就是消息头中的len字段

假如我们现在接收到的数据是这样的:


这个情况下即包含了粘包,也出现了半包的情况,三个数据包粘在了一起,最后一个数据包没有接收完全,出现了半包的情况,看看代码如何处理

- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 
{
while (_readBuf.length >= 10)//因为头部固定10个字节,数据长度至少要大于10个字节,我们才能得到完整的消息描述信息
{
NSData *head = [_readBuf subdataWithRange:NSMakeRange(0, 10)];//取得头部数据
NSData *lengthData = [head subdataWithRange:NSMakeRange(6, 4)];//取得长度数据
NSInteger length = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];//得出内容长度
NSInteger complateDataLength = length + 10;//算出一个包完整的长度(内容长度+头长度)
if (_readBuf.length >= complateDataLength)//如果缓存中数据够一个整包的长度
{
NSData *data = [_readBuf subdataWithRange:NSMakeRange(0, complateDataLength)];//截取一个包的长度(处理粘包)
[self handleTcpResponseData:data];//处理包数据
//从缓存中截掉处理完的数据,继续循环
_readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(complateDataLength, _readBuf.length - complateDataLength)]];
}
else//如果缓存中的数据长度不够一个包的长度,则包不完整(处理半包,继续读取)
{
[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
return;
}
}
//缓存中数据都处理完了,继续读取新数据
[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
}


摘自:https://www.jb51.net/article/105278.htm


收起阅读 »

Socket简析与iOS实现

Socket的基本概念1.定义网络上两个程序通过一个双向通信连接实现数据交互,这种双向通信的连接叫做Socket(套接字)。2.本质网络模型中应用层与TCP/IP协议族通信的中间软件抽象层,是它的一组编程接口(API),也即对TCP/IP的封装。TCP/IP也...
继续阅读 »

Socket的基本概念

1.定义

网络上两个程序通过一个双向通信连接实现数据交互,这种双向通信的连接叫做Socket(套接字)。

2.本质

网络模型中应用层与TCP/IP协议族通信的中间软件抽象层,是它的一组编程接口(API),也即对TCP/IP的封装。TCP/IP也要提供可供程序员做网络开发所用的接口,即Socket编程接口。


3.要素

Socket是网络通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,包含进行网络通信的必须的五种信息:

  • 连接使用的协议
  • 本地主机的IP地址
  • 本地进程的协议端口
  • 远程主机的IP地址
  • 远程进程的协议端口

4.特性

Socket可以支持不同的传输协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接;同理,当使用UDP协议进行连接时,该Socket连接就是一个UDP连接。

多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

5.连接

建立Socket连接至少需要一对套接字,分别运行于服务端(ServerSocket)和客户端(ClientSocket)。套接字直接的连接过程氛围三个步骤:

Step 1 服务器监听

服务端Socket始终处于等待连接状态,实时监听是否有客户端请求连接。

Step 2 客户端请求

客户端Socket提出连接请求,指定服务端Socket的地址和端口号,这时就可以向对应的服务端提出Socket连接请求。

Step 3 连接确认

当服务端Socket监听到客户端Socket提出的连接请求时作出响应,建立一个新的进程,把服务端Socket的描述发送给客户端,该描述得到客户端确认后就可建立起Socket连接。而服务端Socket则继续处于监听状态,继续接收其他客户端Socket的请求。

iOS客户端Socket的实现

1. 数据流方式

- (IBAction)connectToServer:(id)sender {
// 1.与服务器通过三次握手建立连接
NSString *host = @"192.168.1.58";
int port = 1212;

//创建一个socket对象
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];

NSError *error = nil;

// 开始连接
[_socket connectToHost:host
onPort:port
error:&error];

if (error) {
NSLog(@"%@",error);
}
}


#pragma mark - Socket代理方法
// 连接成功
- (void)socket:(GCDAsyncSocket *)sock
didConnectToHost:(NSString *)host
port:(uint16_t)port {
NSLog(@"%s",__func__);
}


// 断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock
withError:(NSError *)err {
if (err) {
NSLog(@"连接失败");
} else {
NSLog(@"正常断开");
}
}


// 发送数据
- (void)socket:(GCDAsyncSocket *)sock
didWriteDataWithTag:(long)tag {

NSLog(@"%s",__func__);

//发送完数据手动读取,-1不设置超时
[sock readDataWithTimeout:-1
tag:tag];
}

// 读取数据
-(void)socket:(GCDAsyncSocket *)sock
didReadData:(NSData *)data
withTag:(long)tag {

NSString *receiverStr = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];

NSLog(@"%s %@",__func__,receiverStr);
}

2.基于第三方开源库CocoaAsyncSocket

2.1客户端通过地址和端口号与服务端建立Socket连接,并写入相关数据。

- (void)connectToServerWithCommand:(NSString *)command
{
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
[_socket setUserData:command];

NSError *error = nil;
[_socket connectToHost:WIFI_DIRECT_HOST onPort:WIFI_DIRECT_PORT error:&error];
if (error) {
NSLog(@"__connect error:%@",error.userInfo);
}

[_socket writeData:[command dataUsingEncoding:NSUTF8StringEncoding] withTimeout:10.0f tag:6];
}

2.2 实现CocoaAsyncSocket的代理方法

#pragma mark - Socket Delegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"Socket连接成功:%s",__func__);
}

-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
if (err) {
NSLog(@"连接失败");
}else{
NSLog(@"正常断开");
}

if ([sock.userData isEqualToString:[NSString stringWithFormat:@"%d",SOCKET_CONNECT_SERVER]])
{
//服务器掉线 重新连接
[self connectToServerWithCommand:@"battery"];
}else
{
return;
}
}

-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"数据发送成功:%s",__func__);
//发送完数据手动读取,-1不设置超时
[sock readDataWithTimeout:-1 tag:tag];
}

-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"读取数据:%s %@",__func__,receiverStr);
}

摘自:https://www.jianshu.com/p/8e599ca5dfe8

收起阅读 »

iOS 接入WebSocket

WebSocket是什么WebSocket协议是 基于TCP 的一种网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket基本原理帧协议:0 ...
继续阅读 »

WebSocket是什么

WebSocket协议是 基于TCP 的一种网络协议。
它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。


WebSocket基本原理

帧协议:

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+

  • fin(1 bit):指示该帧是否构成该消息的最终帧。大多数情况下,消息适合于一个单一的帧,这一点总是默认设置的。实验表明,Firefox 在 32K 之后创建了第二个帧。
  • rsv1,rsv2,rsv3(1 bit each):必须为0,除非扩展里协商定义了非零值的含义。如果收到一个非零值,并且协商的扩展中没有一个定义这个非零值的含义,那么接收端必须抛出失败连接。

  • opcode(4bits):展示了帧表示什么。以下值目前正在使用:
    0x00:这个帧继续前面的有效载荷。
    0x01:此帧包含文本数据。
    0x02:这个帧包含二进制数据。
    0x08:这个帧终止连接。
    0x09:这个帧是一个 ping 。
    0x0a:这个帧是一个 pong 。
    (正如你所看到的,有足够的值未被使用,它们已被保留供将来使用)。
  • payload:最可能被掩盖的实际数据。它的长度是 payload_len 的长度。
  • masking-key(32 bits):从客户端发送到服务器的所有帧都被帧中包含的 32 位值掩盖。
  • 0-125 表示有效载荷的长度。 126 表示以下两个字节表示长度,127 表示接下来的 8 个字节表示长度。所以有效负载的长度在 〜7bit,16bit 和 64bit 括号内。
  • payload_len(7 bits):有效载荷的长度。 WebSocket 的帧有以下长度括号:
  • mask(1 bit):指示连接是否掩盖。就目前而言,从客户端到服务器的每条消息都必须掩盖,如果规范没有掩盖,规范就会终止连接。

其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

WebSocket常见的使用场景

要求服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。(也可以采用HTTP/2)

iOS端实现WebSocket连接的参考方案

SocketRocket

SocketRocket是facebook封装的websocket开源库,采用纯Objective-C编写。
使用者需要自己实现心跳机制,以及适配断网重连等情况。

SocketIO

SocketIO将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口,也就是说,我们在使用SocketIO时,不用担心兼容问题,底层会自动选用最佳的通信方式。因此说,WebSocket是SocketIO的一个子集。

另外,如果后端采用的是原生WebSocket,不建议大家使用SocketIO。
因为SocketIO定制了专有的协议,并不是纯粹的WebSocket,可能会遭遇适配问题。
不过,SocketIO的API极其易用!!!

Starscream

采用Swift编写,不过笔者暂时还没有用过,不发表任何评论。

iOS端利用SocketRocket实现WebSocket连接

示例代码如下,欢迎指正:

import Foundation
import SocketRocket
import Alamofire

/// Websocket连接中的通知
let FSWebSocketConnectingNotification = NSNotification.Name.init("FSWebSocketConnectingNotification")
/// Websocket连接成功的通知
let FSWebSocketDidOpenNotification = NSNotification.Name.init("FSWebSocketDidOpenNotification")
/// Websocket连接收到新消息的通知
let FSWebSocketDidReceiveMessageNotification = NSNotification.Name.init("FSWebSocketDidReceiveMessageNotification")
/// Websocket连接失败的通知
let FSWebSocketFailWithErrorNotification = NSNotification.Name.init("FSWebSocketFailWithErrorNotification")
/// Websocket连接已关闭的通知
let FSWebSocketDidCloseNotification = NSNotification.Name.init("FSWebSocketDidCloseNotification")

/// websocket连接地址,请输入有效的websocket地址!
let WSAddr = "ws://host:port/ws"

/// 心跳包发送间隔,3分钟
let PingDuration = 180

/// Websocket连接对象
class FSWebsocket: NSObject,SRWebSocketDelegate {

static let `default` = FSWebsocket.init()
/// 是否在断开连接后自动重连
var autoReconnect = true

private var reachabilityManager = NetworkReachabilityManager.init(host: "www.baidu.com")
/// websocket连接地址
private var addr = WSAddr
/// websocket连接
private var ws:SRWebSocket?
/// 心跳包定时发送计时器
private var heartbeatTimer:Timer?
/// 重连计数器
private var reconnectCount = 0

override init() {
super.init()

NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)

// 切换网络自动重连
reachabilityManager?.listener = { [weak self] (networkReachabilityStatus) in
// 正在连接中或已经连接成功,不需要重连
if self?.ws?.readyState == .CONNECTING || self?.ws?.readyState == .OPEN{
return
}
// 设置自动重连才会进行自动重连
if let s = self, s.autoReconnect{
self?.reconnect()
}
}
reachabilityManager?.startListening()
}
deinit {
NotificationCenter.default.removeObserver(self)
}

@objc func appDidEnterBackground(){

close()
}
@objc func appWillEnterForeground(){

reconnect()
}

// MARK: - Send Message

@discardableResult
func sendJSON(_ json:[AnyHashable : Any]) -> NSError?{
let jsonData = try? JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
return self.sendData(jsonData)
}

@discardableResult
func sendData(_ data:Data?) -> NSError?{

guard ws?.readyState == SRReadyState.OPEN else{
return NSError.init(domain: "FSWebsocket", code: SRStatusCodeGoingAway.rawValue, userInfo: nil)
}
ws?.send(data)
return nil
}

// MARK: - Connection Management

/// 连接websocket服务器
func open(){

let url = URL(string: self.addr)
assert(url != nil)

self.ws = SRWebSocket.init(url: url!)
ws?.delegate = self
ws?.open()
NotificationCenter.default.post(name: FSWebSocketConnectingNotification, object: self)
}

/// 重新连接
private func reconnect(){

if !autoReconnect{
return
}
if reconnectCount > 64 {
return
}

let seconds = DispatchTime.now().uptimeNanoseconds + UInt64(reconnectCount) * NSEC_PER_SEC
let time = DispatchTime.init(uptimeNanoseconds: seconds)
DispatchQueue.main.asyncAfter(deadline: time) {
self.ws?.close()
self.ws = nil

self.open()
}

if reconnectCount == 0 {
reconnectCount = 1
}
reconnectCount *= 2
}

/// 关闭连接
func close(){

self.destroyHeartbeat()

self.ws?.close()
self.ws = nil

resetConnectCount()
}

/// 发送心跳包
@objc func heartbeat(){

guard ws?.readyState == SRReadyState.OPEN else{
return
}
self.ws?.sendPing(nil)
}
/// 启动心跳
private func startHeartbeat(){

let timer = Timer.init(timeInterval: TimeInterval(PingDuration), target: self, selector: #selector(heartbeat), userInfo: nil, repeats: true)
self.heartbeatTimer = timer
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
}
/// 停止心跳
private func destroyHeartbeat(){

self.heartbeatTimer?.invalidate()
self.heartbeatTimer = nil
}
/// 重置重连尝试次数
private func resetConnectCount(){

reconnectCount = 0
}


// MARK: - SRWebSocketDelegate

func webSocketDidOpen(_ webSocket: SRWebSocket!) {

self.resetConnectCount()
self.startHeartbeat()
NotificationCenter.default.post(name: FSWebSocketDidOpenNotification, object: self)
}

func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {

guard let msgString = message as? NSString else{
return
}
guard let msgData = msgString.data(using: String.Encoding.utf8.rawValue) else{
return
}
// 如果传输的是JSON数据,可以使用JSONSerialization将JSON字符串转换为字典
guard let msgJSON = try? JSONSerialization.jsonObject(with: msgData, options: JSONSerialization.ReadingOptions.allowFragments) else{
return
}
NotificationCenter.default.post(name: FSWebSocketDidReceiveMessageNotification, object: self, userInfo: ["message":msgJSON])
}

func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) {

NotificationCenter.default.post(name: FSWebSocketFailWithErrorNotification, object: self, userInfo: ["error": error])
// 连接失败:
if (error as NSError).code == 50{
// 断网,不重连
return
}
// 当前页面不需要使用websocket,不重连
// 重连次数超过限制,不重连
reconnect()
}

func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) {
// 心跳包响应回调
}

func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {

// 连接被关闭
NotificationCenter.default.post(name: FSWebSocketDidCloseNotification, object: self, userInfo: ["code": code, "reason": reason, "wasClean": wasClean])

close()
}
}



摘自链接:https://www.jianshu.com/p/934c0d79f75e

收起阅读 »