注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

常用开发加密方法

前言相信大家在开发中都遇到过,有些隐秘信息需要做加密传输的场景.A:你就把 XXX 做一下base64加密传过来就行这些问题相信大家都遇到过,那么在实际开发中我们应该如何选择加密方法呢?加密这里我就直接抛出来几个加密规则AES 对称加密,双方只有同一个秘钥ke...
继续阅读 »

前言
相信大家在开发中都遇到过,有些隐秘信息需要做加密传输的场景.
A:你就把 XXX 做一下base64加密传过来就行

这些问题相信大家都遇到过,那么在实际开发中我们应该如何选择加密方法呢?


加密
这里我就直接抛出来几个加密规则

  • AES 对称加密,双方只有同一个秘钥key

  • RSA 非对称加密,生成一对公私钥.

首先要明确一点, 即使做了加密也不能保证我们的信息就是绝对安全的,只是尽可能的提升破解难度,加密算法的实现都是公开的,所以秘钥如何安全的存储是我们要重点考虑的问题.

关于这两种加密算法大家可以网上查一下原理,这里我不介绍原理,只介绍给大家特定场景下如何选择最优的加密规则,以及一些小Tips.

AES
对称加密,很好理解,生成唯一秘钥key,双方本别可以用key做加密/解密.是比较常用的加密首段,AES只是一种加密规则,具体的加密还有很多种,目前主流使用的是AES/GCM.

RSA
非对称加密,生成一对秘钥,public key/private key,
加解密使用时: public key加密, private key解密.
签名验证时 : private key签名 , public key 验签

这里说一下实际案例:

某某公司,2B的后台支付接口,突然有一天一个商家反馈为什么我账户里钱都没有了,通过日志一查发现都是正常操作刷走了.而某公司并没有办法证明自己的系统是没问题的.理论上这个接口的key下发给商户,但是某某公司也是有这个key的,所以到底是谁泄漏了key又是谁刷走了账户里的钱,谁也无法证明.

这里我们要想一个问题,我们要怎么做才能防止出现此类问题后,商户过来说不是我刷的钱,寻求赔偿的时候, 拿出证据打发他们?

这个问题就可以利用RSA来解决,在接入公司生成APP key 要求接入方自己生成一对RSA秘钥,然后讲 public key上传给我们, private key由接入方自己保存, 而我们只需要验证订单中的签名是否是由private key签名的,而非其他阿猫阿狗签名的订单. 如果出现了上诉问题,那么说明接入方的private key泄漏与我们无关,这样我们就能防止接入方抵赖.

完整性校验.防串改

很多情况下我们需要对数据的完整性做校验, 比如对方发过来一个文件, 我们怎么知道这个问题件就是源文件, 而非被别人恶意拦截串改后的问题?

早些年大家下载程序的时候应该会看到,当前文件的md5值是XXXXX,这个就是为了防止文件被修改的存在的.早期我们都是用md5/sha1来做完整性校验,后来由sha1升级出现了sha256.大家可能不知道应该如何选择.

下面是一个经典故事
Google之前公开过两个不同的PDF,而它们拥有相同的sha1值


两个不同的文件拥有相同sha1值,这意味着我们本地使用的程序sha1是源文件非串改后的,但实际上可能早已偷梁换柱.这是很可怕的.
所以推荐大家在用完整性校验时要使用sha256,会更安全些.

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

收起阅读 »

iOS 13:更多系统APP和组件采用Swift编写

苹果在 2014 年 WWDC 发布了全新 Swift 编程语言,Swift 是苹果平台未来的编程语言。自那以后,很多第三方开发者开始使用 Swift 编写程序,不过苹果 iOS 和 macOS 系统,以及各种系统应用还是采用 Objective-C 编写。这...
继续阅读 »

苹果在 2014 年 WWDC 发布了全新 Swift 编程语言,Swift 是苹果平台未来的编程语言。自那以后,很多第三方开发者开始使用 Swift 编写程序,不过苹果 iOS 和 macOS 系统,以及各种系统应用还是采用 Objective-C 编写。

这种情况存在很多原因,首先,苹果目前大量的 Objective-C 代码工作的很完美,没有必要为了重写而重写,没有问题就不要创造新的问题。其次,直到 Swift 5.0,ABI 才稳定,Swift 5.1,模块稳定,对于在系统级别大规模部署很重要。

自 iOS 9 之后,开发者 Alexandre Colucci 一直在统计苹果系统中 Swift 的使用情况。最新的数据显示,在 iOS 13 中,一共有 141 个使用 Swift 编写的二进制可执行文件,是 iOS 12 的两倍多,iOS 12 中有 66 个。


iOS 13 中,Sidecar 副屏、查找和提醒事项等新功能、新应用都采用 Swift 编写,其他使用 Swift 的 app 包括健康、Books 电子书以及一些系统服务,负责 AirPods 和 HomePod 配对的服务,以及查找 App 的离线查找功能等。

转自:https://www.jianshu.com/p/1227b27fcb2c

收起阅读 »

CoreSimulator与Xcode两个文件夹造成Mac中多了100G的“其他”空间

tips 没有购买cleanMyMac的同学,不要担心,我既然写了文章,肯定是不为了让同学们花钱购买软件的。CoreSimulator与Xcode两个文件夹造成Mac中多了100G的“其他”空间;请原谅我表述的不太明白,还是上图吧:1.清理之前mac电脑只剩下...
继续阅读 »

tips 没有购买cleanMyMac的同学,不要担心,我既然写了文章,肯定是不为了让同学们花钱购买软件的。
CoreSimulator与Xcode两个文件夹造成Mac中多了100G的“其他”空间;
请原谅我表述的不太明白,还是上图吧:

1.清理之前mac电脑只剩下了23.4GB的存储空间可用,“其他”这一项目占了200多GB


2.清理了Xcode文件夹中的部分文件


3.清理了CoreSimulator文件夹中的部分文件


使用Clean My Mac 版本4.0.4 中的卸载功能,查看(下边的是我清理之后,清理之前,Xcode占用129GB,现在是37.7GB)


我们通过下图可以得知,Xcode.app本身才7.8个G,可是下边的两个文件夹(清理之前占了120G左右)清理之后还占了30GB左右。


没有购买cleanMyMac的同学,不要担心,我既然写了文章,肯定是不为了让同学们花钱购买软件的。

1.请在电脑上 点击 “前往文件夹”功能,(快捷键:com + shift + g)
Xcode 文件目录:(不要全部删除整个文件,我们去选择删除DeviceSupport里边老旧的版本就好了)

~/Library/Developer/Xcode/iOS\ DeviceSupport

CoreSimulator 文件目录:(不要全部删除整个文件,我们去选择删除Devices里的一些文件就好了)

~/Library/Developer/CoreSimulator/Devices
当然也可以在终端输入  
open ~/Library/Developer/Xcode/iOS\ DeviceSupport

还有

open ~/Library/Developer/CoreSimulator/Devices

也是一样的;

我把12.4以下的都删除了。


想了解这些文件是什么的,可以参照这篇文章 iOS开发-Xcode清理系统内存占用过多的方法



↓ ↓ ↓ ↓ ↓2020年01月08日添加 ↓ ↓ ↓ ↓

感谢评论区的建议。按照建议,亲测后感觉留言里提到ncdu方便快捷,有可取之处。如果有喜欢了解的小伙伴,可以参考这篇文章一个查看MAC硬盘占用的小工具ncdu



转自:https://www.jianshu.com/p/48d8e6870a7c

收起阅读 »

iOS websocket接入

接触WebSocket最近公司的项目中有一个功能 需要服务器主动推数据到APP。考虑到普通的HTTP 通信方式只能由客户端主动拉取,服务器不能主动推给客户端 。然后就想出的2种解决方案。1.和后台沟通了一下 他们那里使用的是WebSocket ,所以就使用We...
继续阅读 »

接触WebSocket

最近公司的项目中有一个功能 需要服务器主动推数据到APP。
考虑到普通的HTTP 通信方式只能由客户端主动拉取,服务器不能主动推给客户端 。然后就想出的2种解决方案。

1.和后台沟通了一下 他们那里使用的是WebSocket ,所以就使用WebSocket让我们app端和服务器建立长连接。这样就可以事实接受他发过来的消息
2.使用推送,也可以实现接收后台发过来的一些消息

最后还是选择了WebSocket,找到了facebook的 SocketRocket 框架。下面是接入过程中的一些记录

WebSocket

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:

WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;

WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。

具体在这儿 WebSocket 是什么原理为什么可以实现持久连接?

用法
我使用的是pod管理库 所以在podfile中加入
pod 'SocketRocket'

在使用命令行工具cd到当前工程 安装
pod install

如果是copy的工程中的 SocketRocket库的github地址:SocketRocket

导入库到工程中以后首先封装一个SocketRocketUtility单例

SocketRocketUtility.m文件中的写法如下:

#import "SocketRocketUtility.h"
#import <SocketRocket.h>

NSString * const kNeedPayOrderNote = @"kNeedPayOrderNote";//发送的通知名称

@interface SocketRocketUtility()<SRWebSocketDelegate>
{
int _index;
NSTimer * heartBeat;
NSTimeInterval reConnectTime;
}

@property (nonatomic,strong) SRWebSocket *socket;

@end

@implementation SocketRocketUtility

+ (SocketRocketUtility *)instance {
static SocketRocketUtility *Instance = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
Instance = [[SocketRocketUtility alloc] init];
});
return Instance;
}

//开启连接
-(void)SRWebSocketOpenWithURLString:(NSString *)urlString {
if (self.socket) {
return;
}

if (!urlString) {
return;
}

//SRWebSocketUrlString 就是websocket的地址 写入自己后台的地址
self.socket = [[SRWebSocket alloc] initWithURLRequest:
[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];

self.socket.delegate = self; //SRWebSocketDelegate 协议

[self.socket open]; //开始连接
}

//关闭连接
- (void)SRWebSocketClose {
if (self.socket){
[self.socket close];
self.socket = nil;
//断开连接时销毁心跳
[self destoryHeartBeat];
}
}

#pragma mark - socket delegate
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(@"连接成功,可以与服务器交流了,同时需要开启心跳");
//每次正常连接的时候清零重连时间
reConnectTime = 0;
//开启心跳 心跳是发送pong的消息 我这里根据后台的要求发送data给后台
[self initHeartBeat];
[[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketDidOpenNote object:nil];
}

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
NSLog(@"连接失败,这里可以实现掉线自动重连,要注意以下几点");
NSLog(@"1.判断当前网络环境,如果断网了就不要连了,等待网络到来,在发起重连");
NSLog(@"2.判断调用层是否需要连接,例如用户都没在聊天界面,连接上去浪费流量");
NSLog(@"3.连接次数限制,如果连接失败了,重试10次左右就可以了,不然就死循环了。)";
_socket = nil;
//连接失败就重连
[self reConnect];
}

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
NSLog(@"被关闭连接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);
//断开连接 同时销毁心跳
[self SRWebSocketClose];
}

/*
该函数是接收服务器发送的pong消息,其中最后一个是接受pong消息的,
在这里就要提一下心跳包,一般情况下建立长连接都会建立一个心跳包,
用于每隔一段时间通知一次服务端,客户端还是在线,这个心跳包其实就是一个ping消息,
我的理解就是建立一个定时器,每隔十秒或者十五秒向服务端发送一个ping消息,这个消息可是是空的
*/
-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload{

NSString *reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
NSLog(@"reply===%@",reply);
}

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
//收到服务器发过来的数据 这里的数据可以和后台约定一个格式 我约定的就是一个字符串 收到以后发送通知到外层 根据类型 实现不同的操作
NSLog(@"%@",message);

[[NSNotificationCenter defaultCenter] postNotificationName:kNeedPayOrderNote object:message];
}

#pragma mark - methods
//重连机制
- (void)reConnect
{
[self SRWebSocketClose];
//超过一分钟就不再重连 所以只会重连5次 2^5 = 64
if (reConnectTime > 64) {
return;
}

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.socket = nil;
[self SRWebSocketOpen];
NSLog(@"重连");
});

//重连时间2的指数级增长
if (reConnectTime == 0) {
reConnectTime = 2;
}else{
reConnectTime *= 2;
}
}

//初始化心跳
- (void)initHeartBeat
{
dispatch_main_async_safe(^{
[self destoryHeartBeat];
__weak typeof(self) weakSelf = self;
//心跳设置为3分钟,NAT超时一般为5分钟
heartBeat = [NSTimer scheduledTimerWithTimeInterval:3*60 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"heart");
//和服务端约定好发送什么作为心跳标识,尽可能的减小心跳包大小
[weakSelf sendData:@"heart"];
}];
[[NSRunLoop currentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];
})
}

//取消心跳
- (void)destoryHeartBeat
{
dispatch_main_async_safe(^{
if (heartBeat) {
[heartBeat invalidate];
heartBeat = nil;
}
})
}

//pingPong机制
- (void)ping{
[self.socket sendPing:nil];
}

#define WeakSelf(ws) __weak __typeof(&*self)weakSelf = self
- (void)sendData:(id)data {

WeakSelf(ws);
dispatch_queue_t queue = dispatch_queue_create("zy", NULL);

dispatch_async(queue, ^{
if (weakSelf.socket != nil) {
// 只有 SR_OPEN 开启状态才能调 send 方法,不然要崩
if (weakSelf.socket.readyState == SR_OPEN) {
[weakSelf.socket send:data]; // 发送数据

} else if (weakSelf.socket.readyState == SR_CONNECTING) {
NSLog(@"正在连接中,重连后其他方法会去自动同步数据");
// 每隔2秒检测一次 socket.readyState 状态,检测 10 次左右
// 只要有一次状态是 SR_OPEN 的就调用 [ws.socket send:data] 发送数据
// 如果 10 次都还是没连上的,那这个发送请求就丢失了,这种情况是服务器的问题了,小概率的
[self reConnect];

} else if (weakSelf.socket.readyState == SR_CLOSING || weakSelf.socket.readyState == SR_CLOSED) {
// websocket 断开了,调用 reConnect 方法重连
[self reConnect];
}
} else {
NSLog(@"没网络,发送失败,一旦断网 socket 会被我设置 nil 的");
}
});
}

-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

然后在需要开启socket的地方调用
[[SocketRocketUtility instance] SRWebSocketOpenWithURLString:@"写入自己后台的地址"];
在需要断开连接的时候调用
[[SocketRocketUtility instance] SRWebSocketClose];

使用这个框架最后一个很重要的 需要注意的一点

这个框架给我们封装的webscoket在调用它的sendPing senddata方法之前,一定要判断当前scoket是否连接,如果不是连接状态,程序则会crash。

结语
这里简单的实现了连接和收发数据 后续看项目需求在加上后续的改进 希望能够帮助第一次写的iOSer 。 希望有更好的方法的童鞋可以有进一步的交流 : )

4月10日 更新:

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

上面发送心跳包的方法是iOS10才可以用的 其他版本会崩溃 要适配版本 要选择 这个方法

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

8月10日 更新demo地址
demo地址
可以下载下来看看哦 :)


demo中的后台地址未设置 所以很多同学直接运行就报错了 设置一个自己后台的地址就ok了 :)

转自:https://www.jianshu.com/p/821b777555d3

收起阅读 »

iOS 用symbolicatecrash符号化崩溃日志中系统库方法堆栈

说明现在已经有很多第三方平台支持解析crash日志中的系统方法了,比如bugly。但是万一遇到情况特殊或者公司要求,还是走上传崩溃日志到自己的服务器,然后自己去定期解析的话,就需要用到symbolicatecrash这个工具了。指令操作均在终端中进行。另外,每...
继续阅读 »

说明
现在已经有很多第三方平台支持解析crash日志中的系统方法了,比如bugly。但是万一遇到情况特殊或者公司要求,还是走上传崩溃日志到自己的服务器,然后自己去定期解析的话,就需要用到symbolicatecrash这个工具了。

指令操作均在终端中进行。
另外,每次打包上架提交审核的时候,把对应的.xcarchive与ipa文件一同拷贝一份,按照版本号保存下来是个好习惯。

1.前期准备工作
前期准备工作只需要在第一次尝试解析的时候进行,如果可以成功执行最终的命令行解析日志就不需要重复执行。

  • 确定Xcode路径,执行如下指令

xcode-select --print-path

目的:确保Xcode路径存在。如果路径中有空格的存在,请把空格去掉。比如如果Xcode 的名字是“Xcode 9.2”请修改成“Xcode9.2”或者“Xcode”。否则后面你会遇到很多稀奇古怪的错误。
修改方法:应用程序→Xcode→重命名

  • 添加Xcode路径
    如果Xcode路径已经存在,或者不需要修改,请跳过这一步。注意如果改过Xcode应用的名字也需要进行这一步操作
    执行如下指令

sudo xcode-select -s 路径

路径部分直接把Xcode应用内Developer文件夹拖拽进去会自动生成。
Developer文件夹:应用程序→Xcode→右键,显示包内容→Contents文件夹→Developer

  • 确定Xcode command line tools是否安装
    执行如下指令

xcode-select --install

如果输出以下内容说明已经安装,否则根据提示安装即可。

xcode-select: error: command line tools are already installed, use "Software Update" to install updates

2.解析准备工作
解析所需文件

解析崩溃日志需要三个文件

①.崩溃日志文件(通常为.crash如果服务器上面是.txt也没关系,直接下下来把尾缀改成.crash就行)
②.产生崩溃日志的app包对应的.dSYM符号表(注意符号表和包一定要匹配。否则,堆栈方法会错乱)
③.崩溃分析工具symbolicatecrash(Xcode自带)

.dSYM符号表的获取:Xcode→window→organizer 选择Archives→选择想要解析崩溃日志的App包→右键,show in finder→右键(.xcarchive),显示包内容→dSYMs→xxx.app.dSYM
如果自己这里没有app打包文件就只有跟打包的同事要。

symbolicatecrash的获取:应用程序(Applications)→Xcode→右键,显示包内容→Contents→SharedFrameworks→DVTFoundation.framework→Versions→A→Resources→symbolicatecrash

tips:如果到了DVTFoundation.framework这里打不开下一步了,选择如下浏览方式即可。


3.解析日志

<1>将上述三个文件放在一个文件夹内
文件夹名称可以任意起,路径随意但最好不要出现中文。


<2>在终端中进入该文件夹内
直接拖拽文件夹到路径部分会自动生成

cd 路径

<3>解析日志

./symbolicatecrash ./*.crash ./*.app.dSYM>symbol.crash

这个方法一次只能解析一个日志文件,然后输出一个解析过后的symbol.crash日志文件(会覆盖之前存在的symbol.crash),这个输出的日志文件就是我们可以直接阅读的日志文件。symbol部分可以任意修改成其他名字。

如果要解析多个日志文件,需要逐一将文件夹内的日志文件替换。或者将所有需要解析的日志文件全部放在文件夹内,但是每次指定需要解析的.crash文件。

如果出现下面类似的错误,报错无法执行

Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line xx(数字).

执行指令

export DEVELOPER_DIR=Xcode Developer文件夹路径

像上面一样把Developer文件夹拖拽到等号后面路径部分就行,然后再执行解析指令就不会报错了。

<4>查看解析结果


<5>给Xcode添加对应固件的符号文件
①.下载对应固件符号文件
这个需要结合崩溃日志的信息来,比如这里日志中提到崩溃发生的固件是8.3(12F70)我们就要去找这个固件的符号文件,找的时候还要注意是否区分了CPU架构。下载地址放在后面

②.下载完成后添加进Xcode
打开Finder:点击菜单前往→前往文件夹→输入
~/Library/Developer/Xcode/iOS DeviceSupport→前往

将下载好的符号文件放入定位到的路径里面。


③.再次解析日志文件


<6>固件符号文件下载地址
首先感谢iOS Crash分析必备:符号化系统库方法作者的无私分享。该文章的作者收集了几乎所有固件的符号文件并分享了出来,为了尊重原作者这里就不放下载地址了。大家可以在他的文章当中找到下载地址,以及目前收集了哪些固件符号文件。

转自:https://www.jianshu.com/p/21532aef2811

收起阅读 »

关于WKWebView的post请求丢失body问题的解决方案

WKWebView的优点这里不做过多介绍,主要说一下最近解决WKWebView的post请求丢失body问题的解决方案。WKWebView 通过loadrequest方法加载Post请求会丢失请求体(body)中的内容,进而导致服务器拿不到body中的内容的问...
继续阅读 »

WKWebView的优点这里不做过多介绍,主要说一下最近解决WKWebView的post请求丢失body问题的解决方案。
WKWebView 通过loadrequest方法加载Post请求会丢失请求体(body)中的内容,进而导致服务器拿不到body中的内容的问题的发生。这个问题的产生主要是因为WKWebView的网络请求的进程与APP不是同一个进程,所以网络请求的过程是这样的:
由APP所在的进程发起request,然后通过IPC通信(进程间通信)将请求的相关信息(请求头、请求行、请求体等)传递给webkit网络线进程接收包装,进行数据的HTTP请求,最终再进行IPC的通信回传给APP所在的进程的。这里如果发起的request请求是post请求的话,由于要进行IPC数据传递,传递的请求体body中根据系统调度,将其舍弃,最终在WKWebView网络进程接受的时候请求体body中的内容变成了空,导致此种情况下的服务器获取不到请求体,导致问题的产生。
为了能够获取POST方法请求之后的body内容,这两天整理了一些解决方案,大致分为三种:

  1. 将网络请求交由Js发起,绕开系统WKWebView的网络的进程请求达到正常请求的目的

  2. 改变POST请求的方法为GET方法(有风险,不一定服务器会接受GET方法)

  3. 将Post请求的请求body内容放入请求的Header中,并通过URLProtocol拦截自定义协议,在拦截中通过NSConnection进行重新请求(重新包装请求body),然后通过回调Client客户端来传递数据内容

三种方法中,我采用了第三种方案,这里说一下第三种方案的实现方式,大致分为三步:

  1. 注册拦截的自定义的scheme

  2. 重写loadRequest()方法,根据request的method方法是否为POST进行URL的拦截替换

  3. 在URLProtocol中进行request的重新包装(获取请求的body内容),使用NSURLConnection进行HTTP请求并将数据回传

这里说明一下为什么要自己去注册自定义的scheme,而不是直接拦截https/http。主要原因是:如果注册了https/http的拦截,那么所有的http(s)请求都会交由系统进程处理,那么此时系统进程会通过IPC的形式传递给实现URLProctol协议的类去处理,在通过IPC传递的过程中丢失body体(上面有讲到),所以在拦截的时候是拿不到POST方法的请求体body的。然而并不是所有的http请求都会走loadrequest()方法(比如js中的ajax请求),所以导致一些POST请求没有被包装(将请求体body内容放到请求头header)就被拦截了,进而丢失请求体body内容,问题一样会产生。所以为了避免这样的问题,我们需要自己去定一个scheme协议,保证不过度拦截并且能够处理我们需要处理的POST请求内容。

以下是具体的实现方式:

  • 注册拦截的自定义的scheme

[NSURLProtocol registerClass:NSClassFromString(@“GCURLProtocol")];
[NSURLProtocol wk_registerScheme:@"gc"];
[NSURLProtocol wk_registerScheme:WkCustomHttp];
[NSURLProtocol wk_registerScheme:WkCustomHttps];
  • 重写loadRequest()方法,根据request的method方法是否为POST进行URL的拦截替换

//包装请求头内容
- (WKNavigation *)loadRequest:(NSURLRequest *)request{
NSLog(@"发起请求:%@ method:%@",request.URL.absoluteString,request.HTTPMethod);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
NSMutableDictionary *requestHeaders = [request.allHTTPHeaderFields mutableCopy];
//判断是否是POST请求,POST请求需要包装request中的body内容到请求头中(会有丢失body问题的产生)
//,包装完成之后重定向到拦截的协议中自己包装处理请求数据内容,拦截协议是GCURLProtocol,请自行搜索
if ([mutableRequest.HTTPMethod isEqualToString:@"POST"] && ([mutableRequest.URL.scheme isEqualToString:@"http"] || [mutableRequest.URL.scheme isEqualToString:@"https"])) {
NSString *absoluteStr = mutableRequest.URL.absoluteString;
if ([[absoluteStr substringWithRange:NSMakeRange(absoluteStr.length-1, 1)] isEqualToString:@"/"]) {
absoluteStr = [absoluteStr stringByReplacingCharactersInRange:NSMakeRange(absoluteStr.length-1, 1) withString:@""];
}

if ([mutableRequest.URL.scheme isEqualToString:@"https"]) {
absoluteStr = [absoluteStr stringByReplacingOccurrencesOfString:@"https" withString:WkCustomHttps];
}else{
absoluteStr = [absoluteStr stringByReplacingOccurrencesOfString:@"http" withString:WkCustomHttp];
}

mutableRequest.URL = [NSURL URLWithString:absoluteStr];
NSString *bodyDataStr = [[NSString alloc]initWithData:mutableRequest.HTTPBody encoding:NSUTF8StringEncoding];
[requestHeaders addEntriesFromDictionary:@{@"httpbody":bodyDataStr}];
mutableRequest.allHTTPHeaderFields = requestHeaders;

NSLog(@"当前请求为POST请求Header:%@",mutableRequest.allHTTPHeaderFields);

}
return [super loadRequest:mutableRequest];
}
  • 在URLProtocol中进行request的重新包装(获取请求的body内容),使用NSURLConnection进行HTTP请求并将数据回传(以下是主要代码)

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {

NSString *scheme = request.URL.scheme;

if ([scheme isEqualToString:InterceptionSchemeKey]){

if ([self propertyForKey:HaveDealRequest inRequest:request]) {
NSLog(@"已经处理,放行");
return NO;
}
return YES;
}

if ([scheme isEqualToString:WkCustomHttp]){

if ([self propertyForKey:HaveDealWkHttpPostBody inRequest:request]) {
NSLog(@"已经处理,放行");
return NO;
}
return YES;
}

if ([scheme isEqualToString:WkCustomHttps]){

if ([self propertyForKey:HaveDealWkHttpsPostBody inRequest:request]) {
NSLog(@"已经处理,放行");
return NO;
}
return YES;
}

return NO;

}
- (void)startLoading {

//截获 gc 链接的所有请求,替换成本地资源或者线上资源
if ([self.request.URL.scheme isEqualToString:InterceptionSchemeKey]) {
[self htmlCacheRequstLoad];
}

else if ([self.request.URL.scheme isEqualToString:WkCustomHttp] || [self.request.URL.scheme isEqualToString:WkCustomHttps]){
[self postBodyAddLoad];
}
else{
NSMutableURLRequest *newRequest = [self cloneRequest:self.request];
NSString *urlString = newRequest.URL.absoluteString;
[self addHttpPostBody:newRequest];
[NSURLProtocol setProperty:@YES forKey:GCProtocolKey inRequest:newRequest];
[self sendRequest:newRequest];
}


}

- (void)addHttpPostBody:(NSMutableURLRequest *)redirectRequest{

//判断当前的请求是否是Post请求
if ([self.request.HTTPMethod isEqualToString:@"POST"]) {
NSLog(@"post请求");
NSMutableDictionary *headerDict = [redirectRequest.allHTTPHeaderFields mutableCopy];
NSString *body = headerDict[@"httpbody"]?:@"";
if (body.length) {
redirectRequest.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
NSLog(@"body:%@",body);
}
}
}
- (void)postBodyAddLoad{

NSMutableURLRequest *cloneRequest = [self cloneRequest:self.request];
if ([cloneRequest.URL.scheme isEqualToString:WkCustomHttps]) {
cloneRequest.URL = [NSURL URLWithString:[cloneRequest.URL.absoluteString stringByReplacingOccurrencesOfString:WkCustomHttps withString:@"https"]];
[NSURLProtocol setProperty:@YES forKey:HaveDealWkHttpsPostBody inRequest:cloneRequest];
}else if ([cloneRequest.URL.scheme isEqualToString:WkCustomHttp]){

cloneRequest.URL = [NSURL URLWithString:[cloneRequest.URL.absoluteString stringByReplacingOccurrencesOfString:WkCustomHttp withString:@"http"]];
[NSURLProtocol setProperty:@YES forKey:HaveDealWkHttpPostBody inRequest:cloneRequest];
}
//添加body内容
[self addHttpPostBody:cloneRequest];
NSLog(@"请求body添加完成:%@",[[NSString alloc]initWithData:cloneRequest.HTTPBody encoding:NSUTF8StringEncoding]);
[self sendRequest:cloneRequest];

}
//复制Request对象
- (NSMutableURLRequest *)cloneRequest:(NSURLRequest *)request
{
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:request.URL cachePolicy:request.cachePolicy timeoutInterval:request.timeoutInterval];

newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
[newRequest setValue:@"image/webp,image/*;q=0.8" forHTTPHeaderField:@"Accept"];

if (request.HTTPMethod) {
newRequest.HTTPMethod = request.HTTPMethod;
}

if (request.HTTPBodyStream) {
newRequest.HTTPBodyStream = request.HTTPBodyStream;
}

if (request.HTTPBody) {
newRequest.HTTPBody = request.HTTPBody;
}

newRequest.HTTPShouldUsePipelining = request.HTTPShouldUsePipelining;
newRequest.mainDocumentURL = request.mainDocumentURL;
newRequest.networkServiceType = request.networkServiceType;

return newRequest;
}

#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
/**
* 收到服务器响应
*/
NSURLResponse *returnResponse = response;
[self.client URLProtocol:self didReceiveResponse:returnResponse cacheStoragePolicy:NSURLCacheStorageAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
/**
* 接收数据
*/
if (!self.recData) {
self.recData = [NSMutableData new];
}
if (data) {
[self.recData appendData:data];
}
}
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response
{
/**
* 重定向
*/
if (response) {
[self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
/**
* 加载失败
*/
[self.client URLProtocol:self didFailWithError:error];
}

转自:https://www.jianshu.com/p/4dfc80ca7db2

收起阅读 »

iOS - 同一个workspace下创建多个项目编程

在iOS开发中,相关联的多个项目可能会放在同一个workspace下进行开发,那习惯了一个项目在一个工作空间下的同学该怎么快速开撸呢?只需要三步而已!第一步,先用Xcode在目标目录下创建一个workspace文件。见图说话。第二步,用Xcode打开works...
继续阅读 »

在iOS开发中,相关联的多个项目可能会放在同一个workspace下进行开发,那习惯了一个项目在一个工作空间下的同学该怎么快速开撸呢?

只需要三步而已!


第一步,先用Xcode在目标目录下创建一个workspace文件。见图说话。


第二步,用Xcode打开workspace文件,然后在该workspace下创建多个Project文件。


在创建工程的过程中有个主意点:将新建Project添加的目标和组 都是workspace。如图:


第三步,多个工程间文件互相引用问题:多个工程间的文件引用方法:在工程A的Setting选项下的Header Search Paths 下添加“$(SRCROOT)/../B”,这个工程A中即可引用工程B的文件,不过导入文件的方式是:#import <Person.m>


如上设置,多个工程间的类就可以共享使用了。



收起阅读 »

iOS- 安装CocoaPods详细过程

一、简介什么是CocoaPodsCocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。CocoaPods的好...
继续阅读 »

一、简介

  • 什么是CocoaPods

CocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。

  • CocoaPods的好处

1、在引入第三方库时它可以自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的-fno-objc-arc配置等。

2、使用CocoaPods可以很方便地查找新的第三方库,这些类库是比较“标准的”,而不是网上随便找到的,这样可以让我们找到真正好用的类库。

二、Cocoapods安装步骤

注意:在终端输入命令时,取$后面部分输入

1、升级Ruby环境

终端输入:$ gem update --system

此时会出现


这是因为你没有权限去升级Ruby

这时应该输入:$ sudo gem update --system


接下来输入密码,注意:输入密码的时候没有任何反应,光标也不会移动,你尽管输入就是了,输完了直接回车。
等一会如果出现

恭喜你,升级Ruby成功了。

2、更换Ruby镜像

首先移除现有的Ruby镜像

终端输入:$ gem sources --remove https://gems.ruby-china.org/

然后添加国内最新镜像源(淘宝的Ruby镜像已经不更新了)

终端输入:$ gem sources -a https://gems.ruby-china.com/

执行完毕之后输入gem sources -l来查看当前镜像

终端输入:$ gem sources -l

如果结果是

*** CURRENT SOURCES ***
https://gems.ruby-china.com/
说明添加成功,否则继续执行$ gem source -a https://gems.ruby-china.com/来添加

3、安装CocoaPods

接下来开始安装
终端输入:$ sudo gem install cocoapods


说明没有权限,需要输入

终端输入:$ sudo gem install -n /usr/local/bin cocoapods

安装成功如下:


到这之后再执行pod setup(PS:这个过程是漫长的,要有耐心)

终端输入:$ pod setup

然后你会看到出现了Setting up CocoaPods master repo,卡住不动了,说明Cocoapods在将它的信息下载到 ~/.cocoapods里。
你可以command+n新建一个终端窗口,执行cd ~/.cocoapods/进入到该文件夹下,然后执行du -sh *来查看文件大小,每隔几分钟查看一次,这个目录最终大小是900多M(我的是930M)
当出现Setup completed的时候说明已经完成了。

4、CocoaPods的使用

1、首先我们来搜索一下三方库
终端输入:$ pod search AFNetworking

这时有可能出现


这是因为之前pod search的时候生成了缓存文件search_index.json
执行rm ~/Library/Caches/CocoaPods/search_index.json来删除该文件
然后再次输入pod search AFNetworking进行搜索
这时会提示Creating search index for spec repo 'master'..
等待一会将会出现搜索结果如下:


出现这个了就说明搜索成功了,看一下上图中的这一句:
pod 'AFNetworking', '~> 3.1.0'
这句话一会我们要用到,这是CocoaPods添加三方库的关键字段
然后退出这个界面(这一步只是验证一下cocoapods有没有安装成功,能不能搜索到你想要的三方库),直接按"q"就退出去了。

2、在工程中创建一个Podfile文件

要想在你的工程中创建Podfile文件,必须先要进到该工程目录下

终端输入:$ cd /Users/liyang/Desktop/CocoaPodsTest
//这是我电脑上的路径,你输入你自己项目的路径或直接拖拽也行

进来之后就创建

终端输入:$ touch Podfile

然后你在你的工程目录下可以看到多了一个Podfile文件

3、编辑你想导入的第三方库的名称及版本

使用vim编辑Podfile文件

终端输入:$ vim Podfile

进入如下界面:


进来之后紧接着按键盘上的英文'i'
下面的"Podsfile" 0L, 0C将变成-- INSERT --
然后就可以编辑文字了,输入以下文字

platform :ios, '7.0'
target 'MyApp' do
pod 'AFNetworking', '~> 3.1.0'
end
解释一下
platform :ios, '7.0'代表当前AFNetworking支持的iOS最低版本是iOS 7.0,
'MyApp'就是你自己的工程名字,
pod 'AFNetworking', '~> 3.1.0'代表要下载的AFNetworking版本是3.1.0及以上版本,还可以去掉后面的'~> 3.1.0',直接写pod 'AFNetworking',这样代表下载的AFNetworking是最新版。
编辑完之后成如下样子:

此时该退出去了,怎么退出去呢?跟着我做,先按左上角的esc键,再按:键,再输入wq,点击回车,就保存并退出去了。

这时候,你会发现你的项目目录中名字为Podfile的文件的内容就是你刚刚输入的内容。

4、把该库下载到Xcode中

终端输入:$ pod install

这就开始下载了,需要一段时间,出现如下界面就说明安装好了


这个时候关闭所有的Xcode窗口,再次打开工程目录会看到多了一个后缀名为.xcworkspace文件。


以后打开工程就双击这个文件打开了,而不再是打开.xcodeproj文件。
进入工程后引入头文件不再是#import "AFNetworking.h",而是#import <AFNetworking.h>




原贴链接:https://www.jianshu.com/p/9e4e36ba8574
收起阅读 »

iOS- 集成Bugly详解

SDK 集成Bugly提供两种集成方式供iOS开发者选择:通过CocoaPods集成手动集成如果您是从Bugly 2.0以下版本升级过来的,请查看iOS SDK 升级指南Bugly iOS SDK 最低兼容系统版本 iOS 7.0通过CocoaPod...
继续阅读 »

SDK 集成

Bugly提供两种集成方式供iOS开发者选择:

  • 通过CocoaPods集成
  • 手动集成

如果您是从Bugly 2.0以下版本升级过来的,请查看iOS SDK 升级指南

Bugly iOS SDK 最低兼容系统版本 iOS 7.0

通过CocoaPods集成

在工程的Podfile里面添加以下代码:

pod 'Bugly'

保存并执行pod install,然后用后缀为.xcworkspace的文件打开工程。

注意:
命令行下执行pod search Bugly,如显示的Bugly版本不是最新的,则先执行pod repo update操作更新本地repo的内容
关于CocoaPods的更多信息请查看 CocoaPods官方网站

手动集成

  • 下载 Bugly iOS SDK
  • 拖拽Bugly.framework文件到Xcode工程内(请勾选Copy items if needed选项)
  • 添加依赖库
    • SystemConfiguration.framework
    • Security.framework
    • libz.dylib 或 libz.tbd
    • libc++.dylib 或 libc++.tbd

初始化SDK

导入头文件

在工程的AppDelegate.m文件导入头文件

#import <Bugly/Bugly.h>

如果是Swift工程,请在对应bridging-header.h中导入

初始化Bugly

在工程AppDelegate.mapplication:didFinishLaunchingWithOptions:方法中初始化:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Bugly startWithAppId:@"此处替换为你的AppId"];
return YES;
}

默认Debug模式,是不会生成dSYM文件,需要开启.重新编译CMD+B,修改配置如下图


Bugly后台显示异常数据


dSYM文件 
iOS平台中,dSYM文件是指具有调试信息的目标文件,文件名通常为:xxx.app.dSYM。

  • 为了方便找回Crash对应的dSYM文件和还原堆栈,建议每次构建或者发布APP版本的时候,备份好dSYM文件。


生成后,在哪里可以找到dSYM文件?




保存log到本地,并上传到Bugly管理后台

1.遵守代理协议

@interface AppDelegate ()<BuglyDelegate>

2.设置代理对象

BuglyConfig *config = [[BuglyConfig alloc] init];
config.delegate = self;
[Bugly startWithAppId:@"你的AppId" config:config];

3.实现代理方法attachmentForException

#pragma mark - Bugly代理 - 捕获异常,回调(@return 返回需上报记录,随 异常上报一起上报)
- (NSString *)attachmentForException:(NSException *)exception {

#ifdef DEBUG // 调试
return [NSString stringWithFormat:@"我是携带信息:%@",[self redirectNSLogToDocumentFolder]];
#endif

return nil;
}

#pragma mark - 保存日志文件
- (NSString *)redirectNSLogToDocumentFolder{
//如果已经连接Xcode调试则不输出到文件
if(isatty(STDOUT_FILENO)) {
return nil;
}
UIDevice *device = [UIDevice currentDevice];
if([[device model] hasSuffix:@"Simulator"]){
//在模拟器不保存到文件中
return nil;
}
//获取Document目录下的Log文件夹,若没有则新建
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *logDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"Log"];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL fileExists = [fileManager fileExistsAtPath:logDirectory];
if (!fileExists) {
[fileManager createDirectoryAtPath:logDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"]];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; //每次启动后都保存一个新的日志文件中
NSString *dateStr = [formatter stringFromDate:[NSDate date]];
NSString *logFilePath = [logDirectory stringByAppendingFormat:@"/%@.txt",dateStr];
// freopen 重定向输出输出流,将log输入到文件
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);

return [[NSString alloc] initWithContentsOfFile:logFilePath encoding:NSUTF8StringEncoding error:nil];

}

Bugly iOS 符号表配置

什么是符号表?

符号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示:

<起始地址> <结束地址> <函数> [<文件名:行号>]

为什么要配置符号表? 
为了能快速并准确地定位用户APP发生Crash的代码位置,Bugly使用符号表对APP发生Crash的程序堆栈进行解析和还原。

自动配置:XCode + sh脚本


使用文档中的方式一进行配置(默认方式)

配置Xcode编译执行脚本

  • 在Xcode工程对应Target的Build Phases中新增Run Scrpit Phase


打开工具包中的dSYM_upload.sh,复制所有内容,在新增的Run Scrpit Phase中粘贴


修改新增的Run Scrpit中的 <YOUR_APP_ID> 为您的App ID<YOUR_APP_KEY>为您的App Key<YOUR_BUNDLE_ID> 为App的Bundle Id



脚本默认在Debug模式及模拟器编译情况下不会上传符号表,在需要上传的时候,请修改下列选项

  • Debug模式编译是否上传,1=上传 0=不上传,默认不上传
UPLOAD_DEBUG_SYMBOLS=0
  • 模拟器编译是否上传,1=上传 0=不上传,默认不上传
UPLOAD_SIMULATOR_SYMBOLS=0

至此,自动上传符号表脚本配置完毕,Bugly 会在每次 Xcode 工程编译后自动完成符号表配置工作。

收起阅读 »

iOS- 研发助手DoraemonKit技术实现(二)

一、前言性能问题极大程度的会影响到用户的体验,对于我们开发者和测试同学要随时随地保证我们app的质量,避免不好的体验带来用户的流失。本篇文章我们来讲一下,性能监控的几款工具的技术实现。主要包括,帧率监控、CPU监控、内存监控、流量监控、卡顿监控和自定义监控这几...
继续阅读 »

一、前言

性能问题极大程度的会影响到用户的体验,对于我们开发者和测试同学要随时随地保证我们app的质量,避免不好的体验带来用户的流失。本篇文章我们来讲一下,性能监控的几款工具的技术实现。主要包括,帧率监控、CPU监控、内存监控、流量监控、卡顿监控和自定义监控这几个功能。

有人说帧率、CPU和内存这些信息我们都可以在Xcode中的Instruments工具进行联调的时候可以查看,为什么还要在客户端中打印出来呢?

  1. 第一、很多测试同学比较关注App质量,但是他们却没有Xcode运行环境,他们对于质量数据无法很有效的查看。
  2. 第二、App端实时的查看App的质量数据,不依赖IDE,方便快捷直观。
  3. 第三、实时采集性能数据,为后期结合测试平台产生性能数据报表提供数据来源。

二、技术实现

3.1:帧率展示

app的流畅度是最直接影响用户体验的,如果我们app持续卡顿,会严重影响我们app的用户留存度。所以对于用户App是否流畅进行监控,能够让我们今早的发现我们app的性能问题。对于App流畅度最直观最简单的监控手段就是对我们App的帧率进行监控。

帧率(FPS)是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的动作就会越流畅。对于我们App开发来说,我们要保持FPS高于50以上,用户体验才会流畅。

在YYKit Demo工程中有一个工具类叫YYFPSLabel,它是基于CADisplayLink这个类做FPS计算的,CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它会在屏幕每次刷新回调一次。既然CADisplayLink可以以屏幕刷新的频率调用指定selector,而且iOS系统中正常的屏幕刷新率为60Hz(60次每秒),那只要在这个方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。

大致实现思路如下:

- (void)startRecord{
if (_link) {
_link.paused = NO;
}else{
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(trigger:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
_record = [DoraemonRecordModel instanceWithType:DoraemonRecordTypeFPS];
_record.startTime = [[NSDate date] timeIntervalSince1970];
}
}

- (void)trigger:(CADisplayLink *)link{
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}

_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
CGFloat fps = _count / delta;
_count = 0;

NSInteger intFps = (NSInteger)(fps+0.5);
// 0~60 对应 高度0~200
[self.record addRecordValue:fps time:[[NSDate date] timeIntervalSince1970]];
[_oscillogramView addHeightValue:fps*200./60. andTipValue:[NSString stringWithFormat:@"%zi",intFps]];
}

值得注意的是基于CADisplayLink实现的 FPS 在生产场景中只有指导意义,不能代表真实的 FPS,因为基于CADisplayLink实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop 的帧率。但要真正定位到准确的性能问题所在,最好还是通过Instrument来确认。

3.2:CPU展示

CPU是移动设备的运算核心和控制核心,如果我们的App的使用率长时间处于高消耗的话,我们的手机会发热,电量使用加剧,导致App产生卡顿,严重影响用户体验。所以对于CPU使用率进行实时的监控,也有利于及时的把控我们App的整体质量,阻止不合格的功能上线。

对于app使用率的获取,网上的方案还是比较统一的。

  1. 使用task_threads函数,获取当前App行程中所有的线程列表。
  2. 对于第一步中获取的线程列表进行遍历,通过thread_info函数获取每一个非闲置线程的cpu使用率,进行相加。
  3. 使用vm_deallocate函数释放资源。

代码实现如下:

+ (CGFloat)cpuUsageForApp {
kern_return_t kr;
thread_array_t thread_list;
mach_msg_type_number_t thread_count;
thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count;
thread_basic_info_t basic_info_th;

// get threads in the task
// 获取当前进程中 线程列表
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS)
return -1;

float tot_cpu = 0;

for (int j = 0; j < thread_count; j++) {
thread_info_count = THREAD_INFO_MAX;
//获取每一个线程信息
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
(thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS)
return -1;

basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
// cpu_usage : Scaled cpu usage percentage. The scale factor is TH_USAGE_SCALE.
//宏定义TH_USAGE_SCALE返回CPU处理总频率:
tot_cpu += basic_info_th->cpu_usage / (float)TH_USAGE_SCALE;
}

} // for each thread

// 注意方法最后要调用 vm_deallocate,防止出现内存泄漏
kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);

return tot_cpu;
}

测试结果基本和Xcode测量出来的cpu使用率是一样的,还是比较准确的。

3.3:内存展示

设备内存和CPU一样都是系统中最稀少的资源,也是最有可能产生竞争的资源,应用内存跟app的性能直接相关。如果一个app在前台消耗内存过多,会引起系统强杀,这种现象叫做OOM。表现跟crash一样,而且这种crash事件无法被捕获到的。

获取app消耗的内存,刚开始使用的是获取使用的物理内存大小resident_size,网上大部分也是这种方案。

//当前app消耗的内存
+ (NSUInteger)useMemoryForApp{
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if(kernelReturn == KERN_SUCCESS)
{
int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
return memoryUsageInByte/1024/1024;
}
else
{
return -1;
}
}

//设备总的内存
+ (NSUInteger)totalMemoryForDevice{
return [NSProcessInfo processInfo].physicalMemory/1024/1024;
}

3.4:流量监控

在线下开发阶段,我们开发要和服务端联调结果,我们需要Xcode断点调试服务器返回的结果是否正确。测试阶段,测试同学会通过Charles设置代理查看结果,这些操作都需要依赖第三方工具才能实现流量监控。能不能有一个工具,能够随身携带,对流量进行监控拦截,能够方便我们很多。我们DoraemonKit就做了这件事。

对于流量监控,业界基本有以上几个方案:

  • 方案1 : 腾讯GT的方案,监控系统的上行流量和下行流量。这样监控的话,力度太粗了,不能得到每一个app的流量统计,更不能的得到每一个接口的流量和统计,不符合我们的需求。

  • 方案2 : 浸入业务方自己的网路库,做流量统计,这种方案可以做的非常细节,但是不是特别通用。我们公司内部omega监控平台就是这么做的,omega的流量监控代码是写在OneNetworking中的。不是特别通用。比如我们杭州团队的网路库是自研的,如果要接入omega的网络监控功能,就需要在自己的网络库中,写流量统计代码。

  • 方案3 : hook系统底层网络库,这种方式比较通用,但是非常繁琐,需要hook很多个类和方法。阿里有篇文档化介绍了他们流量监控的方案,就是采用这种,下面这张图我截取过来的,看一下,还是比较复杂的。


  • 方案4 : 也是DoraemonKit采用的方案,使用iOS中一个非常强大的类,叫NSURLProtocol,这个类可以拦截NSURLConnection、NSUrlSession、UIWebView中所有的网络请求,获取每一个网络请求的request和response对象。但是这个类无法拦截tcp的请求,这个是他的缺点。美团的内部监控工具赫兹就是基于该类进行处理的。

下面就是DoraemonKit中NSURLProtocol的具体实现:

@interface DoraemonNSURLProtocol()<NSURLConnectionDelegate,NSURLConnectionDataDelegate>

@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, assign) NSTimeInterval startTime;
@property (nonatomic, strong) NSURLResponse *response;
@property (nonatomic, strong) NSMutableData *data;
@property (nonatomic, strong) NSError *error;

@end

@implementation DoraemonNSURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
if ([NSURLProtocol propertyForKey:kDoraemonProtocolKey inRequest:request]) {
return NO;
}
if (![DoraemonNetFlowManager shareInstance].canIntercept) {
return NO;
}
if (![request.URL.scheme isEqualToString:@"http"] &&
![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
//NSLog(@"DoraemonNSURLProtocol == %@",request.URL.absoluteString);
return YES;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
//NSLog(@"canonicalRequestForRequest");
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:kDoraemonProtocolKey inRequest:mutableReqeust];
return [mutableReqeust copy];
}

- (void)startLoading{
//NSLog(@"startLoading");
self.connection = [[NSURLConnection alloc] initWithRequest:[[self class] canonicalRequestForRequest:self.request] delegate:self];
[self.connection start];
self.data = [NSMutableData data];
self.startTime = [[NSDate date] timeIntervalSince1970];
}

- (void)stopLoading{
//NSLog(@"stopLoading");
[self.connection cancel];
DoraemonNetFlowHttpModel *httpModel = [DoraemonNetFlowHttpModel dealWithResponseData:self.data response:self.response request:self.request];
if (!self.response) {
httpModel.statusCode = self.error.localizedDescription;
}
httpModel.startTime = self.startTime;
httpModel.endTime = [[NSDate date] timeIntervalSince1970];

httpModel.totalDuration = [NSString stringWithFormat:@"%f",[[NSDate date] timeIntervalSince1970] - self.startTime];
[[DoraemonNetFlowDataSource shareInstance] addHttpModel:httpModel];
}


#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[[self client] URLProtocol:self didFailWithError:error];
self.error = error;
}

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return YES;
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[self client] URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}

- (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[self client] URLProtocol:self didCancelAuthenticationChallenge:challenge];
}

#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
self.response = response;
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[[self client] URLProtocol:self didLoadData:data];
[self.data appendData:data];
}

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse{
return cachedResponse;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[[self client] URLProtocolDidFinishLoading:self];
}

3.5:自定义监控

以上所有的操作都是针对于单个指标,无法提供一套全面的监控数据,自定义监控可以选择你需要监控的数据,目前包括帧率、CPU使用率、内存使用量和流量监控,这些监控没有波形图进行显示,均在后台进行监控,测试完毕,会把这些数据上传到我们后台进行分析。

因为目前后台是基于我们内部平台上开发的,暂时不提供开源。不过后续的话,我们也会考虑将后台的功能的功能对外提供,请大家拭目以待。对于开源版本的话,目前性能测试的结果保存在沙盒Library/Caches/DoraemonPerformance中,使用者可以使用沙盒浏览器功能导出来之后自己进行分析。

DoraemonKit项目地址:github.com/didi/Doraem…


摘自:https://blog.csdn.net/weixin_33847182/article/details/91472599

收起阅读 »

iOS- 研发助手DoraemonKit技术实现(一)

一、前言一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于A...
继续阅读 »

一、前言

一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于App的信息进行快速的查看。一键接入、使用方便,提高开发、测试、视觉同学的工作效率,提高我们App上线的完整度和稳定性。

目前DoraemonKit拥有的功能大概分为以下几点:

  1. 常用工具 : App信息展示,沙盒浏览、MockGPS、H5任意门、子线程UI检查、日志显示。
  2. 性能工具 : 帧率监控、CPU监控、内存监控、流量监控、自定义监控。
  3. 视觉工具 : 颜色吸管、组件检查、对齐标尺。
  4. 业务专区 : 支持业务测试组件接入到DoraemonKit面板中。

拿我们App接入效果如下:

 上面两行是业务线自定义的工具,接入方可以自定义。除此之外都是内置工具集合

因为里面功能比较多,大概会分三篇文章介绍DoraemonKit的使用和技术实现,这是第一篇主要介绍常用工具集中的几款工具实现。

二、技术实现

2.1:App信息展示

我们要看一些手机信息或者App的一些基本信息的时候,需要到系统设置去找,比较麻烦。特别是权限信息,在我们app装的比较多的时候,我们很难快速找到我们app的权限信息。而这些信息从代码角度都是比较容易获取的。我们把我们感兴趣的信息列表出来直接查看,避免了去手机设置里查看或者查看源代码的麻烦。

获取手机型号

我们从手机设置里面是找不到我们的手机具体是哪一款的文字表述的,比如我的手机是iphone8 Pro,在手机型号里面显示的是MQ8E2CH/A。对于iPhone不熟悉的人很难从外表对iphone进行区分。而手机型号,我们从代码角度就很好获取。

+ (NSString *)iphoneType{
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];

//iPhone
if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G";
...
//其他对应关系请看下面对应表
return platform;
}

获取手机系统版本

//获取手机系统版本
NSString *phoneVersion = [[UIDevice currentDevice] systemVersion];
复制代码

获取App BundleId

一个app分为测试版本、企业版本、appStore发售版本,每一个app长得都一样,如何对他们进行区分呢,那就要用到BundleId这个属性了。

//获取bundle id
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];

获取App 版本号

//获取App版本号
NSString *bundleVersionCode = [[[NSBundle mainBundle]infoDictionary] objectForKey:@"CFBundleVersion"];

权限信息查看

当我们发现App运行不正常,比如无法定位,网络一直失败,无法收到推送信息等问题的时候,我们第一个反应就是去手机设置里面去看我们app相关的权限有没有打开。DoraemonKit集成了对于地理位置权限、网络权限、推送权限、相机权限、麦克风权限、相册权限、通讯录权限、日历权限、提醒事项权限的查询。

由于代码比较多,这里就不一一贴出来了。大家可以去DorameonKit/Core/Plugin/AppInfo中自己去查看。这里讲一下,权限查询结果几个值的意义。

  • NotDetermined => 用户还没有选择。
  • Restricted => 该权限受限,比如家长控制。
  • Denied => 用户拒绝使用该权限。
  • Authorized => 用户同意使用该权限。

2.2:沙盒浏览

以前如果我们要去查看App缓存、日志信息,都需要访问沙盒。由于iOS的封闭性,我们无法直接查看沙盒中的文件内容。如果我们要去访问沙盒,基本上有两种方式,第一种使用Xcode自带的工具,从Windows-->Devices进入设备管理界面,通过Download Container的方式导出整个app的沙盒。第二种方式,就是自己写代码,访问沙盒中指定文件,然后使用NSLog的方式打印出来。这两种方式都比较麻烦。

DoraemonKit给出的解决方案:就是自己做一个简单的文件浏览器,通过NSFileManager对象对沙盒文件进行遍历,同时支持对于文件和文件夹的删除操作。对于文件支持本地预览或者通过airdrop的方式或者其他分享方式发送到PC端进行更加细致的操作。

怎么用NSFileManager对象遍历文件和删除文件这里就不说了,大家可以参考DorameonKit/Core/Plugin/Sanbox中的代码。这里讲一下:如何将手机中的文件快速上传到Mac端?刚开始我们还绕了一点路,我们在手机端搭了一个微服务,mac通过浏览器去访问它。后来和同事聊天的时候知道了UIActivityViewController这个类,可以十分便捷地吊起系统分享组件或者是其他注册到系统分享组件中的分享方式,比如微信、钉钉。实现代码非常简单,如下所示:

- (void)shareFileWithPath:(NSString *)filePath{

NSURL *url = [NSURL fileURLWithPath:filePath];
NSArray *objectsToShare = @[url];

UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil];
NSArray *excludedActivities = @[UIActivityTypePostToTwitter, UIActivityTypePostToFacebook,
UIActivityTypePostToWeibo,
UIActivityTypeMessage, UIActivityTypeMail,
UIActivityTypePrint, UIActivityTypeCopyToPasteboard,
UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo, UIActivityTypePostToTencentWeibo];
controller.excludedActivityTypes = excludedActivities;

[self presentViewController:controller animated:YES completion:nil];
}

2.3:MockGPS

我们有些业务会根据地理位置不同,而有不同的业务处理逻辑。而我们开发或者测试,当然不可能去每一个地址都测试一遍。这种情况下,测试同学一般会找到我们让我们手动改掉系统获取经纬度的回调,或者修改GPX文件,然后再重新打一个包。这样也非常麻烦。

DoraemonKit给出的解决方案:提供一套地图界面,支持在地图中滑动选择或者手动输入经纬度,然后自动替换掉我们App中返回的当前经纬度信息。这里的难点是如何不需要重新打包自动替换掉系统返回的当前经纬度信息?

CLLocationManager的delegate中有一个方法如下:

/*
* locationManager:didUpdateLocations:
*
* Discussion:
* Invoked when new locations are available. Required for delivery of
* deferred locations. If implemented, updates will
* not be delivered to locationManager:didUpdateToLocation:fromLocation:
*
* locations is an array of CLLocation objects in chronological order.
*/
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray<CLLocation *> *)locations API_AVAILABLE(ios(6.0), macos(10.9));

我们通常是在这个函数中获取当前系统的经纬度信息。我们如果想要没有侵入式的修改这个函数的默认实现方式,想到的第一个方法就是Method Swizzling。但是真正在实现过程中,你会发现Method Swizzling需要当前实例和方法,方法是- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations 我们有了,但是实例,每一个app都有自己的实现,无法做到统一处理。我们就换了一个思路,如何能获取该实现了该定位方法的实例呢?就是使用Method Swizzling Hook住CLLocationManager的setDelegate方法,就能获取具体是哪一个实例实现了- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations 方法。

具体方法如下:

第一步: 生成一个CLLocationManager的分类CLLocationManager(Doraemon),在这个分类中,实现- (void)doraemon_swizzleLocationDelegate:(id)delegate这个方法,用来进行方法交换。

- (void)doraemon_swizzleLocationDelegate:(id)delegate {
if (delegate) {
//1、让所有的CLLocationManager的代理都设置为[DoraemonGPSMocker shareInstance],让他做中间转发
[self doraemon_swizzleLocationDelegate:[DoraemonGPSMocker shareInstance]];
//2、绑定所有CLLocationManager实例与delegate的关系,用于[DoraemonGPSMocker shareInstance]做目标转发用。
[[DoraemonGPSMocker shareInstance] addLocationBinder:self delegate:delegate];

//3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。
Protocol *proto = objc_getProtocol("CLLocationManagerDelegate");
unsigned int count;
struct objc_method_description *methods = protocol_copyMethodDescriptionList(proto, NO, YES, &count);
NSMutableArray *array = [NSMutableArray array];
for(unsigned i = 0; i < count; i++)
{
SEL sel = methods[i].name;
if ([delegate respondsToSelector:sel]) {
if (![[DoraemonGPSMocker shareInstance] respondsToSelector:sel]) {
NSAssert(NO, @"你在Delegate %@ 中所使用的SEL %@,暂不支持,请联系DoraemonKit开发者",delegate,sel);
}
}
}
free(methods);

}else{
[self doraemon_swizzleLocationDelegate:delegate];
}
}


在这个函数中主要做了三件事情,1、将所有的定位回调统一交给[DoraemonGPSMocker shareInstance]处理 2、[DoraemonGPSMocker shareInstance]绑定了所有CLLocationManager与它的delegate的一一对应关系。3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。

第二步:当有一个定位回调过来的时候,我们先传给[DoraemonGPSMocker shareInstance],然后[DoraemonGPSMocker shareInstance]再转发给它绑定过的所有的delegate。那我们App为例,绑定关系如下:{

    "0x2800a07a0_binder" = "<CLLocationManager: 0x2800a07a0>";
"0x2800a07a0_delegate" = "<MAMapLocationManager: 0x2800a04d0>";
"0x2800b59a0_binder" = "<CLLocationManager: 0x2800b59a0>";
"0x2800b59a0_delegate" = "<KDDriverLocationManager: 0x2829d3bf0>";
}

由此可见,我们App的统一定位KDDriverLocationManager和苹果地图的定位MAMapLocationManager都是使用都是CLLocationManager提供的。

具体 DoraemonGPSMocker这个类如何实现,请参考DorameonKit/Core/Plugin/GPS中的代码。

2.4:H5任意门

有的时候Native和H5开发同时开发一个功能,H5依赖native提供入口,而这个时候Native还没有开发好,这个时候H5开发就没法在App上看到效果。再比如,有些H5页面处于的位置比较深入,就像我们代驾司机端,做单流程比较多,有的H5界面需要很繁琐的操作才能展示到App上,不方便我们查看和定位问题。 这个时候我们可以为app做一个简单的浏览器,输入url,使用自带的容器进行跳转。因为每一个app的H5容器基本上都是自定义过得,都会有自己的bridge定制化,所以这个H5容器没有办法使用系统原生的UIWebView或者WKWebView,就只能交给业务方自己去完成。我们在DorameonKit初始化的时候,提供了一个回调让业务方用自己的H5容器去打开这个Url:

[[DoraemonManager shareInstance] addH5DoorBlock:^(NSString *h5Url) {
//使用自己的H5容器打开这个链接
}];

2.5:子线程UI检查

在iOS中是不允许在子线程中对UI进行操作和渲染的,不然会造成未知的错误和问题,甚至会导致crash。我们在最近几个版本中发现新增了一些crash,调查原因就是在子线程中操作UI导致的。为了对于这种情况可以提早被我们发现,我在在DorameonKit中增加了子线程UI渲染检查查询。

具体事项思路,我们hook住UIView的三个必须在主线程中操作的绘制方法。1、setNeedsLayout 2、setNeedsDisplay 3、setNeedsDisplayInRect:。然后判断他们是不是在子线程中进行操作,如果是在子线程进行操作的话,打印出当前代码调用堆栈,提供给开发进行解决。具体代码如下:

@implementation UIView (Doraemon)

+ (void)load{
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsLayout) swizzledSel:@selector(doraemon_setNeedsLayout)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplay) swizzledSel:@selector(doraemon_setNeedsDisplay)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplayInRect:) swizzledSel:@selector(doraemon_setNeedsDisplayInRect:)];
}

- (void)doraemon_setNeedsLayout{
[self doraemon_setNeedsLayout];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplay{
[self doraemon_setNeedsDisplay];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplayInRect:(CGRect)rect{
[self doraemon_setNeedsDisplayInRect:rect];
[self uiCheck];
}

- (void)uiCheck{
if([[DoraemonCacheManager sharedInstance] subThreadUICheckSwitch]){
if(![NSThread isMainThread]){
NSString *report = [BSBacktraceLogger bs_backtraceOfCurrentThread];
NSDictionary *dic = @{
@"title":[DoraemonUtil dateFormatNow],
@"content":report
};
[[DoraemonSubThreadUICheckManager sharedInstance].checkArray addObject:dic];
}
}
}

@end

2.6:日志显示

这个主要是方便我们查看本地日志,以前我们如果要查看日志,需要自己写代码,访问沙盒导出日志文件,然后再查看。也是比较麻烦的。

DoraemonKit的解决方案是:我们每一次触发日志的时候,都把日志内容显示到界面上,方便我们查看。 如何实现的呢?因为我们这个工具并不是一个通用性的工具,只针对于底层日志库是CocoaLumberjack的情况。稍微讲一下的CocoaLumberjack原理,所有的log都会发给DDLog对象,其运行在自己的一个GCD队列中,之后,DDLog会将log分发给其下注册的一个或者多个Logger中,这一步在多核下面是并发的,效率很高。每一个Logger处理收到的log也是在它们自己的GCD队列下做的,它们询问其下的Formatter,获取Log消息格式,然后根据Logger的逻辑,将log消息分发到不同的地方。系统自带三个Logger处理器,DDTTYLogger,主要将日志发送到Xcode控制台;DDASLLogger,主要讲日志发送到苹果的日志系统Console.app; DDFileLogger,主要将日志发送到文件中保存起来,也是我们开发用到最多的。但是自带的Logger并不满足我们的需求,我们的需求是将日志显示到UI界面中,所以我们需要新建一个类DoraemonLogger,继承于DDAbstractLogger,然后重写logMessage方法,将每一条传过来的日志打印到UI界面中。


DoraemonKit项目地址:github.com/didi/Doraem…


转自:https://blog.csdn.net/weixin_33737134/article/details/91469113

收起阅读 »

iOS使用RunLoop监控线上卡顿

通过iOS性能优化 我们知道,简单来说App卡顿,就是FPS达不到60帧率,丢帧现象,就会卡顿。但是很多时候,我们只知道丢帧了。具体为什么丢帧,却不是很清楚,那么我们要怎么监控呢,首先我们要明白,要找出卡顿,就是要找出主线程做了什么,而线程消息,是依赖RunL...
继续阅读 »

通过iOS性能优化 我们知道,简单来说App卡顿,就是FPS达不到60帧率,丢帧现象,就会卡顿。但是很多时候,我们只知道丢帧了。具体为什么丢帧,却不是很清楚,那么我们要怎么监控呢,首先我们要明白,要找出卡顿,就是要找出主线程做了什么,而线程消息,是依赖RunLoop的,所以我们可以使用RunLoop来监控。

RunLoop是用来监听输入源,进行调度处理的。如果RunLoop的线程进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。

RunLoop和信号量

我们可以使用CFRunLoopObserverRef来监控NSRunLoop的状态,通过它可以实时获得这些状态值的变化。

runloop

关于runloop,可以参照 RunLoop详解之源码分析 这篇文章详细了解。这里简单总结一下:

runloop的状态

/* Run Loop Observer Activities */
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
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变
};

CFRunLoopObserverRef 的使用流程
设置Runloop observer的运行环境

CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
2. 创建Runloop observer对象

第一个参数:用于分配observer对象的内存
第二个参数:用以设置observer所要关注的事件
第三个参数:用于标识该observer是在第一次进入runloop时执行还是每次进入runloop处理时均执行
第四个参数:用于设置该observer的优先级
第五个参数:用于设置该observer的回调函数
第六个参数:用于设置该observer的运行环境
// 创建Runloop observer对象
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);



3. 将新建的observer加入到当前thread的runloop

CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);


4. 将observer从当前thread的runloop中移除

CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);


5. 释放 observer

CFRelease(_observer); _observer = NULL;

信号量
关于信号量,可以详细参考 GCD信号量-dispatch_semaphore_t
简单来说,主要有三个函数

dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量

dispatch_semaphore_create(long value);和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信号量。如果信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。
dispatch_semaphore_signal(dispatch_semaphore_t deem); 发送信号量。该函数会对信号量的值进行加1操作。
通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务……如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。

监控卡顿
原理: 利用观察Runloop各种状态变化的持续时间来检测计算是否发生卡顿
一次有效卡顿采用了“N次卡顿超过阈值T”的判定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:举例,卡顿阈值T=500ms、卡顿次数N=1,可以判定为单次耗时较长的一次有效卡顿;而卡顿阈值T=50ms、卡顿次数N=5,可以判定为频次较快的一次有效卡顿

主要代码

// minimum
static const NSInteger MXRMonitorRunloopMinOneStandstillMillisecond = 20;
static const NSInteger MXRMonitorRunloopMinStandstillCount = 1;

// default
// 超过多少毫秒为一次卡顿
static const NSInteger MXRMonitorRunloopOneStandstillMillisecond = 50;
// 多少次卡顿纪录为一次有效卡顿
static const NSInteger MXRMonitorRunloopStandstillCount = 1;

@interface YZMonitorRunloop(){
CFRunLoopObserverRef _observer; // 观察者
dispatch_semaphore_t _semaphore; // 信号量
CFRunLoopActivity _activity; // 状态
}
@property (nonatomic, assign) BOOL isCancel; //f是否取消检测
@property (nonatomic, assign) NSInteger countTime; // 耗时次数
@property (nonatomic, strong) NSMutableArray *backtrace;
-(void)registerObserver{
// 1. 设置Runloop observer的运行环境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
// 2. 创建Runloop observer对象

// 第一个参数:用于分配observer对象的内存
// 第二个参数:用以设置observer所要关注的事件
// 第三个参数:用于标识该observer是在第一次进入runloop时执行还是每次进入runloop处理时均执行
// 第四个参数:用于设置该observer的优先级
// 第五个参数:用于设置该observer的回调函数
// 第六个参数:用于设置该observer的运行环境
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
// 3. 将新建的observer加入到当前thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 创建信号 dispatchSemaphore的知识参考:https://www.jianshu.com/p/24ffa819379c
_semaphore = dispatch_semaphore_create(0); ////Dispatch Semaphore保证同步

__weak __typeof(self) weakSelf = self;

// dispatch_queue_t queue = dispatch_queue_create("kadun", NULL);

// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// dispatch_async(queue, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
while (YES) {
if (strongSelf.isCancel) {
return;
}
// N次卡顿超过阈值T记录为一次卡顿
// 等待信号量:如果信号量是0,则阻塞当前线程;如果信号量大于0,则此函数会把信号量-1,继续执行线程。此处超时时间设为limitMillisecond 毫秒。
// 返回值:如果线程是唤醒的,则返回非0,否则返回0
long semaphoreWait = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));

if (semaphoreWait != 0) {

// 如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠(kCFRunLoopBeforeSources),或者线程唤醒后接收消息时间过长(kCFRunLoopAfterWaiting)而无法进入下一步的话,就可以认为是线程受阻。
//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够监测到是否卡顿
if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {

if (++strongSelf.countTime < strongSelf.standstillCount){
NSLog(@"%ld",strongSelf.countTime);
continue;
}
[strongSelf logStack];
[strongSelf printLogTrace];

NSString *backtrace = [YZCallStack yz_backtraceOfMainThread];
NSLog(@"++++%@",backtrace);

[[YZLogFile sharedInstance] writefile:backtrace];

if (strongSelf.callbackWhenStandStill) {
strongSelf.callbackWhenStandStill();
}
}
}
strongSelf.countTime = 0;
}
});
}

demo测试
我把demo放在了github demo地址

使用时候,只需要

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

[[YZMonitorRunloop sharedInstance] startMonitor];
[YZMonitorRunloop sharedInstance].callbackWhenStandStill = ^{
NSLog(@"eagle.检测到卡顿了");
};
return YES;
}

控制器中,每次点击屏幕,休眠1秒钟,如下

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
usleep(1 * 1000 * 1000); // 1秒

}

@end

点击屏幕之后,打印如下

YZMonitorRunLoopDemo[10288:1915706] ==========检测到卡顿之后调用堆栈==========
(
"0 YZMonitorRunLoopDemo 0x00000001022c653c -[YZMonitorRunloop logStack] + 96",
"1 YZMonitorRunLoopDemo 0x00000001022c62a0 __36-[YZMonitorRunloop registerObserver]_block_invoke + 484",
"2 libdispatch.dylib 0x00000001026ab6f0 _dispatch_call_block_and_release + 24",
"3 libdispatch.dylib 0x00000001026acc74 _dispatch_client_callout + 16",
"4 libdispatch.dylib 0x00000001026afad4 _dispatch_queue_override_invoke + 876",
"5 libdispatch.dylib 0x00000001026bddc8 _dispatch_root_queue_drain + 372",
"6 libdispatch.dylib 0x00000001026be7ac _dispatch_worker_thread2 + 156",
"7 libsystem_pthread.dylib 0x00000001b534d1b4 _pthread_wqthread + 464",
"8 libsystem_pthread.dylib 0x00000001b534fcd4 start_wqthread + 4"
)

libsystem_kernel.dylib 0x1b52ca400 __semwait_signal + 8
libsystem_c.dylib 0x1b524156c nanosleep + 212
libsystem_c.dylib 0x1b5241444 usleep + 64
YZMonitorRunLoopDemo 0x1022c18dc -[ViewController touchesBegan:withEvent:] + 76
UIKitCore 0x1e1f4fcdc <redacted> + 336
UIKitCore 0x1e1f4fb78 <redacted> + 60
UIKitCore 0x1e1f5e0f8 <redacted> + 1584
UIKitCore 0x1e1f5f52c <redacted> + 3140
UIKitCore 0x1e1f3f59c <redacted> + 340
UIKitCore 0x1e2005714 <redacted> + 1768
UIKitCore 0x1e2007e40 <redacted> + 4828
UIKitCore 0x1e2001070 <redacted> + 152
CoreFoundation 0x1b56bf018 <redacted> + 24
CoreFoundation 0x1b56bef98 <redacted> + 88
CoreFoundation 0x1b56be880 <redacted> + 176
CoreFoundation 0x1b56b97

即可定位到卡顿位置
-[ViewController touchesBegan:withEvent:]
卡顿日志写入本地
上面已经监控到了卡顿,和调用堆栈。如果是debug模式下,可以直接看日志,如果想在线上查看的话,可以写入本地,然后上传到服务器

写入本地数据库
创建本地路径

-(NSString *)getLogPath{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
NSString *homePath = [paths objectAtIndex:0];

NSString *filePath = [homePath stringByAppendingPathComponent:@"Caton.log"];
return filePath;
}

如果是第一次写入,带上设备信息,手机型号等信息

NSString *filePath = [self getLogPath];
NSFileManager *fileManager = [NSFileManager defaultManager];

if(![fileManager fileExistsAtPath:filePath]) //如果不存在
{
NSString *str = @"卡顿日志";
NSString *systemVersion = [NSString stringWithFormat:@"手机版本: %@",[YZAppInfoUtil iphoneSystemVersion]];
NSString *iphoneType = [NSString stringWithFormat:@"手机型号: %@",[YZAppInfoUtil iphoneType]];
str = [NSString stringWithFormat:@"%@\n%@\n%@",str,systemVersion,iphoneType];
[str writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

}

如果本地文件已经存在,就先判断大小是否过大,决定是否直接写入,还是先上传到服务器

float filesize = -1.0;
if ([fileManager fileExistsAtPath:filePath]) {
NSDictionary *fileDic = [fileManager attributesOfItemAtPath:filePath error:nil];
unsigned long long size = [[fileDic objectForKey:NSFileSize] longLongValue];
filesize = 1.0 * size / 1024;
}

NSLog(@"文件大小 filesize = %lf",filesize);
NSLog(@"文件内容 %@",string);
NSLog(@" ---------------------------------");

if (filesize > (self.MAXFileLength > 0 ? self.MAXFileLength:DefaultMAXLogFileLength)) {
// 上传到服务器
NSLog(@" 上传到服务器");
[self update];
[self clearLocalLogFile];
[self writeToLocalLogFilePath:filePath contentStr:string];
}else{
NSLog(@"继续写入本地");
[self writeToLocalLogFilePath:filePath contentStr:string];
}

压缩日志,上传服务器
因为都是文本数据,所以我们可以压缩之后,打打降低占用空间,然后进行上传,上传成功之后,删除本地,然后继续写入,等待下次写日志

压缩工具
使用 SSZipArchive具体使用起来也很简单,

// Unzipping
NSString *zipPath = @"path_to_your_zip_file";
NSString *destinationPath = @"path_to_the_folder_where_you_want_it_unzipped";
[SSZipArchive unzipFileAtPath:zipPath toDestination:destinationPath];
// Zipping
NSString *zippedPath = @"path_where_you_want_the_file_created";
NSArray *inputPaths = [NSArray arrayWithObjects:
[[NSBundle mainBundle] pathForResource:@"photo1" ofType:@"jpg"],
[[NSBundle mainBundle] pathForResource:@"photo2" ofType:@"jpg"]
nil];
[SSZipArchive createZipFileAtPath:zippedPath withFilesAtPaths:inputPaths];

代码中

NSString *zipPath = [self getLogZipPath];
NSString *password = nil;
NSMutableArray *filePaths = [[NSMutableArray alloc] init];
[filePaths addObject:[self getLogPath]];
BOOL success = [SSZipArchive createZipFileAtPath:zipPath withFilesAtPaths:filePaths withPassword:password.length > 0 ? password : nil];

if (success) {
NSLog(@"压缩成功");

}else{
NSLog(@"压缩失败");
}

具体如果上传到服务器,使用者可以用AFN等将本地的 zip文件上传到文件服务器即可,就不赘述了。
至此,我们做到了,用runloop,监控卡顿,写入日志,然后压缩上传服务器,删除本地的过程。
详细代码见demo地址

转自:https://www.jianshu.com/p/05ae5ff5a9c1

收起阅读 »

iOS序列化的进阶方案——Protocol Buffer

前言最近项目需要,引入Protocol Buffer来做对象序列化。正文Protocol Buffer是Google出的序列化数据格式,下面简称pb。我们更常用的序列化数据格式应该是json,json和pb本质上都是对象的序列化和反序列化,在项目中json也是...
继续阅读 »

前言

最近项目需要,引入Protocol Buffer来做对象序列化。

正文
Protocol Buffer是Google出的序列化数据格式,下面简称pb。
我们更常用的序列化数据格式应该是json,json和pb本质上都是对象的序列化和反序列化,在项目中json也是前后端通信的主要数据格式。
在本地存储时,我们可以使用YYModel将对象转成json对应的NSData,也可以使用NSKeyedArchiver结合实现NSCoding协议把对象转成NSData,进而将二进制数据存储在沙盒中或者数据库。
那么为什么不使用json,而要用pb?
因为项目中序列化数据到沙盒是一个高频场景,尝试过数据库、NSCoding+NSKeyedArchiver、YYModel等方法都有各自瓶颈:数据内容比较大数据库会造成体积膨胀过快不便管理,NSCoding+NSKeyedArchiver在序列化数据量较大的情况下性能不佳,YYModel在变动的时候不太友好。

相对而言,pb有以下特点:

  • pb是一种可扩展的序列化数据数据格式,新老版本的数据可以相互读取;

  • pb是使用字节流方式进行序列化,体积小速度快;(相对而言json是用字符串表示的,光表示字符串的""符号就有很多)

  • pb的代码是由描述文件proto生成,proto是文本文件便于做版本管理;

pb的使用

使用pb首先要定义proto的数据结构,语法非常简单,可以直接上手写:

syntax = "proto3";
message LYItemData {
uint32 itemId = 1;
string itemContentStr = 2;
}

这里定义一个最简单的message,第一行是声明proto的版本,然后添加两个属性itemId和itemContentStr;
使用的时候,用[LYItemData parseFromData:data error:nil];可以将NSData转换成对象,访问LYItemData类的data属性,可以拿到其序列化之后的二进制数据;
代码很简单, 序列化和反序列化都只有一行,使用样例:

NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test_data"];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
LYItemData *itemData;
if (data) {
itemData = [LYItemData parseFromData:data error:nil]; // 反序列化
}
else {
itemData = [LYItemData new];
itemData.itemId = (int)time(NULL);
itemData.itemContentStr = [self timeStampConversionNSString:itemData.itemId];
[[NSFileManager defaultManager] createFileAtPath:path contents:itemData.data attributes:nil]; // 访问itemData.data属性时会做一次序列化
}

message可以定义容器类型,包括数组、map等;
定义数组使用repeated,表示该元素是重复的,数量从0到若干个不等;
定义字典使用map,map里面带两个参数,分别表示key和value的type;

message LYArrayData {
repeated LYItemData items = 1;
map<int32, string> idToContentStrMap = 2;
}

也可以在message中声明另外一个message 的属性

message LYProtobufLocalData {
uint64 dataId = 1;
string dataContentStr = 2;
uint32 updateTime = 3;
LYArrayData arrData = 4;
}

了解这些常见的message定义方式,就可以满足大多数开发,其他用到再学也不迟。
其他使用方式例如any、oneof、reserved、enum、import、package可以自行探究,我们项目中没有使用到。
不管哪种定义方式,在定义成员属性的时候,都需要指定一个数字,这个数字是tag,需要保证在类中是唯一的。
tag是属性的唯一标识符,pb会在存储和读取的时候用到这个属性。

了解这些常见的message定义方式,就可以满足大多数开发,其他用到再学也不迟。
其他使用方式例如any、oneof、reserved、enum、import、package可以自行探究,我们项目中没有使用到。
不管哪种定义方式,在定义成员属性的时候,都需要指定一个数字,这个数字是tag,需要保证在类中是唯一的。
tag是属性的唯一标识符,pb会在存储和读取的时候用到这个属性。

代码生成
代码生成可以和Xcode结合,在每次编译之后自动生成。
在 Build Phases 里面添加一段脚本(下图中的Run Proto):先cd到proto所在的目录,然后运行脚本即可。

cd ${SOURCE_ROOT}/LearnProtoBuf/PB/
./protoc ProtobufData.proto --objc_out=./


如果项目中有多个proto,此处可以使用sh脚本,把路径名作为参数传入,在sh脚本里面分别对每个proto文件做代码生成。
如果不想使用这种方式,也可以按照传统方法先安装protobuf,网上教程比较多,这里不再赘述。

总结
在Restful架构逐渐被RPC架构淘汰的现在,pb取代json作为前后端的通信数据格式也是时代的潮流。
json最大的优势或许是后端已有的很多服务都是用json通信,一时间无法完全替换。
pb简单易用,对持续变更更加友好。
一次定义,多端使用;
版本更迭,格式兼容。

附录
官方参考文档--OC代码生成
PB-Github
二进制encode原理

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


收起阅读 »

探讨SWIFT 5.2的新功能特性

从表面上看,SWIFT 5.2在新的语言特性方面肯定是一个小版本,因为这个新版本的大部分重点是提高SWIFT底层基础结构的速度和稳定性,例如如何报告编译器错误,以及如何解决构建级依赖。然而,斯威夫特5.2总数新的语言特性可能相对较小,它确实包括两个新功能,它们...
继续阅读 »

从表面上看,SWIFT 5.2在新的语言特性方面肯定是一个小版本,因为这个新版本的大部分重点是提高SWIFT底层基础结构的速度和稳定性,例如如何报告编译器错误,以及如何解决构建级依赖。

然而,斯威夫特5.2总数新的语言特性可能相对较小,它确实包括两个新功能,它们可能会对SWIFT的整体功能产生相当大的影响。函数式程序设计语言.

本周,让我们探讨这些特性,以及我们如何可能使用它们来接受一些在函数式编程世界中非常流行的不同范例--在面向对象的SWIFT代码库中,它们可能会感觉更加一致和熟悉。

在我们开始之前,作为Xcode 11.4的一部分,SWIFT5.2仍然处于测试版,请注意,本文是一篇非常探索性的文章,代表了我对这些新语言特性的第一印象。随着我在生产中使用新特性获得更多经验,我的观点可能会发生变化,尽管我将尝试在这种情况下更新这篇文章,但我建议您使用本文作为灵感,亲自探索这些新特性,而不是直接使用以原样呈现的解决方案。

有了这个小小的免责声明,让我们开始探索吧!

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

调用类型为函数

尽管SWIFT并不是一种严格的函数式编程语言,但毫无疑问,函数在其总体设计和使用中扮演着非常重要的角色。从闭包如何作为异步回调使用,到集合如何大量使用典型的函数模式(如map和reduce-职能无处不在。

SWIFT5.2的有趣之处在于它开始模糊函数和类型之间的界限。尽管我们一直能够将任何给定类型的实例方法作为函数传递(因为SWIFT支持一级函数),我们现在能够调用某些类型,就好像它们本身是函数一样。.

让我们先来看看一个使用Cache我们内置的类型“SWIFT中的缓存”-这提供了一个更多的“快速友好”包装上的APINSCache:

class Cache {
private let wrapped = NSCache()
private let dateProvider: () -> Date
private let entryLifetime: TimeInterval

...

func insert(_ value: Value, forKey key: Key) {
...
}
}

假设我们想要向上面的类型添加一个方便的API--让我们自动使用插入值的id作为它的缓存键,以防当前Value类型符合标准库的Identifiable协议。虽然我们可以简单地命名新的apiinsert还有,我们要给它起一个非常特别的名字-callAsFunction:

extension Cache where Value: Identifiable, Key == Value.ID {
func callAsFunction(_ value: Value) {
insert(value, forKey: value.id)
}
}

这似乎是一种奇怪的命名约定,但通过这样命名我们的新方便方法,我们实际上已经给出了Cache输入一个有趣的新功能--它现在可能被称为函数--如下所示:

let document: Document = ...
let cache = Cache()

// We can now call our 'cache' variable as if it was referencing a
// function or a closure:
cache(document)

可以说,这既很酷,也很奇怪。但问题是-它有什么用呢?让我们继续探索,看看DocumentRenderer协议,它为用于呈现的各种类型定义了一个公共接口。Document应用程序中的实例:

protocol DocumentRenderer {
func render(_ document: Document,
in context: DocumentRenderingContext,
enableAnnotations: Bool)
}

类似于我们之前向我们的Cache类型,让我们在这里做同样的事情-只是这一次,我们将扩展上面的协议,以允许任何符合的类型被调用为一个函数,其中包含一组默认参数:

extension DocumentRenderer {
func callAsFunction(_ document: Document) {
render(document,
in: .makeDefaultContext(),
enableAnnotations: false
)
}
}

上述两个变化在孤立的情况下看起来可能不那么令人印象深刻,但是如果我们将它们放在一起,我们就可以看到为一些更复杂的类型提供基于功能的方便API的吸引力。例如,我们在这里构建了一个DocumentViewController-使用我们的Cache类型,以及基于核心动画的DocumentRenderer协议--在加载文档时,这两种协议现在都可以简单地作为函数调用:

class DocumentViewController: UIViewController {
private let cache: Cache
private let render: CoreAnimationDocumentRenderer

...

private func documentDidLoad(_ document: Document) {
cache(document)
render(document)
}
}

这很酷,特别是如果我们的目标是轻量级API设计或者如果我们在建造某种形式的领域专用语言。虽然通过传递实例方法来实现类似的结果一直是可能的好像它们是封闭的-通过允许直接调用我们的类型,我们都避免了手动传递这些方法,并且能够保留API可能使用的任何外部参数标签。

例如,假设我们还想做一个PriceCalculator变成一个可调用的类型。为了维护原始API的语义,我们将保留for外部参数标签,即使在声明callAsFunction执行情况-如下:

extension PriceCalculator {
func callAsFunction(for product: Product) -> Int {
calculatePrice(for: product)
}
}

下面是上述方法与存储对类型的引用的比较calculatePrice方法-请注意第一段代码是如何丢弃参数标签的,而第二段代码是如何保留参数标签的:

// Using a method reference:
let calculatePrice = PriceCalculator().calculatePrice
...
calculatePrice(product)

// Calling our type directly:
let calculatePrice = PriceCalculator()
...
calculatePrice(for: product)

让类型像函数一样被调用是一个非常有趣的概念,但也许更有趣的是,它还使我们能够走相反的方向--并将函数转换为适当的类型。

面向对象的函数式编程
虽然在许多函数式编程概念中有着巨大的威力,但当使用大量面向对象的框架(就像大多数Apple的框架一样)时,应用这些概念和模式往往是很有挑战性的。让我们看看SWIFT5.2的新可调用类型功能是否可以帮助我们改变这种状况。
后续精彩内容请转到我的博客继续观看

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

收起阅读 »

iOS- WMZDropDownMenu:App各种类型筛选菜单

软件介绍一个能几乎实现所有 App 各种类型筛选菜单的控件,可悬浮。目前已实现 闲鱼 / 美团 / Boss直聘 / 京东 / 饿了么 / 淘宝 / 拼多多 / 赶集网 / 美图外卖 等等的筛选菜单,可以自由调用代理实现自己想组装的筛选功能和 UI,且控件的生...
继续阅读 »

软件介绍

一个能几乎实现所有 App 各种类型筛选菜单的控件,可悬浮。目前已实现 闲鱼 / 美团 / Boss直聘 / 京东 / 饿了么 / 淘宝 / 拼多多 / 赶集网 / 美图外卖 等等的筛选菜单,可以自由调用代理实现自己想组装的筛选功能和 UI,且控件的生命周期自动管理,悬浮自动管理。

实现功能

  • 组合自定义功能
  • 支持自定义多选|单选|复选
  • 支持自定义弹出的动画 (目前已实现向下,向左全屏,向右全屏,拼多多对话框弹出,boss直聘全屏弹出)
  • 支持自定义tableView/collectionView头尾视图
  • 支持自定义全局头尾视图
  • 支持自定义collectionCell/tableViewCell视图
  • 支持自定义标题
  • 支持自定义点击回收视图
  • 支持自定义回收列表
  • 支持任意级的联动(由于数据比较庞杂,暂时自动适配不了无限级的联动,所以需要你调用一个方法更新数据传给我,详情看Demo)
  • 支持嵌套使用,即两个筛选菜单可以连着使用
  • 支持放在放在任意视图上,tableviewHeadView毫无疑问支持且无须写其他代码只要放上去即可
  • 支持控制器消失自动关闭视图,无须再控制器消失方法里手动关闭
  • 链式实现所有配置的自定义修改 (总之,你想要的基本都有,不想要的也有)

效果图


用法:

组装全在一些代理里,代理方法可能有点多~ ~,不过只有两个是必实现的,其他的都是可选的)

WMZDropMenuDelegate 
@required 一定实现的方法
*/
- (NSArray*)titleArrInMenu:(WMZDropDownMenu *)menu;
/*
*返回WMZDropIndexPath每行 每列的数据
*/
- (NSArray*)menu:(WMZDropDownMenu *)menu
dataForRowAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;
@optional 可选实现的方法
/*
*返回setion行标题有多少列 默认1列
*/
- (NSInteger)menu:(WMZDropDownMenu *)menu numberOfRowsInSection:
(NSInteger)section;
/*
*自定义tableviewCell内容 默认WMZDropTableViewCell 如果要使用默认的
cell返回 nil
*/
- (UITableViewCell*)menu:(WMZDropDownMenu *)menu
cellForUITableView:(WMZDropTableView*)tableView AtIndexPath:
(NSIndexPath*)indexpath dataForIndexPath:(WMZDropTree*)model;
/*
*自定义tableView headView
*/
- (UITableViewHeaderFooterView*)menu:(WMZDropDownMenu *)menu
headViewForUITableView:(WMZDropTableView*)tableView
AtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;
/*
*自定义tableView footView
*/
- (UITableViewHeaderFooterView*)menu:(WMZDropDownMenu *)menu
footViewForUITableView:(WMZDropTableView*)tableView
AtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;

/*
*自定义collectionViewCell内容
*/
- (UICollectionViewCell*)menu:(WMZDropDownMenu *)menu
cellForUICollectionView:(WMZDropCollectionView*)collectionView
AtDropIndexPath:(WMZDropIndexPath*)dropIndexPath AtIndexPath:
(NSIndexPath*)indexpath dataForIndexPath:(WMZDropTree*)model;
/*
*自定义collectionView headView
*/
- (UICollectionReusableView*)menu:(WMZDropDownMenu *)menu
headViewForUICollectionView:(WMZDropCollectionView*)collectionView
AtDropIndexPath:(WMZDropIndexPath*)dropIndexPath AtIndexPath:
(NSIndexPath*)indexpath;

/*
*自定义collectionView footView
*/
- (UICollectionReusableView*)menu:(WMZDropDownMenu *)menu
footViewForUICollectionView:(WMZDropCollectionView*)collectionView
AtDropIndexPath:(WMZDropIndexPath*)dropIndexPath AtIndexPath:
(NSIndexPath*)indexpath;

/*
*headView标题
*/
- (NSString*)menu:(WMZDropDownMenu *)menu
titleForHeadViewAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;
/*
*footView标题
*/
- (NSString*)menu:(WMZDropDownMenu *)menu
titleForFootViewAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;


/*
*返回WMZDropIndexPath每行 每列 indexpath的cell的高度 默认35
*/
- (CGFloat)menu:(WMZDropDownMenu *)menu heightAtDropIndexPath:
(WMZDropIndexPath*)dropIndexPath AtIndexPath:
(NSIndexPath*)indexpath;
/*
*自定义headView高度 collectionView默认35
*/
- (CGFloat)menu:(WMZDropDownMenu *)menu
heightForHeadViewAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;
/*
*自定义footView高度
*/
- (CGFloat)menu:(WMZDropDownMenu *)menu
heightForFootViewAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;

#pragma -mark 自定义用户交互的每行的头尾视图
/*
*自定义每行全局头部视图 多用于交互事件
*/
- (UIView*)menu:(WMZDropDownMenu *)menu
userInteractionHeadViewInSection:(NSInteger)section;
/*
*自定义每行全局尾部视图 多用于交互事件
*/
- (UIView*)menu:(WMZDropDownMenu *)menu
userInteractionFootViewInSection:(NSInteger)section;
#pragma -mark 样式动画相关代理
/*
*返回WMZDropIndexPath每行 每列的UI样式 默认MenuUITableView
注:设置了dropIndexPath.section 设置了 MenuUITableView 那么row则全部
为MenuUITableView 保持统一风格
*/
- (MenuUIStyle)menu:(WMZDropDownMenu *)menu
uiStyleForRowIndexPath:(WMZDropIndexPath*)dropIndexPath;
/*
*返回section行标题数据视图出现的动画样式 默认
MenuShowAnimalBottom
注:最后一个默认是筛选 弹出动画为 MenuShowAnimalRight
*/
- (MenuShowAnimalStyle)menu:(WMZDropDownMenu *)menu
showAnimalStyleForRowInSection:(NSInteger)section;
/*
*返回section行标题数据视图消失的动画样式 默认 MenuHideAnimalTop
注:最后一个默认是筛选 消失动画为 MenuHideAnimalLeft
*/
- (MenuHideAnimalStyle)menu:(WMZDropDownMenu *)menu
hideAnimalStyleForRowInSection:(NSInteger)section;
/*
*返回WMZDropIndexPath每行 每列的编辑类型 单选|多选 默认单选
*/
- (MenuEditStyle)menu:(WMZDropDownMenu *)menu
editStyleForRowAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;
/*
*返回WMZDropIndexPath每行 每列 显示的个数
注:
样式MenuUITableView 默认4个
样式MenuUICollectionView 默认1个 传值无效
*/
- (NSInteger)menu:(WMZDropDownMenu *)menu
countForRowAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;
/*
*WMZDropIndexPath是否显示收缩功能 default >参数
wCollectionViewSectionShowExpandCount 显示
*/
- (BOOL)menu:(WMZDropDownMenu *)menu
showExpandAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;

/*
*WMZDropIndexPath上的内容点击 是否关闭视图 default YES
*/
- (BOOL)menu:(WMZDropDownMenu *)menu
closeWithTapAtDropIndexPath:(WMZDropIndexPath*)dropIndexPath;

/*
*是否关联 其他标题 即选中其他标题 此标题会不会取消选中状态 default
YES 取消,互不关联
*/
- (BOOL)menu:(WMZDropDownMenu *)menu
dropIndexPathConnectInSection:(NSInteger)section;

#pragma -mark 交互自定义代理
/*
*cell点击方法
*/
- (void)menu:(WMZDropDownMenu *)menu
didSelectRowAtDropIndexPath:(WMZDropIndexPath *)dropIndexPath
dataIndexPath:(NSIndexPath*)indexpath data:(WMZDropTree*)data;
/*
*标题点击方法
*/
- (void)menu:(WMZDropDownMenu *)menu didSelectTitleInSection:
(NSInteger)section btn:(WMZDropMenuBtn*)selectBtn;
/*
*确定方法 多个选择
selectNoramalData 转化后的的模型数据
selectData 字符串数据
*/
- (void)menu:(WMZDropDownMenu *)menu didConfirmAtSection:
(NSInteger)section selectNoramelData:(
NSMutableArray*)selectNoramalData selectStringData:
(NSMutableArray*)selectData;


/*
*自定义标题按钮视图 返回配置 参数说明
offset 按钮的间距
y 按钮的y坐标 自动会居中
*/
- (NSDictionary*)menu:(WMZDropDownMenu *)menu
customTitleInSection:
(NSInteger)section withTitleBtn:(WMZDropMenuBtn*)menuBtn;

/*
*自定义修改默认collectionView尾部视图
*/
- (void)menu:(WMZDropDownMenu *)menu
customDefauultCollectionFootView:(WMZDropConfirmView*)confirmView;

下载地址:https://gitee.com/mirrors/WMZDropDownMenu

收起阅读 »

IOS-图片浏览之YBImageBrowser的简单使用

1.安装第一种方式 使用 cocoapodspod 'YBImageBrowser'    注意:请尽量使用最新版本(1.1.2);若搜索不到库,可使用rm ~/Library/Caches/CocoaPods/sear...
继续阅读 »

1.安装

第一种方式 使用 cocoapods

pod 'YBImageBrowser'    

注意:请尽量使用最新版本(1.1.2);若搜索不到库,可使用rm ~/Library/Caches/CocoaPods/search_index.json移除本地索引然后再执行安装,或者更新一下 cocoapods 版本。

第二种方式 手动导入

直接将该 Demo 的 YBImageBrowser 文件夹拖入你的工程中,并在你的 Podfile 里面添加:

pod 'SDWebImage', '~> 4.3.3'
pod 'FLAnimatedImage', '~> 1.0.12'


2.使用

我这里是采用代理数据源的方式,完整代码如下:

#import "ViewController.h"
#import "YBImageBrowser.h"
#import
@interface ViewController (){
NSArray *imageArray;
NSMutableArray *imageViewArray;
NSInteger currentIndex;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
imageViewArray = [[NSMutableArray alloc]init];
imageArray = @[
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118687954&di=d92e4024fe4c2e4379cce3d3771ae105&imgtype=0&src=http%3A%2F%2Fimg3.duitang.com%2Fuploads%2Fitem%2F201605%2F18%2F20160518181939_nCZWu.gif",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118772581&di=29b994a8fcaaf72498454e6d207bc29a&imgtype=0&src=http%3A%2F%2Fimglf2.ph.126.net%2F_s_WfySuHWpGNA10-LrKEQ%3D%3D%2F1616792266326335483.gif",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118803027&di=beab81af52d767ebf74b03610508eb36&imgtype=0&src=http%3A%2F%2Fe.hiphotos.baidu.com%2Fbaike%2Fpic%2Fitem%2F2e2eb9389b504fc2995aaaa1efdde71190ef6d08.jpg",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118823131&di=aa588a997ac0599df4e87ae39ebc7406&imgtype=0&src=http%3A%2F%2Fimg3.duitang.com%2Fuploads%2Fitem%2F201605%2F08%2F20160508154653_AQavc.png",
@"https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=722693321,3238602439&fm=27&gp=0.jpg",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118892596&di=5e8f287b5c62ca0c813a548246faf148&imgtype=0&src=http%3A%2F%2Fwx1.sinaimg.cn%2Fcrop.0.0.1080.606.1000%2F8d7ad99bly1fcte4d1a8kj20u00u0gnb.jpg",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118914981&di=7fa3504d8767ab709c4fb519ad67cf09&imgtype=0&src=http%3A%2F%2Fimg5.duitang.com%2Fuploads%2Fitem%2F201410%2F05%2F20141005221124_awAhx.jpeg",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118934390&di=fbb86678336593d38c78878bc33d90c3&imgtype=0&src=http%3A%2F%2Fi2.hdslb.com%2Fbfs%2Farchive%2Fe90aa49ddb2fa345fa588cf098baf7b3d0e27553.jpg",
@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524118984884&di=7c73ddf9d321ef94a19567337628580b&imgtype=0&src=http%3A%2F%2Fimg5q.duitang.com%2Fuploads%2Fitem%2F201506%2F07%2F20150607185100_XQvYT.jpeg"
];
[self initUI];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)initUI{
NSInteger rowCount = 3;
CGFloat width = self.view.bounds.size.width;
CGFloat imgW = width/rowCount;
CGFloat imgH = imgW;
CGFloat xPoint = 0;
CGFloat yPoint = 100;
NSInteger index = 0;
for (NSString *imgUrl in imageArray) {
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(xPoint, yPoint, imgW, imgH)];
button.userInteractionEnabled = YES;
button.tag = index;
//点击图片放大
[button addTarget:self action:@selector(imgViewClick:) forControlEvents:UIControlEventTouchUpInside];
UIImageView *img = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imgW, imgH)];
[button addSubview:img];

[img sd_setImageWithURL:[NSURL URLWithString:imgUrl] placeholderImage:[UIImage imageNamed:@"no_img.png"]];
[imageViewArray addObject:img];

xPoint += imgW;
if ((index+1)%rowCount==0) {
yPoint += imgH;
xPoint = 0;
}
[self.view addSubview:button];
index++;
}
}
-(void)imgViewClick:(UIButton *)btn{
currentIndex = btn.tag;
YBImageBrowser *browser = [YBImageBrowser new];
browser.dataSource = self;
browser.currentIndex = btn.tag;
//展示
[browser show];
}
//YBImageBrowserDataSource 代理实现赋值数据
- (NSInteger)numberInYBImageBrowser:(YBImageBrowser *)imageBrowser {
return imageArray.count;
}
- (YBImageBrowserModel *)yBImageBrowser:(YBImageBrowser *)imageBrowser modelForCellAtIndex:(NSInteger)index {
NSString *urlStr = [imageArray objectAtIndex:index];
YBImageBrowserModel *model = [YBImageBrowserModel new];
model.url = [NSURL URLWithString:urlStr];
//model.sourceImageView = [imageViewArray objectAtIndex:index];
return model;
}
- (UIImageView *)imageViewOfTouchForImageBrowser:(YBImageBrowser *)imageBrowser {
return [imageViewArray objectAtIndex:currentIndex];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end


3.效果


git地址:https://github.com/indulgeIn/YBImageBrowser


收起阅读 »

iOS- 多页面嵌套(JXPagerView、JXCategoryView)

目录 1. 示例 2. 详细说明Podfile中导入 pod 'JXPagingView/Pager' pod 'JXCategoryView'1. 示例VC// 头部View高#define JXTableHeaderViewHeight (kIs_...
继续阅读 »
目录
1. 示例
2. 详细说明
Podfile中导入
pod 'JXPagingView/Pager'
pod 'JXCategoryView'
1. 示例


VC

// 头部View高
#define JXTableHeaderViewHeight (kIs_iPhoneX?200+44:200)
// 菜单项View高
#define JXheightForHeaderInSection 40


#import <JXPagingView/JXPagerView.h>
#import <JXCategoryView/JXCategoryView.h>
<JXPagerViewDelegate, JXCategoryViewDelegate>

/**
顶部View(自定义View)
*/
@property (nonatomic,strong) ZYTeamplayerHeadView *teamplayerHeadV;
/**
菜单项View
*/
@property (nonatomic,strong) JXCategoryTitleView *categoryView;
/**
内容View
*/
@property (nonatomic, strong) JXPagerView *pagingView;
/**
内容View,建议这里使用控制器
*/
@property (nonatomic, strong) NSArray <ZYTeamplayerContentView *> *listViewArray;
/**
菜单项标题数组
*/
@property (nonatomic,copy) NSArray *itemArr;

-(void)viewDidLoad{
[super viewDidLoad];
[self.view addSubview:self.pagingView];
}

#pragma mark - JXPagingViewDelegate
/**
自定义头部视图
*/
- (UIView *)tableHeaderViewInPagerView:(JXPagerView *)pagerView {
return self.teamplayerHeadV;
}
/**
自定义头部视图高

@param pagerView pagerView
@return 头部视图高
*/
- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView {
return JXTableHeaderViewHeight;
}
/**
菜单项View

@param pagerView pagerView
@return 菜单项View
*/
- (UIView *)viewForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
return self.categoryView;
}
/**
菜单项View高

@param pagerView pagerView
@return 菜单项View高
*/
- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
return JXheightForHeaderInSection;
}
/**
内容子视图数组

@param pagerView pagerView
@return 内容子视图数组
*/
- (NSArray<UIView<JXPagerViewListViewDelegate> *> *)listViewsInPagerView:(JXPagerView *)pagerView {
return self.listViewArray;
}
/**
上下滚动后调用
*/
- (void)mainTableViewDidScroll:(UIScrollView *)scrollView {
//计算偏移量
CGFloat P = scrollView.contentOffset.y/(JXTableHeaderViewHeight-kNavBarAndStatusBarHeight);
}

#pragma mark - JXCategoryViewDelegate
/**
选中菜单项后调用

@param categoryView 菜单项View
@param index 下表
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index {
self.navigationController.interactivePopGestureRecognizer.enabled = (index == 0);
}
/**
滑动并切换内容视图后调用

@param categoryView 菜单项View
@param index 下表
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didScrollSelectedItemAtIndex:(NSInteger)index{
}


#pragma mark 懒加载
/**
总视图
*/
-(JXPagerView *)pagingView{
if(!_pagingView){
//
_pagingView = [[JXPagerView alloc] initWithDelegate:self];
_pagingView.frame = self.view.bounds;
}
return _pagingView;
}
/**
自定义头部视图
*/
-(ZYTeamplayerHeadView *)teamplayerHeadV{
if(!_teamplayerHeadV){
_teamplayerHeadV=[ZYTeamplayerHeadView new];
[_teamplayerHeadV setFrame:CGRectMake(0, 0, kScreenWidth, JXTableHeaderViewHeight)];
}
return _teamplayerHeadV;
}
/**
菜单项视图View
*/
-(JXCategoryTitleView *)categoryView{
if(!_categoryView){
//
_categoryView = [[JXCategoryTitleView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, JXheightForHeaderInSection)];
// dele
_categoryView.delegate = self;
// 设置菜单项标题数组
_categoryView.titles = self.itemArr;
// 背景色
_categoryView.backgroundColor = [UIColor whiteColor];
// 标题色、标题选中色、标题字体、标题选中字体
_categoryView.titleColor = kTitleColor;
_categoryView.titleSelectedColor = kTintClolor;
_categoryView.titleFont=kFont(16);
_categoryView.titleSelectedFont=kFontBold(16);
// 标题色是否渐变过渡
_categoryView.titleColorGradientEnabled = YES;

// 下划线
JXCategoryIndicatorLineView *lineView = [[JXCategoryIndicatorLineView alloc] init];
// 下划线颜色
lineView.indicatorLineViewColor = kTintClolor;
// 下划线宽度
lineView.indicatorLineWidth = 35;
_categoryView.indicators = @[lineView];

// 联动(categoryView和pagingView)
_categoryView.contentScrollView = self.pagingView.listContainerView.collectionView;
// 返回上一页侧滑手势(仅在index==0时有效)
self.navigationController.interactivePopGestureRecognizer.enabled = (_categoryView.selectedIndex == 0);
}

return _categoryView;
}
/**
内容视图数组
*/
-(NSArray<ZYTeamplayerContentView *> *)listViewArray{
if(!_listViewArray){
// 内容视图(通过PageType属性区分页面)
CGRect rect=CGRectMake(0, 0, kScreenWidth, kScreenHeight-kNavBarAndStatusBarHeight-JXTableHeaderViewHeight-JXheightForHeaderInSection);
ZYTeamplayerContentView *playerView = [[ZYTeamplayerContentView alloc] initWithFrame:rect];
[playerView setPageType:ZYTeamplayerContentViewTypePlayer];
ZYTeamplayerContentView *infoView = [[ZYTeamplayerContentView alloc] initWithFrame:rect];
[infoView setPageType:ZYTeamplayerContentViewTypeTeam];
_listViewArray = @[playerView, infoView];
}
return _listViewArray;
}
/**
菜单项标题数组
*/
-(NSArray *)itemArr{
if(!_itemArr){
_itemArr=@[@"球员",@"信息"];
}
return _itemArr;
}
添加下拉刷新

__weak typeof(self)weakSelf = self;
self.pagingView.mainTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 修改
// [self.categoryView reloadData];
// [self.pagingView reloadData];
[weakSelf.pagingView.mainTableView.mj_header endRefreshing];
});
}];

自定义内容视图View

#import "JXPagerView.h"

typedef enum{
ZYTeamplayerContentViewTypePlayer, // 球员
ZYTeamplayerContentViewTypeTeam, // 信息
}ZYTeamplayerContentViewType;

<JXPagerViewListViewDelegate>
/**
页面类型
*/
@property (nonatomic,assign) ZYTeamplayerContentViewType pageType;

@property (nonatomic, copy) void(^scrollCallback)(UIScrollView *scrollView);
// 必须加(用于联动)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
self.scrollCallback(scrollView);
}


#pragma mark - JXPagingViewListViewDelegate
- (UIView *)listView {
return self;
}
/**
返回一个可滚动的视图
*/
- (UIScrollView *)listScrollView {
return self.contentTableView;
}
/**
用于联动
*/
- (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback {
self.scrollCallback = callback;
}


-(void)layoutSubviews{
[self.contentTableView setFrame:self.bounds];
}
-(UITableView *)contentTableView{
if(!_contentTableView){
_contentTableView=[[UITableView alloc]initWithFrame:CGRectZero style:UITableViewStyleGrouped];
[_contentTableView setDelegate:self];
[_contentTableView setDataSource:self];
[_contentTableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
[_contentTableView setBackgroundColor:[UIColor whiteColor]];
[_contentTableView setContentInset:UIEdgeInsetsMake(0, 0, kNavBarAndStatusBarHeight, 0)]; //
[self addSubview:_contentTableView];
[_contentTableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.bottom.mas_equalTo(0);
}];
}
return _contentTableView;
}
2. 详细说明
菜单项

JXCategoryTitleView 文本菜单项

@interface JXCategoryTitleView : JXCategoryIndicatorView
/**
菜单项标题数组
*/
@property (nonatomic, strong) NSArray <NSString *>*titles;

/**
标题项标题行数 (默认:1)
*/
@property (nonatomic, assign) NSInteger titleNumberOfLines;
/**
标题项标题颜色
默认:[UIColor blackColor]
*/
@property (nonatomic, strong) UIColor *titleColor;
/**
标题项标题字体
默认:[UIFont systemFontOfSize:15]
*/
@property (nonatomic, strong) UIFont *titleFont;
/**
标题项标题选中颜色
默认:[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *titleSelectedColor;
/**
标题项标题选中字体
默认:[UIFont systemFontOfSize:15]
*/
@property (nonatomic, strong) UIFont *titleSelectedFont;

/**
默认:NO,title的颜色是否渐变过渡
*/
@property (nonatomic, assign) BOOL titleColorGradientEnabled;
/**
默认:NO,titleLabel是否遮罩过滤。
*/
@property (nonatomic, assign) BOOL titleLabelMaskEnabled;
//---------------titleLabelZoomEnabled(忽略选中后字体)------------------//

/**
默认为NO。
为YES时titleSelectedFont失效,以titleFont为准。
*/
@property (nonatomic, assign) BOOL titleLabelZoomEnabled;
/**
默认1.2。
titleLabelZoomEnabled为YES才生效。
是对字号的缩放,比如titleFont的pointSize为10,放大之后字号就是10*1.2=12。
*/
@property (nonatomic, assign) CGFloat titleLabelZoomScale; //
/**
手势滚动中,是否需要更新zoom状态。默认为YES
*/
@property (nonatomic, assign) BOOL titleLabelZoomScrollGradientEnabled;
//---------------titleLabelStrokeWidth(忽略选中后字体)--------------------//

/**
是否使用Stroke,用于控制字体的粗细(底层通过NSStrokeWidthAttributeName实现)
默认:NO
*/
@property (nonatomic, assign) BOOL titleLabelStrokeWidthEnabled;
/**
默认:-3。
使用该属性,务必让titleFont和titleSelectedFont设置为一样的!!!
*/
@property (nonatomic, assign) CGFloat titleLabelSelectedStrokeWidth;
//----------------------titleLabel缩放锚点中心位置-----------------------//

/**
titleLabel锚点位置(用于调整titleLabel缩放时的基准位置)

typedef NS_ENUM(NSUInteger, JXCategoryTitleLabelAnchorPointStyle) {
JXCategoryTitleLabelAnchorPointStyleCenter, 默认
JXCategoryTitleLabelAnchorPointStyleTop,
JXCategoryTitleLabelAnchorPointStyleBottom,
};
*/
@property (nonatomic, assign) JXCategoryTitleLabelAnchorPointStyle titleLabelAnchorPointStyle;
/**
titleLabel锚点垂直方向的位置偏移,数值越大越偏离中心,默认为:0
*/
@property (nonatomic, assign) CGFloat titleLabelVerticalOffset;
@end

JXCategoryImageView 图片菜单项

@interface JXCategoryImageView : JXCategoryIndicatorView

/**
未选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*imageNames;
/**
未选中图片源(url)
*/
@property (nonatomic, strong) NSArray <NSURL *>*imageURLs;
/**
选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*selectedImageNames;
/**
选中图片源(url)
*/
@property (nonatomic, strong) NSArray <NSURL *>*selectedImageURLs;
/**
使用imageURL从远端下载图片进行加载,建议使用SDWebImage等第三方库进行下载。
*/
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);

/**
图片大小
默认CGSizeMake(20, 20)
*/
@property (nonatomic, assign) CGSize imageSize;
/**
图片圆角
*/
@property (nonatomic, assign) CGFloat imageCornerRadius;
/**
是否使用缩放效果
默认为NO
*/
@property (nonatomic, assign) BOOL imageZoomEnabled;
/**
缩放比例
默认1.2,
imageZoomEnabled为YES才生效
*/
@property (nonatomic, assign) CGFloat imageZoomScale;
@end

JXCategoryTitleImageView 文本+图片 菜单项

@interface JXCategoryTitleImageView : JXCategoryTitleView
/**
未选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*imageNames;
/**
选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*selectedImageNames;
/**
未选中图片源(url)
通过loadImageCallback回调加载
*/
@property (nonatomic, strong) NSArray <NSURL *>*imageURLs;
/**
选中图片源(url)
通过loadImageCallback回调加载
*/
@property (nonatomic, strong) NSArray <NSURL *>*selectedImageURLs;
/**
图片源为url时使用
*/
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);

/**
默认@[JXCategoryTitleImageType_LeftImage...]
*/
@property (nonatomic, strong) NSArray <NSNumber *> *imageTypes;
/**
图片大小
默认CGSizeMake(20, 20)
*/
@property (nonatomic, assign) CGSize imageSize;
/**
titleLabel和ImageView的间距,默认5
*/
@property (nonatomic, assign) CGFloat titleImageSpacing;
/**
图片是否缩放。默认为NO
*/
@property (nonatomic, assign) BOOL imageZoomEnabled;
/**
图片缩放的最大scale。默认1.2,
imageZoomEnabled为YES才生效
*/
@property (nonatomic, assign) CGFloat imageZoomScale;
@end

JXCategoryNumberView 文本+数字 菜单项

@interface JXCategoryNumberView : JXCategoryTitleView
/**
需要与titles的count对应
*/
@property (nonatomic, strong) NSArray <NSNumber *> *counts;
/**
block内默认不会格式化数字,直接转成字符串显示。
如果业务需要数字超过999显示999+,可以通过该block实现。
*/
@property (nonatomic, copy) NSString *(^numberStringFormatterBlock)(NSInteger number);

/**
numberLabel的font
默认:[UIFont systemFontOfSize:11]
*/
@property (nonatomic, strong) UIFont *numberLabelFont;
/**
数字的背景色
默认:[UIColor colorWithRed:241/255.0 green:147/255.0 blue:95/255.0 alpha:1]
*/
@property (nonatomic, strong) UIColor *numberBackgroundColor;
/**
数字的title颜色
默认:[UIColor whiteColor]
*/
@property (nonatomic, strong) UIColor *numberTitleColor;
/**
numberLabel的宽度补偿,默认:10
总宽度=文字内容的宽度+补偿的宽度
*/
@property (nonatomic, assign) CGFloat numberLabelWidthIncrement;
/**
numberLabel的高度
默认:14
*/
@property (nonatomic, assign) CGFloat numberLabelHeight;
@end

JXCategoryDotView 文本+小角标 菜单项

@interface JXCategoryDotView : JXCategoryTitleView
/**
相对于titleLabel的位置,
默认:JXCategoryDotRelativePosition_TopRight
*/
@property (nonatomic, assign) JXCategoryDotRelativePosition relativePosition;

/**
@[@(布尔值)]数组,控制红点是否显示
*/
@property (nonatomic, strong) NSArray <NSNumber *> *dotStates;
/**
红点的尺寸。
默认:CGSizeMake(10, 10)
*/
@property (nonatomic, assign) CGSize dotSize;
/**
红点的圆角值。
默认:JXCategoryViewAutomaticDimension(self.dotSize.height/2)
*/
@property (nonatomic, assign) CGFloat dotCornerRadius;
/**
红点的颜色。
默认:[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *dotColor;
@end

JXCategoryIndicatorView 基类

@interface JXCategoryIndicatorView : JXCategoryBaseView
/**
下划线
*/
@property (nonatomic, strong) NSArray <UIView<JXCategoryIndicatorProtocol> *> *indicators;

//----------------------菜单项背景色-----------------------//
/**
是否开启背景色
默认:NO
*/
@property (nonatomic, assign) BOOL cellBackgroundColorGradientEnabled;
/**
未选中背景色
默认:[UIColor clearColor]
前提:cellBackgroundColorGradientEnabled为true
*/
@property (nonatomic, strong) UIColor *cellBackgroundUnselectedColor;
/**
选中背景色
默认:[UIColor grayColor]
前提:cellBackgroundColorGradientEnabled为true
*/
@property (nonatomic, strong) UIColor *cellBackgroundSelectedColor;

//----------------------separatorLine-----------------------//
/**
是否显示分割线。默认为NO
*/
@property (nonatomic, assign) BOOL separatorLineShowEnabled;
/**
分割线颜色。默认为[UIColor lightGrayColor]
前提;separatorLineShowEnabled为true
*/
@property (nonatomic, strong) UIColor *separatorLineColor;
/**
分割线的size
默认为CGSizeMake(1/[UIScreen mainScreen].scale, 20)
前提;separatorLineShowEnabled为true
*/
@property (nonatomic, assign) CGSize separatorLineSize;

/**
当contentScrollView滚动时候,处理跟随手势的过渡效果。
根据cellModel的左右位置、是否选中、ratio进行过滤数据计算。

@param leftCellModel 左边的cellModel
@param rightCellModel 右边的cellModel
@param ratio 从左往右方向计算的百分比
*/
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio NS_REQUIRES_SUPER;
@end

JXCategoryBaseView 基类

@interface JXCategoryBaseView : UIView
/**
菜单项视图
*/
@property (nonatomic, strong, readonly) JXCategoryCollectionView *collectionView;
/**
需要关联的内容视图
*/
@property (nonatomic, strong) UIScrollView *contentScrollView;

@property (nonatomic, strong) NSArray <JXCategoryBaseCellModel *> *dataSource;
/**
dele<JXCategoryViewDelegate>
*/
@property (nonatomic, weak) id<JXCategoryViewDelegate> delegate;
/**
初始化选中index
*/
@property (nonatomic, assign) NSInteger defaultSelectedIndex; //
/**
当前选中index(只读)
*/
@property (nonatomic, assign, readonly) NSInteger selectedIndex;
/**
默认为YES,
只有当delegate未实现`- (void)categoryView:(JXCategoryBaseView *)categoryView didClickedItemContentScrollViewTransitionToIndex:(NSInteger)index`代理方法时才有效
*/
@property (nonatomic, assign) BOOL contentScrollViewClickTransitionAnimationEnabled;

/**
整体左边距
默认JXCategoryViewAutomaticDimension(等于cellSpacing)
*/
@property (nonatomic, assign) CGFloat contentEdgeInsetLeft;
/**
整体右边距
默认JXCategoryViewAutomaticDimension(等于cellSpacing)
*/
@property (nonatomic, assign) CGFloat contentEdgeInsetRight;
/**
菜单项之间的间距
默认20
*/
@property (nonatomic, assign) CGFloat cellSpacing;
/**
当collectionView.contentSize.width小于JXCategoryBaseView的宽度,是否将cellSpacing均分。
默认为YES。
*/
@property (nonatomic, assign) BOOL averageCellSpacingEnabled;
/**
菜单项宽度
默认:JXCategoryViewAutomaticDimension
*/
@property (nonatomic, assign) CGFloat cellWidth;
/**
菜单项宽度补偿(总宽度=宽度+k补偿宽度)
默认:0
*/
@property (nonatomic, assign) CGFloat cellWidthIncrement;


//----------------cellWidthZoomEnabled(菜单项缩放)---------------//
/**
菜单项的宽度是否缩放
默认为NO
*/
@property (nonatomic, assign) BOOL cellWidthZoomEnabled;
/**
默认1.2,
cellWidthZoomEnabled为YES才生效
*/
@property (nonatomic, assign) CGFloat cellWidthZoomScale;
/**
手势滚动过程中,是否需要更新菜单项的宽度。
默认为YES
*/
@property (nonatomic, assign) BOOL cellWidthZoomScrollGradientEnabled;
/**
是否开启选中动画。
默认为NO。
自定义的菜单项选中动画需要自己实现。
*/
@property (nonatomic, assign) BOOL selectedAnimationEnabled;
/**
菜单项选中动画的时间。
默认0.25
*/
@property (nonatomic, assign) NSTimeInterval selectedAnimationDuration;
/**
选中目标index的item

@param index 目标index
*/
- (void)selectItemAtIndex:(NSInteger)index;
/**
初始化的时候无需调用。
重新配置categoryView,需要调用该方法进行刷新。
*/
- (void)reloadData;
/**
刷新指定的index的菜单项
内部会触发`- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index`方法进行cellModel刷新

@param index 指定cell的index
*/
- (void)reloadCellAtIndex:(NSInteger)index;


#pragma mark - Subclass use
- (CGRect)getTargetCellFrame:(NSInteger)targetIndex;
#pragma mark - Subclass Override
- (void)initializeData NS_REQUIRES_SUPER;
- (void)initializeViews NS_REQUIRES_SUPER;

/**
reloadData方法调用,重新生成数据源赋值到self.dataSource
*/
- (void)refreshDataSource;
/**
reloadData方法调用,根据数据源重新刷新状态;
*/
- (void)refreshState NS_REQUIRES_SUPER;
/**
reloadData时,返回每个菜单项的宽度

@param index 目标index
@return cellWidth
*/
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index;
/**
refreshState时调用,重置cellModel的状态

@param cellModel 待重置的cellModel
@param index cellModel在数组中的index
*/
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index;
/**
选中某个item时,刷新将要选中与取消选中的cellModel

@param selectedCellModel 将要选中的cellModel
@param unselectedCellModel 取消选中的cellModel
*/
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel NS_REQUIRES_SUPER;
/**
关联的contentScrollView的contentOffset发生了改变时调用

@param contentOffset 偏移量
*/
- (void)contentOffsetOfContentScrollViewDidChanged:(CGPoint)contentOffset NS_REQUIRES_SUPER;
/**
选中某一个item的时候调用,该方法用于子类重载。
如果外部要选中某个index,请使用`- (void)selectItemAtIndex:(NSUInteger)index;`

@param index 选中的index
@param selectedType JXCategoryCellSelectedType
@return 返回值为NO,表示触发内部某些判断(点击了同一个cell),子类无需后续操作。
*/
- (BOOL)selectCellAtIndex:(NSInteger)index selectedType:(JXCategoryCellSelectedType)selectedType NS_REQUIRES_SUPER;


/**
返回自定义菜单项的class

@return cell class
*/
- (Class)preferredCellClass;
@end

JXCategoryViewDelegate 协议

@protocol JXCategoryViewDelegate <NSObject>
@optional
/**
点击选中或者滚动选中都会调用该方法。
适用于只关心选中事件,不关心具体是点击还是滚动选中的。

@param categoryView categoryView description
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index;
/**
点击选中的情况才会调用该方法

@param categoryView categoryView description
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didClickSelectedItemAtIndex:(NSInteger)index;
/**
滚动选中的情况才会调用该方法

@param categoryView categoryView description
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didScrollSelectedItemAtIndex:(NSInteger)index;
/**
正在滚动中的回调

@param categoryView categoryView description
@param leftIndex 正在滚动中,相对位置处于左边的index
@param rightIndex 正在滚动中,相对位置处于右边的index
@param ratio 从左往右计算的百分比
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio;
@end
下划线

JXCategoryIndicatorLineView 直线

@interface JXCategoryIndicatorLineView : JXCategoryIndicatorComponentView
/**
lineStyle

JXCategoryIndicatorLineStyle_Normal = 0,默认
JXCategoryIndicatorLineStyle_Lengthen = 1,
JXCategoryIndicatorLineStyle_LengthenOffset = 2,
*/
@property (nonatomic, assign) JXCategoryIndicatorLineStyle lineStyle;
/**
line滚动时x的偏移量,默认为10;
lineStyle为JXCategoryIndicatorLineStyle_LengthenOffset有用;
*/
@property (nonatomic, assign) CGFloat lineScrollOffsetX;
/**
lineView的高度。
默认:3
*/
@property (nonatomic, assign) CGFloat indicatorLineViewHeight;
/**
lineView的宽度。
默认JXCategoryViewAutomaticDimension(与cellWidth相等)
*/
@property (nonatomic, assign) CGFloat indicatorLineWidth;
/**
lineView的圆角值。
默认JXCategoryViewAutomaticDimension (等于self.indicatorLineViewHeight/2)
*/
@property (nonatomic, assign) CGFloat indicatorLineViewCornerRadius;
/**
lineView的颜色。
默认为[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *indicatorLineViewColor;
@end

JXCategoryIndicatorTriangleView 三角形

@interface JXCategoryIndicatorTriangleView : JXCategoryIndicatorComponentView
/**
三角形的尺寸。
默认:CGSizeMake(14, 10)
*/
@property (nonatomic, assign) CGSize triangleViewSize;
/**
三角形的颜色值。
默认:[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *triangleViewColor;
@end



收起阅读 »

iOS-TZImagePickerController获取图片视频

TZImagePickerControllerDemo项目介绍TZImagePickerControllerDemoPodfile新增行 pod 'TZImagePickerController'使用之前导入头文件 #import <...
继续阅读 »

TZImagePickerControllerDemo

项目介绍

TZImagePickerControllerDemo

Podfile新增行 pod 'TZImagePickerController'

使用之前导入头文件 #import <TZImagePickerController.h>

使用说明

1. 定义 类变量:
  UIImagePickerController* picker_library_;
2.实现 UIImagePickerControllerDelegate 这个delegate,还需要UINavigationControllerDelegate 这个代理
3. 以模态的方式,显示 图片选取器

picker_library_ = [[UIImagePickerController alloc] init];  
picker_library_.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker_library_.allowsEditing = YES;
picker_camera_.allowsImageEditing=YES;
picker_library_.delegate = self;
[self presentModalViewController: picker_library_
animated: YES];

其中,sourceType 指定了 几种 图片的来源:
UIImagePickerControllerSourceTypePhotoLibrary:表示显示所有的照片
UIImagePickerControllerSourceTypeCamera:表示从摄像头选取照片
UIImagePickerControllerSourceTypeSavedPhotosAlbum:表示仅仅从相册中选取照片。
allowEditing和allowsImageEditing 设置为YES,表示 允许用户编辑图片,否则,不允许用户编辑。

选照片

//MaxImagesCount  可以选着的最大条目数
TZImagePickerController *imagePicker = [[TZImagePickerController alloc] initWithMaxImagesCount:1 delegate:self];

// 是否显示可选原图按钮
imagePicker.allowPickingOriginalPhoto = NO;
// 是否允许显示视频
imagePicker.allowPickingVideo = NO;
// 是否允许显示图片
imagePicker.allowPickingImage = YES;

// 这是一个navigation 只能present
[self presentViewController:imagePicker animated:YES completion:nil];

选择照片的回调

// 选择照片的回调
-(void)imagePickerController:(TZImagePickerController *)picker
didFinishPickingPhotos:(NSArray<UIImage *> *)photos
sourceAssets:(NSArray *)assets
isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto{

}

选视频

//MaxImagesCount  可以选着的最大条目数
TZImagePickerController *imagePicker = [[TZImagePickerController alloc] initWithMaxImagesCount:2 delegate:self];

// 是否显示可选原图按钮
imagePicker.allowPickingOriginalPhoto = NO;
// 是否允许显示视频
imagePicker.allowPickingVideo = YES;
// 是否允许显示图片
imagePicker.allowPickingImage = NO;

// 这是一个navigation 只能present
[self presentViewController:imagePicker animated:YES completion:nil];

选择视频的回调

// 选择视频的回调
-(void)imagePickerController:(TZImagePickerController *)picker
didFinishPickingVideo:(UIImage *)coverImage
sourceAssets:(PHAsset *)asset{

}
收起阅读 »

iOS-MBprogressHUD的使用

看开发文档中,涉及到六种基础的提示框typedef NS_ENUM(NSInteger, MBProgressHUDMode) { /**使用UIActivityIndicatorView显示进度。这是菊花默认值。 */ MBProgressHUDModeIn...
继续阅读 »

看开发文档中,涉及到六种基础的提示框

typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
/**使用UIActivityIndicatorView显示进度。这是菊花默认值。 */
MBProgressHUDModeIndeterminate,
/** 使用圆形的饼图来显示进度。 */
MBProgressHUDModeDeterminate,
/** 使用水平进度条显示进度 */
MBProgressHUDModeDeterminateHorizontalBar,
/** 使用圆环进度视图显示进度。*/
MBProgressHUDModeAnnularDeterminate,
/** 自定义的view*/
MBProgressHUDModeCustomView,
/** 仅显示标签 */
MBProgressHUDModeText
};

使用函数

+ (void)showToast:(NSString *)title withView:(UIView *)view {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.mode = MBProgressHUDModeIndeterminate;
hud.labelText = title;
[hud hide:YES afterDelay:1];
}

运行例子:


//1,设置背景框的透明度  默认0.8
hud.opacity = 1;

//2,设置背景框的背景颜色和透明度, 设置背景颜色之后opacity属性的设置将会失效
hud.color = [UIColor redColor];
hud.color = [HUD.color colorWithAlphaComponent:1];

//3,设置背景框的圆角值,默认是10
hud.cornerRadius = 20.0;

//4,设置提示信息 信息颜色,字体
hud.labelColor = [UIColor blueColor];
hud.labelFont = [UIFont systemFontOfSize:13];
hud.labelText = @"Loading...";

//5,设置提示信息详情 详情颜色,字体
hud.detailsLabelColor = [UIColor blueColor];
hud.detailsLabelFont = [UIFont systemFontOfSize:13];
hud.detailsLabelText = @"LoadingLoading...";

//6,设置菊花颜色 只能设置菊花的颜色
hud.activityIndicatorColor = [UIColor blackColor];

//7,设置一个渐变层
hud.dimBackground = YES;

//9,设置提示框的相对于父视图中心点的便宜,正值 向右下偏移,负值左上
hud.xOffset = -80;
hud.yOffset = -100;

//10,设置各个元素距离矩形边框的距离
hud.margin = 0;

//11,背景框的最小大小
hud.minSize = CGSizeMake(50, 50);

//12设置背景框的实际大小 readonly
CGSize size = HUD.size;

//13,是否强制背景框宽高相等
hud.square = YES;


收起阅读 »

WKWebView 使用问题整理

一. WKWebView处理window.open问题WKWebView加载页面, 当页面使用window.open跳转时候, 无响应, 需要实现WKUIDelegate协议实现-(WKWebView *)webView:(WKWebView *)webVie...
继续阅读 »

一. WKWebView处理window.open问题

  • WKWebView加载页面, 当页面使用window.open跳转时候, 无响应, 需要实现WKUIDelegate协议实现

-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{

WKFrameInfo *frameInfo = navigationAction.targetFrame;
if (![frameInfo isMainFrame]) {
//1. 本页跳转
[webView loadRequest:navigationAction.request];

//2. 获取url 打开新的 vc 实现跳转到新页面
//NSString *urlStr = [[navigationAction.request URL] absoluteString];
}
return nil;
}

注意 :
1- 使用 window.open 在移动端可能引发兼容问题, 建议前端对移动端标签使用location.href处理
2- ajax 处理window.open时候, 同步时可以响应跳转, 异步时不会响应跳转

$.ajax({
url: '',
async: true,
complete: function (xhr) {
window.open("http://www.baidu.com");
}
});

二. WKWebView处理a标签问题

方案1: 不建议使用

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
// 将a标签 跳转方式全部改为本页
[webView evaluateJavaScript:@"var aArr = document.getElementsByTagName('a');for(var i=0;i}
方案2: WKNavigationDelegate协议实现

-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// webview 本页重新加载
if (navigationAction.targetFrame == nil) {
[webView loadRequest:navigationAction.request];
}
decisionHandler(WKNavigationActionPolicyAllow);
return;

}
方案3: WKUIDelegate协议实现

-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{

WKFrameInfo *frameInfo = navigationAction.targetFrame;
if (![frameInfo isMainFrame]) {

// 可创建新页面打开 [WebView new]
// 也可重新加载本页面 [webView loadRequest:navigationAction.request];

}
return nil;

}

注意 : 如果方案2与方案3 代码中均实现, 程序会先执行方案2

三. WKWebView处理alert 问题

  • WKWebView加载页面, 当页面使用alert()、confirm()和prompt(),默认无响应. 若要正常使用这三个方法,需要实现WKUIDelegate中的三个方法模拟JS的这三个方法

JS 处理实现方法
function showAlert() {
alert("js_alertMessage");
}

function showConfirm() {
confirm("js_confirmMessage");
}

function showPrompt() {
prompt("js_prompt", "js_prompt_defaultMessage");
}

App 处理
//! alert(message)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
completionHandler();
}

//! confirm(message)
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
completionHandler();
}

//! prompt(prompt, defaultText)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {
completionHandler();
}

注意: completionHandler();需要被执行, 不然会引发crash.

四. WKWebView与JS简单交互

  • -WKWebView加载页面, 当需要给js简单交互, 可如下处理

// JS 处理
document.getElementById("btn").onclick = function () {

var url = "APP://action?params";
window.location.href = url;
}

// App 处理
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

if ([navigationAction.request.URL.scheme caseInsensitiveCompare:@"APP"] == NSOrderedSame) {
// 进行业务处理
decisionHandler(WKNavigationActionPolicyCancel);
}else{
if (navigationAction.targetFrame == nil) {
[webView loadRequest:navigationAction.request];
}
decisionHandler(WKNavigationActionPolicyAllow);
}
return;
}
// App 处理
NSString *func = [NSString stringWithFormat:@"loadData('%@', '%@')", @"aaa", @"bbb"];
[webView evaluateJavaScript:func completionHandler:nil];

// JS 处理
function loadData(action, params){

document.getElementById("returnValue").innerHTML = action + '?' + params;
}

注意:
1 webView调用 evaluateJavaScript:completionHandler:方法, 要确保前端的JS方法不在闭包中, 如window.onload = function() {} 中的方法就无法调用.
2 如果交互复杂 可以使用 WebViewJavascriptBridge 实现

五. WKWebView相关文档

WKWebView 那些坑

让 WKWebView 支持 NSURLProtocol

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



收起阅读 »

iOS面试题(四)

1. OC 的消息机制消息机制可以分为三个部分1. 消息传递当我么调用方法的时候,方法的调用都会转化为objc_msgSend这样来传递。第一步会根据对象的isa指针找到所属的类(也就是类对象)第二步,会根据类对象里面的catch里面查找。catch是个散列表...
继续阅读 »

1. OC 的消息机制

消息机制可以分为三个部分

1. 消息传递

  • 当我么调用方法的时候,方法的调用都会转化为objc_msgSend这样来传递。

  • 第一步会根据对象的isa指针找到所属的类(也就是类对象)

  • 第二步,会根据类对象里面的catch里面查找。catch是个散列表,是根据@selector(方法名)来获取对应的IMP,从而开始调用

  • 第三步,如果第二步没有找到,会继续查找到类对象里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面

  • 第四步,如果第三部也没有找到,会根据类对象里面的superclass指针,查找super的catch,如果也是没有查找,会继续查找到superclass里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面

  • 第五步,如果第四部还是没有查找到,此时会根据类的superclass,继续第四部操作

.......

  • 第六步。如果一直查找到基类都没有找到响应的方法,则会进入动态解析里面

2. 动态解析

  • 当消息传递,没有找到对应的IMP的时候,会进入的动态解析中

  • 此时会根据方法是类方法,还是实例方法分别调用+(BOOL)resolveClassMethod:(SEL)sel、+(BOOL)resolveInstanceMethod:(SEL)sel

  • 我们可以实现这两个方法,使用Runtime的class_addMethod来添加对应的IMP

  • 如果添加后,返回true,没有添加则调用父类方法

  • 注意:其实返回true或者false,结果都是一样的,再次掉消息传递步骤

3. 消息转发

  • 如果我们没有实现动态解析方法,就会走到消息转发这里

  • 第一步,会调用-(id)forwardingTargetForSelector:(SEL)aSelector方法,我们可以在这里,返回一个响应aSelector的对象。当返回不为nil时候,系统会继续再次走消息转发,继续查找对应的IMP

  • 第二步,如果第一步返回nil或者self(自己),此时系统会继续走这里-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,需要返回aSelector的一个签名

  • 第三步,如果返回了签名,就会到这里-(void)forwardInvocation:(NSInvocation *)anInvocation,相应的我们可以根据anInvocation,可以获取到参数、target、方法名等,再次操作的空间就很多了,看你需求喽。此时我们什么都不操作也是没问题的,

  • 注意:当我们是类方法的时候,其实我们可以将以上方法的-改为+,即可实现了类方法的转发


2.weak表是如何存储__weak指针的

  • weak关键字,我们都知道,当对象销毁的时候,也会将指针赋值为nil,而weak的底层也是将指针和对象以键值对的形式存储在哈希表里面

  • 当使用__weak修饰的时候,底层会调用id objc_storeWeak(id *location, id newObj)传递两个参数

        第一个参数为指针,第二个参数为所指向的对象

  • 第二步,继续调用storeWeak(location, (objc_object *)newObj)

     1. 第一个参数是指针,第二个参数是对象的地址

     2. 再次方法里面会根据对象地址生成一个SideTables对象

  • 第三步,调用id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)

     1. weak_table则为SideTables的一个属性,referent_id为对象,referrer_id则为那个弱引用的指针

     2. 在此里面会根据对象地址和指针生成一个weak_entry_t

  • 第四步,会继续调用static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)

     重点:在此方法里面会根据对象 & weak_table->mask(表示weak表里面可以存储的大小减一,例如:表可以存储10个对象,那么mask就是9), 生成对应的index,如果index对应已经存储上对象,则会index++的方式找到未存储的对应,并将new_entry存储进去,储存在weak_table里的weak_entries属                    性里面

  • 注意:当一个对象多个weak指针指向的时候,生成的也是一个entry,多个指针时保存在entry里面referrers属性里面

  • 以下为简易的源码:

id
objc_storeWeak(id *location, id newObj)
{
return storeWeak
(location, (objc_object *)newObj);
}
static id
storeWeak(id *location, objc_object *newObj) {
// 根据对象生成新的SideTable
SideTable *newTable = &SideTables()[newObj];
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
}
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating){
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

// 根据对象和指针生成一个entry
weak_entry_t new_entry(referent, referrer);
// 检查是是否该去扩容
weak_grow_maybe(weak_table);
// 将新的entry 插入到表里面
weak_entry_insert(weak_table, &new_entry);
}
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;

size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
}

weak_table的扩容,根据存储条数 >= 最大存储条数的3/4时,就会按照两倍的方式进行扩容,并且会将已经有的条目再次生成新的index(因为扩容后,weak_table的mask发生了改变)。进行保存

  • 以下为简易的源码:

static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = (weak_table->mask ? weak_table->mask + 1 : 0);
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
// calloc 分配新的控件
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
// mask 就是大小减一
weak_table->mask = new_size - 1;
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
weak_entry_insert(weak_table, entry);
}
}
}

3. 方法catch表是如何存储方法的

  • 我们都是知道调用方法的时候,会根据对象的isa查找到对象类对象,并开始在catch表里面查询对应的IMP

  • 其实catch是个散列表,是根据方法的@selector(方法名) & catch->mask(catck表最大数量 - 1)得到index,如果index已经存储了新的方法,那么就会index++,如果index对应的值为nil时,将响应的方法,插入到catch表里面

  • 核心代码

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
// 获取类对象的catch地址
cache_t *cache = &cls->cache
// 获取key
cache_key_t key = (cache_key_t)sel;
// 找到bucket
bucket_t *bucket = cache->find(key, receiver);
}

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
// catch表的buckets属性
bucket_t *b = buckets();
// catch 表示的mask 最大值 - 1
mask_t m = mask();

mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}

注意:catch表的扩容,同样也是和weak_table一样按照2倍的方式进行扩容,但是注意:扩容后,以前缓存的方法则会被删除掉。

简易代码

void cache_t::expand() {
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
reallocate(oldCapacity, newCapacity);
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 获取旧的oldBuckets
bucket_t *oldBuckets = buckets();
// 重新分配新的
bucket_t *newBuckets = allocateBuckets(newCapacity);
// free 掉旧的
cache_collect_free(oldBuckets, oldCapacity);
}

4. 优化后isa指针是什么样的?存储都有哪些内容?

  • 最新的Objective-C的对象里面的isa指针已经不是单单的指向所属类的地址了的指针了,而时变成了一个共用体,并且使用位域来存储更多的信息


5. App启动流程,以及如何优化?

  • 启动顺序

     1. dyld,Apple的动态连接器,可以用来装载Mach-O文件(可执行文件、动态库)

       1.1、装载App的可执行文件,同事递归加载所有依赖的动态库

       1.2 、当dyld把可执行文件、动态库装载完毕后,会通知Runtime进行下一步的处理

  • Runtime

     1. 调用map_images进行可执行文件内容的解析和处理

     2. 在load_images里面调用call_load_methods,调用所有class和category的+load方法

     3. 进行各种objc结构的初始化(注册Objc类,初始化类对象等等)

     4. 到目前未知,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP..)都已经按照格式成功加载到内存中,被runtime管理

  • main函数调用

     1. 所有初始化工作结束后,dyld就会调用main函数

     2. 截下来就是UIApplicationMan函数,AppDelegate的application:didFinishLaunchingWithOptions:的

  • App启动速度优化

      1. dyld

       1.1、减少动态库,合并一些自定义的动态库,以及定期清理一些不需要的动态库

       1.2、较少Objc类、category的数量、以及定期清理一些不必要的类和分类

       1.3、Swift尽量使用struct

     2. Runtime

       2.1、使用+initialize和dispatch_once取代Objc的+load方法、C++的静态构造器

     3. main

       3.1、再不印象用户体验的情况下面,尽可能的将一些操作延迟,不要全部放到finishLaunching

       3.2、一些网络请求

       3.3、一些第三方的注册

       3.4、以及window的rootViewController 的viewDidload方法,也别做耗时操作

     4. 注意:我们可以添加环境变量可以打印出App的启动时间分析(Edit scheme -> Run -> Arguments)

       4.1、DYLD_PRINT_STATISTICS设置为1,可以打印出来每个阶段的时间

       4.2、如果需要更详细的信息,那就设置DYLD_PRINT_STATISTICS_DETAILS为1


6. App瘦身

  • 资源(图片、音频、视频等)

    1. 可以采取无损压缩

     2. 使用LSUnusedResources去除没有用的资源 LSUnusedResources

  • 可执行文件瘦身

     1. Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为true

     2. 去掉一些异常支持 Enable C++ Exceptions、Enable Objective-C Exceptions设置为false

     3. 使用AppCode检测未使用的代码:菜单栏 -> Code -> Inspect Code,等编译完成后,会看到未使用的类

  • 生成LinkMap文件,可以查看可执行文件的具体组成

     1. 可借助第三方工具解析LinkMap文件LinkMap


     Link Map解析结果




收起阅读 »

iOS面试题(三)

1. ARC帮我们做了什么?使用LLVM + Runtime 结合帮我管理对象的生命周期LLVM 帮我们在代码合适的地方添加release、retarn、autorelease等添加计数器或者减少计数器操作Runtime 帮我们像__weak、copy等关键字...
继续阅读 »

1. ARC帮我们做了什么?

  • 使用LLVM + Runtime 结合帮我管理对象的生命周期

  • LLVM 帮我们在代码合适的地方添加release、retarn、autorelease等添加计数器或者减少计数器操作

  • Runtime 帮我们像__weak、copy等关键字的操作

2.initialize和load是如何调用的?它们会多次调用吗?

  • load方法说在应用加载的时候,Runtime直接拿到load的IMP直接去调用的,而不是像其他方式根据objc_msgSend(消息机制)来调用方法的

  • load方法调用的顺序是根据类的加载的前后进行调用的,但是每个类调用的顺序是superclass->class->category顺序调用的,每个load方法只会调用一次(手动调用不算)

  • 一下为Runtime源码的主要代码:

load_images(const char *path __unused, const struct mach_header *mh) {
// 准备classcategory
prepare_load_methods((const headerType *)mh);
// 调用load方法
call_load_methods();
}

void prepare_load_methods(const headerType *mhdr) {
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
add_category_to_loadable_list(cat);
}
}

static void schedule_class_load(Class cls) {
// 开始递归,加载superclass
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
}

void call_load_methods(void) {
do {
while (loadable_classes_used > 0) {
call_class_loads();
}
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
}

static void call_class_loads(void) {
// 在此add_class_to_loadable_list 里面准备了所有重写load的方法的类
struct loadable_class *classes = loadable_classes;
// Call all +loads for the detached list.
for ( int i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 获取到load 方法的imp
load_method_t load_method = (load_method_t)classes[i].method;
// 调用laod 方法
(*load_method)(cls, SEL_load);
}
}

static bool call_category_loads(void) {
// 在prepare_load_methods 方法里面准备了所有重新load方法的category
struct loadable_category *cats = loadable_categories;
for (int i = 0; i < used; i++) {
// 获取到catgegory
Category cat = cats[i].cat;
// 获取category 的load 方法的IMP实现
load_method_t load_method = (load_method_t)cats[i].method;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
// 调用load方法
(*load_method)(cls, SEL_load);
}
}
}
  • initialize方法的调用其实和其他方法调用一样的,objc_msgSend(消息机制)来调用的。调用的数序是:没有初始话的superclass -> 实现initialize的categort 或者 实现了initialize的class,如果class没有实现initialize 方法,则会调用superclass的initialize,因为initialize的底层是使用了objc_msgSend

  • 看下Runtime底层调用_class_initialize的源码

void _class_initialize(Class cls) {
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
// 又是个递归
_class_initialize(supercls);
}
// 调用 initialize方法
callInitialize(cls);
}
// objc_msgSend 调用 initialize 方法
void callInitialize(Class cls) {
// **注意:因为使用了objc_msgSend,有可能调用class的 initialize **
objc_msgSend(cls, SEL_initialize);
}

总结:
load方法一个类只会调用一次(除去手动调用),而调用的数序是,从superclass -> class -> category,category里面的顺序是先编译,先调用
initialize方法,一个类可能会调用多次,如果子类没有实现initialize方法,当第一次使用此类的时候,会调用superclass。而调用的顺序是,superclass -> 实现initialize的category 或者 实现了initialize方法(没有category实现initialize) 或者 superclass的initialize (没有子类和category实现initialize方法)

3.说下autoreleasepool

  • 在MRC下,当对象调用autorerelease方法时候,会将对象加入到对象前面的哪一个autoreleasepool里面,并且当autoreleasepool作用域释放的时候,会对里面的所有的对象进行一次release操作。

  • autoreleasepool底层是使用了AutoreleasePoolPage对象来管理的,AutoreleasePoolPage是一个双向的链表,每个AutoreleasePoolPage都有4096个字节,除了用来存放内部的成员变量,剩下的控件都会用来存放autorelease对象的地址

/// AutoreleasePoolPage 的简化的结构
class AutoreleasePoolPage {
magic_t const magic;
// 下一次可以存储对象的地址
id *next;
pthread_t const thread;
// 标识上一个page对象
AutoreleasePoolPage * const parent;
// 标识下一个page对昂
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
  • 当autoreleasepool开始的时候,会调用AutorelasePoolPage的push方法,会讲一个标识POOL_BOUNDARY添加到AutoreleasePoolPage对象里面,并且返回POOL_BOUNDARY的地址r1(暂且这样叫)

  • 当对像进行relase的时候,会将对象的地址添加到当前AutorelasePoolPage里面,依次添加。

  • 当autoreleasepool作用域结束的时候,会调用AutorelasePoolPage的pop(r1)方法(r1为当前aotoreleasepool开始的加入标识POOL_BOUNDARY的地址),AutorelasePoolPage则会将里面保存的对象的从左后一个开始进行release操作,当碰到r1时候,标识当前那个autoreleasepool里面所有的对象都进行了一次release操作。

@autoreleasepool {
// 此处会调用
void *ctxt = AutoreleasePoolPage::push();
// 添加到最近的一个autoreleasepool中
[[[NSObject alloc]init] autorelease];
//移除作用域的时候调用
AutoreleasePoolPage:pop(ctxt)
}
// autoreleasepool 作用域开始会调用AutoreleasePoolPage::push()
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
// 创建一个心的page对象
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
// 已经有了page对象,讲`pool_boundary`添加进去
dest = autoreleaseFast(POOL_BOUNDARY);
}
}
static inline id *autoreleaseFast(id obj)
{
// 获取正在使用的page对昂
AutoreleasePoolPage *page = hotPage();
// page还没有装满
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
// 已经添加满了
return autoreleaseFullPage(obj, page);
} else {
// 没有page对象,创建心的page对象
return autoreleaseNoPage(obj);
}
}
// 对象调用release 的简介源码
id objc_object::rootAutorelease2() {
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj) {
// 同样也是添加进去
id *dest = autoreleaseFast(obj);
return obj;
}
// page调用pop简介源码 *token 表示结束的标识
static inline void pop(void *token) {
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
page->releaseUntil(stop);
}
// 释放对象的源码
void releaseUntil(id *stop) {
// next 标识当前page可以存储对象的下一个地址
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
// 因为page是个双向链表,当page为空的时候,需要往上查找parent的page对象里面存储的睇相
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
id obj = *--page->next;
if (obj != POOL_BOUNDARY) {// obj 不是刚开始传入的POOL_BOUNDARY及表示对象,所以需要调用一次操作
objc_release(obj);
}
}
}


autoreleasepool和runloop的关系

  • runloop里面会注册两个Observer来监听runloop的状态变化

  • 其中一个Observer监听的状态为kCFRunLoopEntry进入runloop的状态,则会调用AutoreleasePoolPage::push()方法

  • 另外中一个Observer监听的状态为kCFRunLoopBeforeWaiting、kCFRunLoopExit,即将休眠和退出当前的runloop。

  • 在kCFRunLoopBeforeWaiting的回掉里面会调用AutoreleasePoolPage::pop(ctxt)和AutoreleasePoolPage::(push)方法,释放上一个autoreleasepool里面添加的对象,并开启下一个autoreleasepool。

  • 在kCFRunLoopExit的Observer回掉里面会调用AutoreleasePoolPage::(push)释放autoreleasepool里面的对象

4.category属性是存储在那里?

  • 我们都知道可以使用Runtime的objc_setAssociatedObject、objc_getAssociatedObject两个方法给category的属性重写get、set方法,而此属性的值是存储在那里呢?

  • 其实此属性的值保存在一个AssociationsManager里面。

  • 我们也是可以根据源码看一下

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// 一下为精简的代码
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
}
}
}


5.category方法是如何添加的?

  • 当我们给分类添加相同的方法的时候,会调用category里面的方法,而不是调用我们class里面的方法

  • 当编译器编译的时候,编译器会将category编译成category_t这样的结构体,等类初始化的时候,会将分类的信息同步到class_rw_t里面,包含:method、property、protocol等,同步的时候会将category里面的信息添加到class的前面(而不是替换掉class里面的方法),而方法调用的时候,而是遍历class_rw_t里面的方法,所以找到分类里面的IMP则返回。 

  • 使用memmove,将类方法移动到后面
  • 使用memcpy,将分类的方法copy到前面
  • 当多个分类有相同的方法的时候,调用的顺序是后编译先调用
  • 当类初始化同步category的时候,会使用while(i--)的倒序循环,将后编译的category添加到最前面。
         
收起阅读 »

iOS-一些常用第三方资源

一:第三方插件1:基于响应式编程思想的oc地址:https://github.com/ReactiveCocoa/ReactiveCocoa2:hud提示框地址:https://github.com/jdg/MBProgressHUD3:XML/HTML解析地...
继续阅读 »

一:第三方插件

1:基于响应式编程思想的oc

地址:https://github.com/ReactiveCocoa/ReactiveCocoa

2:hud提示框

地址:https://github.com/jdg/MBProgressHUD

3:XML/HTML解析

地址:https://github.com/topfunky/hpple

4:有文字输入时,能根据键盘是否弹出来调整自身显示内容的位置

地址:https://github.com/michaeltyson/TPKeyboardAvoiding

5:状态栏提示框

地址:https://github.com/jaydee3/JDStatusBarNotification

6:block工具包。将很多需要用delegate实现的方法整合成了block的形式

地址:https://github.com/zwaldowski/BlocksKit

7:图片加载

地址:https://github.com/rs/SDWebImage

8:正则表达式

地址:https://github.com/wezm/RegexKitLite

9:Masonry代码布局

地址:https://github.com/SnapKit/Masonry

10:弹出窗

地址:https://github.com/sberrevoets/SDCAlertView

11:Button的样式

地址:https://github.com/mattlawer/BButton

12:验证网络连接状态

地址:https://github.com/tonymillion/Reachability

13:自动计算表格行高

地址:https://github.com/forkingdog/UITableView-FDTemplateLayoutCell

14:动画效果的启动页

地址:https://github.com/IFTTT/JazzHands

15:iOS快速简单集成国内三大平台分享

地址:https://github.com/xumeng/XMShareModule

16:五项能力值展示的五边形

地址:https://github.com/dsxNiubility/SXFiveScoreShow

17:自动识别网址号码邮箱和表情的label

地址:https://github.com/molon/MLEmojiLabel

18:IM对话功能的封装

地址:https://github.com/ZhipingYang/UUChatTableView

19:字典转模型框架

地址:https://github.com/CoderMJLee/MJExtension

20:下拉上拉刷数据

地址:https://github.com/CoderMJLee/MJRefresh

21:表格行左右划动菜单

地址:https://github.com/MortimerGoro/MGSwipeTableCell

22:图文混搭

地址:https://github.com/zhouande/TLAttributedLabel

23:可以简单展示在UINavigationBar下方,类似Music app的播放列表视图,弹出菜单视图

地址:https://github.com/DrummerB/BFNavigationBarDrawer

24:比如筛选、模糊、优化、蒙版、调整大小、旋转以及保存等等。同时还提供了一个UIImageView子类从URL异步加载图片,并在下载完毕时展示图片。

地址:https://github.com/Nyx0uf/NYXImagesKit

25:底部TabBar

地址:https://github.com/robbdimitrov/RDVTabBarController

26:表情面版

地址:https://github.com/ayushgoel/AGEmojiKeyboard

27:记录框架

地址:https://github.com/CocoaLumberjack/CocoaLumberjack

28:IOS与javascript交互

地址:https://github.com/marcuswestin/WebViewJavascriptBridge

29:图表统计展示

地址:https://github.com/kevinzhow/PNChart

30:appStore评分

地址:https://github.com/arashpayan/appirater

31:iOS-Categories 扩展类大全

地址:https://github.com/shaojiankui/IOS-Categories

32:扫描二维码,仿微信效果,带有扫描条

地址:https://github.com/JxbSir/JxbScanQR

33:动效弹出视图(弹出窗里面为文字,可以定义弹出的方向,及显示的时间)--AMPopTip

地址:https://github.com/andreamazz/AMPopTip

34:基于Masonry自动计算行高扩展

地址:https://github.com/632840804/HYBMasonryAutoCellHeight

 35:模仿新浪微博弹出菜单

地址:https://github.com/wwdc14/HyPopMenuView

 36:搜索历史标签

地址:https://github.com/zhiwupei/SearchHistory

 37:快速集成新手引导的类库

地址:https://github.com/StrongX/XSportLight

38:设置页面的封装

地址:https://github.com/renzifeng/ZFSetting

39:带箭头的弹出视图插件

地址:https://github.com/xiekw2010/DXPopover

40:下拉菜单插件

地址:https://github.com/dopcn/DOPDropDownMenu/

41:表格空白提示插件

地址:https://github.com/dzenbot/DZNEmptyDataSet

42:给任意UIView视图四条边框加上阴影,可以自定义阴影的颜色、粗细程度、透明程度以及位置(上下左右边框)

地址:https://github.com/Seitk/UIView-Shadow-Maker

43:不错的日期时间插件

地址:https://github.com/CoderXL/UUDatePicker

44:底部弹出选择

地址:https://github.com/skywinder/ActionSheetPicker-3.0

45:比较不错的引导页面插件

地址:https://github.com/ealeksandrov/EAIntroView

46:两个APP跳转的插件

地址:https://github.com/usebutton/DeepLinkKit

47:本地存取NSUserDefaults插件

地址:https://github.com/gangverk/GVUserDefaults

48:NSArray 和 NSDictionary关于LINQ的操作方式,封装一些常用的操作

地址:https://github.com/ColinEberhardt/LinqToObjectiveC

49:可以监控网络请求的内容

地址:https://github.com/coderyi/NetworkEye

50:时间帮助插件,可以快速获取时间,比较,增加等操作

地址:https://github.com/MatthewYork/DateTools

51: 不错的链式动作

地址:https://github.com/jhurray/JHChainableAnimations

52:弹出层视图,背景效果(可以自定义视图的内容)

地址:https://github.com/HJaycee/JCAlertView

53:圆形进度条的显示,中间可显示值

地址:https://github.com/mdinacci/MDRadialProgress

54:很帅的数据加载动画(可以用于数据列表加载的展现)

地址:https://github.com/NghiaTranUIT/FeSpinner 

55:一个开源的AFnetworking上层的封装(猿题库等运用)

地址:https://github.com/yuantiku/YTKNetwork

56:CBStoreHouseRefreshControl:一个效果很酷炫的下拉刷新控件

地址:https://github.com/coolbeet/CBStoreHouseRefreshControl

57:AFNetworking-RACExtensions:针对ReactiveCocoa的AF封装

地址:https://github.com/CodaFi/AFNetworking-RACExtensions

58:模糊效果(毛玻璃)

地址:https://github.com/nicklockwood/FXBlurView

二:源代码实例

1:Coding.net客户端

地址:https://coding.net/u/coding/p/Coding-iOS/git

2:高仿美团iOS版

地址:https://github.com/lookingstars/meituan

3:模仿网易新闻做的精仿网易新闻

地址:https://github.com/dsxNiubility/SXNews

4:支付宝高仿版

地址:https://github.com/gsdios/GSD_ZHIFUBAO

5:高仿百度传课iOS版

地址:https://github.com/lookingstars/chuanke

6:模仿一元云购

地址:https://github.com/JxbSir/YiYuanYunGou

7:wordpress源代码

地址:https://github.com/wordpress-mobile/WordPress-iOS

8:v2ex源代码(文章类型,若报SVProgressHUD错,则把Podfile中的SVProgressHUD移除)

地址:https://github.com/singro/v2ex

9:PHPHub客户端(IOS8.0以上)

地址:https://github.com/Aufree/phphub-ios

10:快速搭建项目源代码

地址:https://github.com/wujunyang/MobileProject

三:辅助软件

1:XCODE文档注解插件VVDocumenter

地址:https://github.com/onevcat/VVDocumenter-Xcode

2:将JSON格式化输出为模型的属性

地址:https://github.com/EnjoySR/ESJsonFormat-Xcode

3:图片提示插件

地址:https://github.com/ksuther/KSImageNamed-Xcode

4:图片转换插件

地址:https://github.com/rickytan/RTImageAssets


收起阅读 »

ios-本地存储的五种方式

ios数据存储的5种方式NSUserDefaults(Preference偏好设置)plist存储归档SQLite3CoreData应用沙盒Document:适合存储重要的数据, iTunes同步应用时会同步该文件下的内容,(比如游戏中的存档)Library/...
继续阅读 »

ios数据存储的5种方式

  1. NSUserDefaults(Preference偏好设置)
  2. plist存储
  3. 归档
  4. SQLite3
  5. CoreData

应用沙盒

Document:适合存储重要的数据, iTunes同步应用时会同步该文件下的内容,(比如游戏中的存档)
Library/Caches:适合存储体积大,不需要备份的非重要数据,iTunes不会同步该文件
Library/Preferences:通常保存应用的设置信息, iTunes会同步
tmp:保存应用的临时文件,用完就删除,系统可能在应用没在运行时删除该目录下的文件,iTunes不会同步

获取沙盒路径

Document:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject;

NSuserDefault

NSuserDefault适合存储轻量级的本地数据,支持的数据类型有:NSNumber,NSString,NSDate,NSArray,NSDictionary,BOOL,NSData

沙盒路径为 Library/Preferences
文件格式为 .plist

优点:

  1. 不需要关心文件名
  2. 快速进行键值对存储
  3. 直接存储基本数据类型

缺点:

  1. 不能存储自定义数据
  2. 取出的数据都是不可变的
- (IBAction)userDefaultSave:(id)sender {
NSArray *testArray = @[@"test1", @"test2", @"test3"];
[[NSUserDefaults standardUserDefaults] setObject:testArray forKey:@"arrayKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (IBAction)userDefaultLoad:(id)sender {
NSArray *testArray = [[NSUserDefaults standardUserDefaults] objectForKey:@"arrayKey"];
NSLog(@"%@", testArray);
}

plist存储

plist支持的数据类型:

NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
不支持BOOL
而且最外层好像要用`NSArray 或 NSDictionary,偷个懒还没验证

- (IBAction)plistSave:(id)sender {
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [cachePath stringByAppendingPathComponent:@"testPlist.plist"];

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@"ran" forKey:@"name"];
[dict setObject:@"18" forKey:@"age"];
[dict writeToFile:filePath atomically:YES];
}

- (IBAction)plistLoad:(id)sender {
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [cachePath stringByAppendingPathComponent:@"testPlist.plist"];

NSDictionary *t = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(@"%@",t);
}

归档

存储自定义对象

  1. 首先新建Person类,并遵守NSCoding协议
@interface Person : NSObject<NSCoding>

@property(nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *age;

@end

实现协议方法:

@implementation Person

- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
_name = [coder decodeObjectForKey:@"name"];
_age = [coder decodeObjectForKey:@"age"];
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{

[coder encodeObject:self.name forKey:@"name"];
[coder encodeObject:self.age forKey:@"age"];

}
@end

归档解档

- (IBAction)archive:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];

Person *p1 = [[Person alloc] init];
p1.name = @"ran";
p1.age = @"18";

[NSKeyedArchiver archiveRootObject:p1 toFile:filePath];
}

- (IBAction)unarchive:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];

Person *p1 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath] ;

NSLog(@"%@", p1.name);
NSLog(@"%@", p1.age);
}

但是这种方法只能存储一个对象,存储多个对象要采用如下的方法:

- (IBAction)archiveManyObject:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];

NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; //将数据区连接到NSKeyedArchiver对象

Person *p1 = [[Person alloc] init];
p1.name = @"ran1";
p1.age = @"18";
[archiver encodeObject:p1 forKey:@"person1"];

Person *p2 = [[Person alloc] init];
p2.name = @"ran2";
p2.age = @"19";
[archiver encodeObject:p2 forKey:@"person2"];

[archiver finishEncoding];

[data writeToFile:filePath atomically:YES];
}

- (IBAction)unarchiveManyObject:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
NSData *data = [NSData dataWithContentsOfFile:filePath];

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *p1 = [unarchiver decodeObjectForKey:@"person1"];
Person *p2 = [unarchiver decodeObjectForKey:@"person2"];
[unarchiver finishDecoding];

NSLog(@"%@", p1.name);
NSLog(@"%@", p2.name);
}

SQLite3

数据库(splite):
splite是一个轻量级,跨平台的小型数据库,可移植性比较高,有着和MySpl几乎相同的数据库语句,以及无需服务器即可使用的优点:

数据库的优点:

  1. 该方案可以存储大量的数据,存储和检索的速度非常快.
  2. 能对数据进行大量的聚合,这样比起使用对象来讲操作要快.

数据库的缺点:

  1. 它没有提供数据库的创建方式
  2. 它的底层是基于C语言框架设计的, 没有面向对象的API, 用起来非常麻烦
  3. 发杂的数据模型的数据建表,非常麻烦
    在实际开发中我们都是使用的是FMDB第三方开源的数据库,该数据库是基于splite封装的面向对象的框架.
#import "SqliteVC.h"
#import "Person.h"
@interface SqliteVC() {

sqlite3 *_db;

}
@end

@implementation SqliteVC

- (void)viewDidLoad {
[super viewDidLoad];

NSString *fileName = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"student.sqlite"];
NSLog(@"fileName = %@",fileName);

int result = sqlite3_open(fileName.UTF8String, &_db); //创建(打开)数据库,如果数据库不存在,会自动创建 数据库文件的路径必须以C字符串(而非NSString)传入

if (result == SQLITE_OK) {
NSLog(@"成功打开数据库");

char *errorMesg = NULL;
const char *sql = "create table if not exists t_person (id integer primary key autoincrement, name text, age integer);";
int result = sqlite3_exec(_db, sql, NULL, NULL, &errorMesg); //sqlite3_exec()可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据

if (result == SQLITE_OK) {
NSLog(@"成功创建t_person表");
} else {
NSLog(@"创建t_person表失败:%s",errorMesg);
}

} else {
NSLog(@"打开数据库失败");
}
}
- (IBAction)insert:(id)sender {
for (int i = 0; i < 30; i++) {

NSString *name = [NSString stringWithFormat:@"person-%d",arc4random()0];
int age = arc4random() % 100;

char *errorMesg = NULL;
NSString *sql = [NSString stringWithFormat:@"insert into t_person (name,age) values ('%@',%d);",name, age];
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errorMesg);

if (result == SQLITE_OK) {
NSLog(@"添加数据成功");
} else {
NSLog(@"添加数据失败");
}
}
}

- (IBAction)delete:(id)sender {
char *errorMesg = NULL;
NSString *sql = @"delete from t_person where age >= 0";
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errorMesg);

if (result == SQLITE_OK) {
NSLog(@"删除成功");
}else {
NSLog(@"删除失败");
}
}

- (IBAction)query:(id)sender {
const char *sql = "select id, name, age from t_person;"; //"select id, name, age from t_person where age >= 50;"
sqlite3_stmt *stmt = NULL; //定义一个stmt存放结果集
int result = sqlite3_prepare_v2(_db, sql, -1, &stmt, NULL); //检测SQL语句的合法性

if (result == SQLITE_OK) {
NSLog(@"查询语句合法");

while (sqlite3_step(stmt) == SQLITE_ROW) {

int ID = sqlite3_column_int(stmt, 0);
const unsigned char *sname = sqlite3_column_text(stmt, 1);
NSString *name = [NSString stringWithUTF8String:(const char *)sname];
int age = sqlite3_column_int(stmt, 2);

NSLog(@"%d %@ %d",ID, name, age);
}
} else {
NSLog(@"查询语句非法");
}
}

- (IBAction)update:(id)sender {
NSString *sql = @"update t_person set name = '哈哈' where age > 60";
char *errorMesg = NULL;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errorMesg);

if (result == SQLITE_OK) {
NSLog(@"更改成功");
}else {

NSLog(@"更改失败");
}
}

coreData

coreData是苹果官方在iOS5之后推出的综合性数据库,其使用了对象关系映射技术,将对象转换成数据,将数据存储在本地的数据库中
coreData为了提高效率,需要将数据存储在不同的数据库中,比如:在使用的时候,最好是将本地的数据保存到内存中,这样的目的是访问速度比较快.

CoreData与SQLite进行对比

SQLite
1、基于C接口,需要使用SQL语句,代码繁琐
2、在处理大量数据时,表关系更直观
3、在OC中不是可视化,不易理解


CoreData
1、可视化,且具有undo/redo能力
2、可以实现多种文件格式:
* NSSQLiteStoreType
* NSBinaryStoreType
* NSInMemoryStoreType
* NSXMLStoreTyp
3、苹果官方API支持,与iOS结合更紧密

CoreData核心类与结构

NSManagedObjectContext(数据上下文)

  • 对象管理上下文,负责数据的实际操作(重要)
  • 作用:插入数据,查询数据,删除数据,更新数据

NSPersistentStoreCoordinator(持久化存储助理)

  • 相当于数据库的连接器
  • 作用:设置数据存储的名字,位置,存储方式,和存储时机

NSManagedObjectModel(数据模型)

  • 数据库所有表格或数据结构,包含各实体的定义信息
  • 作用:添加实体的属性,建立属性之间的关系
  • 操作方法:视图编辑器,或代码

NSManagedObject(被管理的数据记录)

  • 数据库中的表格记录

NSEntityDescription(实体结构)

  • 相当于表格结构

NSFetchRequest(数据请求)

  • 相当于查询语句

后缀为.xcdatamodeld的包

  • 里面是.xcdatamodel文件,用数据模型编辑器编辑
  • 编译后为.momd或.mom文件

类关系图

开始创建coredata

步骤:
1.创建模型文件 [相当于一个数据库]
2.添加实体 [一张表]
3.创建实体类 [相当模型--表结构]
4.生成上下文 关联模型文件生成数据库

1.创建模型文件

New File -> iOS -> Core Data ->Data Model

2.创建实体

Codegen

3.创建实体类

创建结果如图所示:

1.生成上下文 关联模型文件生成数据库,进行增删查改操作

#import "coredataVC.h"
#import <CoreData/CoreData.h>
#import "Student+CoreDataProperties.h"

@interface coredataVC ()

@property(nonatomic, strong)NSManagedObjectContext *context;

@end

@implementation coredataVC

- (void)viewDidLoad {
[super viewDidLoad];

//entity 记得勾选 language:objective-c 和 codegen:manual/none
[self createSql];
}

- (void)createSql {
//获取模型路径
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Person" withExtension:@"momd"];
//根据模型文件创建模型对象
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

//利用模型对象创建持久化存储助理
NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];

//数据库的名称和路径
NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *sqlPath = [docStr stringByAppendingPathComponent:@"coreData.sqlite"];
NSURL *sqlUrl = [NSURL fileURLWithPath:sqlPath];
NSLog(@"数据库 path = %@", sqlPath);

NSError *error = nil; //设置数据库相关信息 添加一个持久化存储库并设置类型和路径,NSSQLiteStoreType:SQLite作为存储库
[store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sqlUrl options:nil error:&error];

if (error) {
NSLog(@"添加数据库失败:%@",error);
} else {
NSLog(@"添加数据库成功");
}

//3、创建上下文 保存信息 对数据库进行操作 关联持久化助理
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = store;
_context = context;
}


- (IBAction)insertClick:(id)sender {
Student * student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:_context];
student.name = [NSString stringWithFormat:@"stu-%d",arc4random()0];
student.age = arc4random()%30;

NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"数据插入到数据库成功");
}else{
NSLog(@"数据插入到数据库失败");
}
}


- (IBAction)deleteClick:(id)sender {
//创建删除请求
NSFetchRequest *deleRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];

//删除条件 没有任何条件就是读取所有的数据
//NSPredicate *pre = [NSPredicate predicateWithFormat:@"age < %d", 10];
//deleRequest.predicate = pre;

//返回需要删除的对象数组
NSArray *deleArray = [_context executeFetchRequest:deleRequest error:nil];

//从数据库中删除
for (Student *stu in deleArray) {
[_context deleteObject:stu];
}

NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"删除数据成功");
}else{
NSLog(@"删除数据失败, %@", error);
}
}


- (IBAction)queryClick:(id)sender {
//创建查询请求
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];

//查询条件 没有任何条件就是读取所有的数据
NSPredicate *pre = [NSPredicate predicateWithFormat:@"age >= 0"];
request.predicate = pre;

// 从第几页开始显示 通过这个属性实现分页
//request.fetchOffset = 0;
// 每页显示多少条数据
//request.fetchLimit = 6;

//发送查询请求
NSArray *resArray = [_context executeFetchRequest:request error:nil];

//打印查询结果
for (Student *stu in resArray) {
NSLog(@"name=%@, age=%d",stu.name, stu.age);
}
}


- (IBAction)updateClick:(id)sender {
//创建查询请求
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];

NSPredicate *pre = [NSPredicate predicateWithFormat:@"age >= 0"];
request.predicate = pre;

//发送请求
NSArray *resArray = [_context executeFetchRequest:request error:nil];

//修改
for (Student *stu in resArray) {
stu.name = @"ran";
}

NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"更新数据成功");
}else{
NSLog(@"更新数据失败, %@", error);
}
}

@end


转自:https://blog.csdn.net/u013712343/article/details/106698848

收起阅读 »

iOS 显示动态图、GIF图方法总结

一、WebView加载可以通过WebView加载本地Gif图和网络Gif图,但图片大小不能自适应控件大小,也不能设置Gif图播放时间。使用如下:// 1、WebView加载- (void)webViewShowGif { UIWebView *webVi...
继续阅读 »

一、WebView加载

可以通过WebView加载本地Gif图和网络Gif图,但图片大小不能自适应控件大小,也不能设置Gif图播放时间。使用如下:

// 1、WebView加载
- (void)webViewShowGif {
UIWebView *webView = self.viewArr[0];

// 本地地址
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"gif"];
// 网路地址
// NSString *imagePath = @"http://qq.yh31.com/tp/zjbq/201711092144541829.gif";

NSURL *imageUrl = [NSURL URLWithString:imagePath];
NSURLRequest *request = [NSURLRequest requestWithURL:imageUrl];
[webView loadRequest:request];
}

二、UIImageView加载多图动画

把动态图拆分成一张张图片,将一系列帧添加到animationImages数组里面,然后设置animation一系列属性,如动画时间,动画重复次数。例:

// 2、UIImageView加载多张图片,播放
- (void)imageViewStartAnimating {
UIImageView *imageView = self.viewArr[1];

NSMutableArray *imageArr = [NSMutableArray arrayWithCapacity:3];
for (int i = 0; i<3; i++) {
NSString *imageStr = [NSString stringWithFormat:@"import_progress%d",i + 1];
UIImage *image = [UIImage imageNamed:imageStr];
[imageArr addObject:image];
}
imageView.animationImages = imageArr;
imageView.animationDuration = 2;
[imageView startAnimating];
}

三、SDWebImage加载本地GIF

在SDWebImage这个库里有一个UIImage+GIF的类别,使用sd_animatedGIFWithData方法可以将GIF图片数据专为图片。例:

// 3、SDWebImage加载本地GIF
- (void)imageViewLocalGif {
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"happy" ofType:@"gif"];
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
UIImage *image = [UIImage sd_animatedGIFWithData:imageData];

UIImageView *imageView = self.viewArr[2];
imageView.image = image;
}

四、SDWebImage加载网络GIF

首先将网络gif图下载到本地,然后再用sd_animatedGIFWithData方法,转为可用的图片,下载gif图的方式有两种

方式一:采用SDWebImageDownloader下载,回调里面会有NSData。只是,你会发现采用SDWebImageDownloader下载,界面显示就是没有sd_setImageWithURL方法流畅,这是因为sd_setImageWithURL里面对cache和线程做了很多处理,保证了UI的流畅。

NSString *imageStr = @"http://qq.yh31.com/tp/zjbq/201711142021166458.gif";
NSURL *imgeUrl = [NSURL URLWithString:imageStr];
SDWebImageDownloaderOptions options = 0;
UIImageView *imageView = self.viewArr[3];

// 方法一 SDWebImageDownloader下载
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imgeUrl
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {

} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
imageView.image = [UIImage sd_animatedGIFWithData:data];
}];

方式二、sd_setImageWithURL下载,回调的时候不用image,去直接读cache。(首先要了解sd_setImageWithURL里的内部逻辑,下载完之后先入cache,再执行block,这才保证外面可以直接读取到),取出来的就是NSData。首次下载成功时,可能获取data失败,因为这次图片可能还没存储成功,有延迟。

// 方法二 sd_setImageWithURL下载
SDWebImageOptions opt = SDWebImageRetryFailed | SDWebImageAvoidAutoSetImage;
[imageView sd_setImageWithURL:imgeUrl
placeholderImage:nil
options:opt
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {

if (image.images && image.images.count) {
NSString *path = [[SDImageCache sharedImageCache] defaultCachePathForKey:imageURL.absoluteString];
NSData *data = [NSData dataWithContentsOfFile:path];
UIImage *gifImage = [UIImage sd_animatedGIFWithData:data];
imageView.image = gifImage;
}
}];

五、FLAnimatedImage使用  

FLAnimatedImage 是由Flipboard开源的iOS平台上播放GIF动画的一个优秀解决方案,在内存占用和播放体验都有不错的表现。FLAnimatedImage项目的流程比较简单,FLAnimatedImage就是负责GIF数据的处理,然后提供给FLAnimatedImageView一个UIImage对象。FLAnimatedImageView拿到UIImage对象显示出来就可以了。 例:

// 5、FLAnimatedImage使用
- (void)animatedImageViewShowGif {
FLAnimatedImageView *imageView = self.viewArr[4];

NSURL *url = [[NSBundle mainBundle] URLForResource:@"weiwei" withExtension:@"gif"];
NSData *data = [NSData dataWithContentsOfURL:url];
FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:data];
imageView.animatedImage = animatedImage;
}

六、YYImage使用

1.显示本地gif 

//load loacle gif image
- (void)loadLocaleGifImage{
//yyImage show gif image
[self labelFactoryWithFrame:CGRectMake(0, kScreenHeight/2 - 20, kScreenWidth, 20) title:@"yyImage"];
YYImage *yyimage = [YYImage imageNamed:@"test.gif"];
YYAnimatedImageView *yyImageView = [[YYAnimatedImageView alloc] initWithImage:yyimage];
yyImageView.frame = CGRectMake(0, kScreenHeight/2, kScreenWidth, kScreenHeight/3);
[self.view addSubview:yyImageView];
}

 2.加载网络gif图

//download network gif image
- (void)downloadNetworkGifImage{

//yyImage show gif image
[self labelFactoryWithFrame:CGRectMake(0, kScreenHeight/2 - 20, kScreenWidth, 20) title:@"yyImage"];
YYImage *yyimage = [YYImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://photocdn.sohu.com/20151214/mp48444247_1450092561460_10.gif"]]];
YYAnimatedImageView *yyImageView = [[YYAnimatedImageView alloc] initWithImage:yyimage];
yyImageView.frame = CGRectMake(0, kScreenHeight/2, kScreenWidth, kScreenHeight/3);
[self.view addSubview:yyImageView];
}

- (void)labelFactoryWithFrame:(CGRect)frame title:(NSString *)title{

UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.textAlignment = NSTextAlignmentCenter;
label.textColor = [UIColor blackColor];
label.font = [UIFont systemFontOfSize:14];
label.text = title;
[self.view addSubview:label];
}


收起阅读 »

iOS -Masonry详解

现在iPhone手机屏幕越来越多, 屏幕适配也越来越重要. Masonry就是为屏幕适配而生的三方框架.Masonry基础APImas_makeConstraints() 添加约束mas_remakeConstraints() 移除之前的约束,重新添加...
继续阅读 »

现在iPhone手机屏幕越来越多, 屏幕适配也越来越重要. Masonry就是为屏幕适配而生的三方框架.

Masonry基础API

mas_makeConstraints()    添加约束
mas_remakeConstraints() 移除之前的约束,重新添加新的约束
mas_updateConstraints() 更新约束,写哪条更新哪条,其他约束不变

equalTo() 参数是对象类型,一般是视图对象或者mas_width这样的坐标系对象
mas_equalTo() 和上面功能相同,参数可以传递基础数据类型对象,可以理解为比上面的API更强大

width() 用来表示宽度,例如代表view的宽度
mas_width() 用来获取宽度的值。和上面的区别在于,一个代表某个坐标系对象,一个用来获取坐标系对象的值

更新约束和布局

Masonry本质上就是对系统AutoLayout进行的封装,包括里面很多的API,都是对系统API进行了一次二次包装。
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
};

Masonry示例代码

Masonry本质上就是对系统AutoLayout进行的封装,包括里面很多的API,都是对系统API进行了一次二次包装。
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
};

常用方法

设置内边距

/** 
设置yellow视图和self.view等大,并且有10的内边距。
注意根据UIView的坐标系,下面right和bottom进行了取反。所以不能写成下面这样,否则right、bottom这两个方向会出现问题。
make.edges.equalTo(self.view).with.offset(10);

除了下面例子中的offset()方法,还有针对不同坐标系的centerOffset()、sizeOffset()、valueOffset()之类的方法。
*/
[self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).with.offset(10);
make.top.equalTo(self.view).with.offset(10);
make.right.equalTo(self.view).with.offset(-10);
make.bottom.equalTo(self.view).with.offset(-10);
}];

通过insets简化设置内边距的方式

// 下面的方法和上面例子等价,区别在于使用insets()方法。
[self.blueView mas_makeConstraints:^(MASConstraintMaker *make) {
// 下、右不需要写负号,insets方法中已经为我们做了取反的操作了。
make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));
}];

更新约束

// 设置greenView的center和size,这样就可以达到简单进行约束的目的
[self.greenView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
// 这里通过mas_equalTo给size设置了基础数据类型的参数,参数为CGSize的结构体
make.size.mas_equalTo(CGSizeMake(300, 300));
}];

// 为了更清楚的看出约束变化的效果,在显示两秒后更新约束。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 指定更新size,其他约束不变。
[self.greenView mas_updateConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(100, 100));
}];
});

大于等于和小于等于某个值的约束

[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
// 设置宽度小于等于200
make.width.lessThanOrEqualTo(@200);
// 设置高度大于等于10
make.height.greaterThanOrEqualTo(@(10));
}];

self.textLabel.text = @"这是测试的字符串。能看到1、2、3个步骤,第一步当然是上传照片了,要上传正面近照哦。上传后,网站会自动识别你的面部,如果觉得识别的不准,你还可以手动修改一下。左边可以看到16项修改参数,最上面是整体修改,你也可以根据自己的意愿单独修改某项,将鼠标放到选项上面,右边的预览图会显示相应的位置。";

textLabel只需要设置一个属性即可

self.textLabel.numberOfLines = 0;

使用基础数据类型当做参数

/** 
如果想使用基础数据类型当做参数,Masonry为我们提供了"mas_xx"格式的宏定义。
这些宏定义会将传入的基础数据类型转换为NSNumber类型,这个过程叫做封箱(Auto Boxing)。

"mas_xx"开头的宏定义,内部都是通过MASBoxValue()函数实现的。
这样的宏定义主要有四个,分别是mas_equalTo()、mas_offset()和大于等于、小于等于四个。
*/
[self.redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.width.mas_equalTo(100);
make.height.mas_equalTo(100);
}];

设置约束优先级

/** 
Masonry为我们提供了三个默认的方法,priorityLow()、priorityMedium()、priorityHigh(),这三个方法内部对应着不同的默认优先级。
除了这三个方法,我们也可以自己设置优先级的值,可以通过priority()方法来设置。
*/
[self.redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.width.equalTo(self.view).priorityLow();
make.width.mas_equalTo(20).priorityHigh();
make.height.equalTo(self.view).priority(200);
make.height.mas_equalTo(100).priority(1000);
}];

Masonry也帮我们定义好了一些默认的优先级常量,分别对应着不同的数值,优先级最大数值是1000。
static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500;
static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow;
static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;

设置约束比例

// 设置当前约束值乘以多少,例如这个例子是redView的宽度是self.view宽度的0.2倍。
[self.redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.height.mas_equalTo(30);
make.width.equalTo(self.view).multipliedBy(0.2);
}];

小练习

子视图等高/等宽练习

/**
下面的例子是通过给equalTo()方法传入一个数组,设置数组中子视图及当前make对应的视图之间等高。

需要注意的是,下面block中设置边距的时候,应该用insets来设置,而不是用offset。
因为用offset设置right和bottom的边距时,这两个值应该是负数,所以如果通过offset来统一设置值会有问题。
*/

CGFloat padding = 10;

UIView *redView = [[UIView alloc]init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];

UIView *blueView = [[UIView alloc]init];
blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:blueView];

UIView *yellowView = [[UIView alloc]init];
yellowView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:yellowView];

/********** 等高 ***********/
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, 0, padding));
make.bottom.equalTo(blueView.mas_top).offset(-padding);
}];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, 0, padding));
make.bottom.equalTo(yellowView.mas_top).offset(-padding);
}];

/**
下面设置make.height的数组是关键,通过这个数组可以设置这三个视图高度相等。其他例如宽度之类的,也是类似的方式。
*/
[yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, padding, padding));
make.height.equalTo(@[blueView, redView]);
}];

/********** 等宽 ***********/
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, padding, 0));
make.right.equalTo(blueView.mas_left).offset(-padding);
}];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(padding, 0, padding, 0));
make.right.equalTo(yellowView.mas_left).offset(-padding);
}];
[yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.right.equalTo(self.view).insets(UIEdgeInsetsMake(padding, 0, padding, padding));
make.width.equalTo(@[redView, blueView]);
}];


子视图垂直居中练习

CGFloat padding = 10;

UIView *redView = [[UIView alloc]init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];

UIView *blueView = [[UIView alloc]init];
blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:blueView];

[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.left.equalTo(self.view).mas_offset(padding);
make.right.equalTo(blueView.mas_left).mas_offset(-padding);
//make.width.equalTo(blueView);
make.height.mas_equalTo(150);
}];

[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.right.equalTo(self.view).mas_offset(-padding);
make.width.equalTo(redView);
make.height.mas_equalTo(150);
}];



转自:https://www.jianshu.com/p/587efafdd2b3

收起阅读 »

iOS动态换肤-支持暗夜模式

适配暗夜模式iOS13新出现了暗夜模式,苹果新增了一些API方便我们来做适配。这里不做深入,只是稍微总结下。适配暗夜模式,无非就是界面显示上的一些变化,暗夜模式下,主题由默认的白色调变为了深色调,相应的,我们的APP在显示上也需要做相应调整。主要包括两个方面:...
继续阅读 »

适配暗夜模式

iOS13新出现了暗夜模式,苹果新增了一些API方便我们来做适配。这里不做深入,只是稍微总结下。

适配暗夜模式,无非就是界面显示上的一些变化,暗夜模式下,主题由默认的白色调变为了深色调,相应的,我们的APP在显示上也需要做相应调整。主要包括两个方面:颜色的变化(视图颜色色,字体颜色等)和图片的改变;

  • 关于颜色改变:UIcolor新增了一个分类\color{red}{UIColor (DynamicColors)},提供了动态color的API。通过特征收集器traitCollection,可以动态判断当前手机的一些界面特征信息。

/*使用时可以做下进一步封装。*/
[UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
UIColor *color = [UIColor lightGrayColor];
if (@available(iOS 13.0,*)) {
if (traitCollection.userInterfaceStyle ==UIUserInterfaceStyleDark ) {
color =[UIColor blackColor];//dark
}else if(traitCollection.userInterfaceStyle ==UIUserInterfaceStyleLight){
color =[UIColor lightGrayColor];//light
}
}
return color;
}];
  • 关于图片:可以在Assets.xcassets中给每一套图片设置对应的模式,系统自动根据当前的模式取用相应的图片;


  • 监听模式的改变:做好以上两点只能部分满足需求,很多时候我们需要确切的知道当前的模式,并且知道用户什么时候切换的模式;

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
[super traitCollectionDidChange:previousTraitCollection];
if ([UITraitCollection currentTraitCollection].userInterfaceStyle !=previousTraitCollection.userInterfaceStyle ) {
NSLog(@"用户切换了模式,在这里做适配工作");
}else{
NSLog(@"用户没有切换模式");
}
}

关于这种适配方式的几点看法:

  • 工作繁琐,代码杂乱。界面如果需要做一些定制化的改变,就需要监听模式的改变,可能出现一个界面适配的代码出现在好几个地方。

  • 扩展性不高。现在出现了一个暗夜模式,将来会不会再有其他模式?如果APP本来就有几套主题,那么适配起来更加繁琐杂乱。

  • 细细想来,适配暗夜模式,不就是切换主题吗,单独给暗夜模式弄一套对应皮肤就完了。下面看看,如何给APP便捷高效、扩展性性高地换肤。

动态换肤(DynamicSkin)
代码简洁,便于维护;
自动适配暗夜模式,不需要自己每个界面去监听模式的切换;

使用步骤
1.引入框架,导入头文件
手动引入或者通过CocoaPods

pod 'DynamicSkin'
#import "DPDynamicTheme.h"

2.配置模型
继承DPThemeConfig,根据自己的需求,配置相应字段即可。

#import "DPThemeConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface TestConfig : DPThemeConfig
@property(nonatomic,copy)NSString*color1;
@property(nonatomic,copy)NSString*color2;
@property(nonatomic,copy)NSString*img1;
@property(nonatomic,copy)NSString*tabOne;
@property(nonatomic,copy)NSString*tabTwo;
@property(nonatomic,copy)NSString*tabThree;
@property(nonatomic,copy)NSString*tabTextColorNormal;
@property(nonatomic,copy)NSString*tabTextColorSelect;
@property(nonatomic,copy)NSString*state;
@end

NS_ASSUME_NONNULL_END

3.设置默认主题

__weak typeof (self)weakSelf = self;
//用户切换暗夜模式,或则主动切换pushCurrentThemme:,会触发该回调
[self tz_dynamicTheme:^(TestConfig * _Nullable config) {
[weakSelf.image sd_setImageWithURL:[NSURL URLWithString:config.img1]];
weakSelf.statelabel.text = config.state;
} WithIdentifier:NSStringFromClass([self class])];
}

4.数据绑定

__weak typeof (self)weakSelf = self;
//用户切换暗夜模式,或则主动切换pushCurrentThemme:,会触发该回调
[self tz_dynamicTheme:^(TestConfig * _Nullable config) {
[weakSelf.image sd_setImageWithURL:[NSURL URLWithString:config.img1]];
weakSelf.statelabel.text = config.state;
} WithIdentifier:NSStringFromClass([self class])];
}

5.销毁不需要的回调

-(void)dealloc{
//identifer需要和当前界面绑定的保持一致
[[DPThemeManager manager] removeUpdateWithIdentifer:NSStringFromClass([self class])];
}

转自:https://www.jianshu.com/p/50f24d5af4cd

收起阅读 »

iOS -SDWebImage的使用和底层原理

一、SDWebImage的使用1、SDWebImage的安装集成有2种方式:(1)直接到github地址下载,链接https://github.com/rs/SDWebImage        (2)用coco...
继续阅读 »

一、SDWebImage的使用

1、SDWebImage的安装集成有2种方式:

(1)直接到github地址下载,链接https://github.com/rs/SDWebImage        

(2)用cocoapods安装,在文件夹生成的podfile文件中添加pod 'SDWebImage' ,终端cd + 文件位置,然后pod install即可

2、UITableView中导入头文件UIImageView+WebCache.h

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

如果在加载完图片后,需要做些其他操作,可以使用block回调

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
... completion code here ...
}];

3、SDWebImageManager的使用

UIImageView(WebCache) 分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。SDWebImageManager也可以单独使用。

SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
// do something with image
}
}];

4、单独使用SDWebImageDownloader异步下载图片

我们还可以单独使用 SDWebImageDownloader 来下载图片,但是图片内容不会缓存。

SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// do something with image
}
}];

5、单独使用SDImageCache异步缓存图片

SDImageCache 支持内存缓存和异步的磁盘缓存(可选),如果你想单独使用 SDImageCache 来缓存数据的话,可以使用单例,也可以创建一个有独立命名空间的 SDImageCache 实例。

添加缓存的方法:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];

读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
// image is not nil if image was found
}];

6、自定义缓存key

有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的 key。

SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
};

二、使用过程中常见问题

问题 1:使用 UITableViewCell 中的 imageView 加载不同尺寸的网络图片时会出现尺寸缩放问题。

解决方案: 

自定义 UITableViewCell,重写 -layoutSubviews 方法,调整位置尺寸; 
或者直接弃用 UITableViewCell 的 imageView,自己添加一个 imageView 作为子控件。

问题 2:图片刷新问题:SDWebImage 在进行缓存时忽略了所有服务器返回的 caching control 设置,并且在缓存时没有做时间限制,这也就意味着图片 URL 必须是静态的了,要求服务器上一个 URL 对应的图片内容不允许更新。但是如果存储图片的服务器不由自己控制,也就是说 图片内容更新了,URL 却没有更新,这种情况怎么办?

解决方案:在调用 sd_setImageWithURL: placeholderImage: options:方法时设置 options 参数为 SDWebImageRefreshCached,这样虽然会降低性能,但是下载图片时会照顾到服务器返回的 caching control。

问题 3:在加载图片时,如何添加默认的 progress indicator ? 

解决方案:在调用 -sd_setImageWithURL:方法之前,先调用下面的方法:

[imageView sd_setShowActivityIndicatorView:YES]; 

[imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];

问题4:如果在加载图片的过程中出现程序报错(App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.)

你需要操作如下--------
(1)、在Info.plist中添加 NSAppTransportSecurity 类型 Dictionary ;

(2)、在 NSAppTransportSecurity 下添加 NSAllowsArbitraryLoads 类型Boolean ,值设为 YES;

三、SDWebImage底层原理


1)当我门需要获取网络图片的时候,我们首先需要的便是URL,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URl相关的图片,如果有则直接返回image,如果没有则进行下一步。

2)当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。

3)如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。

4)刷新UI(当然根据实际情况操作)将image存入沙盒缓存。

四、SDWebImage源码实现步骤

常见的四种加载方式

1、无占位图直接加载(如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

2、有占位图直接加载(如果URL加载不到则展示占位图,如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

3、有占位图直接加载,并且实现图片加载完之后的Block可以继续完成下一步操作(如果URL加载不到则展示占位图,如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

4、可以选择options的形式加载图片,(如果URL加载不到则展示占位图,如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

/*使用可更换optionsType的加载方式

-------------Options 枚举下的加载方式-----------
SDWebImageRetryFailed 默认情况下,当URL无法下载时,URL就会被列入黑名单,这样库就不会继续尝试了。此标记禁用此黑名单。
SDWebImageLowPriority 默认情况下,图像下载是在UI交互过程中启动的,这标志禁用该特性,导致在UIScrollView减速方面延迟下载。
SDWebImageCacheMemoryOnly 此标记禁用磁盘缓存
SDWebImageProgressiveDownload 此标志可以进行渐进式下载,在下载过程中,图像会逐步显示,就像浏览器所做的那样。默认情况下,图像只显示一次完全下载。
SDWebImageRefreshCached 即使缓存了映像,也要尊重HTTP响应缓存控制,并在需要的情况下从远程位置刷新映像。磁盘缓存将由NSURLCache来处理,而不是使用SDWebImage,这会导致轻微的性能下降。这个选项有助于处理在同一个请求URL后面更改的图像,例如Facebook图形api概要图。如果刷新了缓存的图像,那么完成块就会被缓存的图像和最后的图像再次调用一次。只有当你不能用嵌入的缓存破坏参数使你的url静态时,才使用这个标志。
SDWebImageContinueInBackground 在iOS 4+中,如果应用程序进入后台,可以继续下载图片。这是通过请求系统在后台获得额外的时间来完成请求完成的。如果后台任务过期,操作将被取消。
SDWebImageHandleCookies 通过设置NSMutableURLRequest来处理存储在NSHTTPCookieStore中的cookie。HTTPShouldHandleCookies =是的;
SDWebImageAllowInvalidSSLCertificates 启用不受信任的SSL证书。用于测试目的。在生产中使用谨慎。
SDWebImageHighPriority 默认情况下,图像按顺序装载在队列中。这个标志把它们移到队列的前面。
SDWebImageDelayPlaceholder 默认情况下,在图像加载时加载占位符图像。此标志将延迟加载占位符图像,直到图像完成加载。
SDWebImageTransformAnimatedImage 我们通常不会在动画图像上调用transformdownloade昏暗委托方法,因为大多数转换代码会把它搞砸。无论如何,使用这个标志来转换它们。* /
SDWebImageAvoidAutoSetImage 默认情况下,图像会在下载后添加到imageView中。但是在某些情况下,我们想要在设置图像之前有手(例如,应用一个过滤器或将它添加到交叉衰减动画中)使用这个标记如果你想在成功完成时手工设置图像
SDWebImageScaleDownLargeImages 默认情况下,图像会被解码,以尊重它们原来的大小。在iOS上,这一标志将把图像缩小到与设备受限内存兼容的大小。*如果“SDWebImageProgressiveDownload”标志设置禁用缩减。
*/

以上四个常用方法,点击进去查看内部实现代码时,你会发现所有方法都指向------>


源码注释解释的含义是

用url、占位符和自定义选项设置imageView图像。下载是异步的和缓存的。
@param url是图像的url。
@param占位符将首先设置的图像,直到图像请求完成。
@param选择在下载图像时使用的选项。
@参见SDWebImageOptions用于可能的值。
@param progressBlock在下载@note时,在后台队列
@param completedBlock的后台进程中执行进程块,该块是在操作完成时被调用的。这个块没有返回值,并将所请求的UIImage作为第一个参数。在出现错误时,图像参数为nil,第二个参数可能包含一个NSError。第三个参数是一个布尔值,指示是否从本地缓存或网络检索图像。第四个参数是原始图像url。

下面是图解(上面展示了每句话的备注)

1、设置展位图,并且取消当前下载任务


2、创建一个新的下载操作


3、下载操作代码(判断流是否存在,如果不存在则将其存在失效列表中,防止重复下载无效流)-----在这里他对NSString和NSURL的转换做了判断。原因是(非常常见的错误是使用NSString对象而不是NSURL发送URL。出于某种奇怪的原因,Xcode不会对这种类型的不匹配发出任何警告。在这里,我们通过允许url作为NSString传递来确保这个错误。)括号当中是文档给出的解释,所以这里做了强制转换。


4、利用唯一生成的key,到缓存--->内存---->磁盘中分别寻找。


5、寻找的顺序 缓存---->磁盘---->在没有就下载


下载流程之后就是清理缓存(种类) 1、清理所有内存缓存镜像 2、清理所有磁盘缓存镜像3、清理过期的缓存映像从磁盘中删除

/*
异步清除所有磁盘缓存映像。非阻塞方法-立即返回。@param完成一个应该在缓存过期后执行的块(可选)

注意:这里要注意[[SDImageCache sharedImageCache] clearDisk];方法会报错,下面clearDiskOnCompletion的方法会替代上面的方法
*/
[[SDImageCache sharedImageCache] clearDiskOnCompletion:^{

}];

/*
Clear all memory cached images --->清除所有缓存镜像
*/
[[SDImageCache sharedImageCache] clearMemory];

/*
异步将所有过期的缓存映像从磁盘中删除。非阻塞方法-立即返回。@param completionBlock在缓存过期后执行(可选)--->故名思义他是不能删除你当前缓存的大小的
*/
[[SDImageCache sharedImageCache] deleteOldFilesWithCompletionBlock:^{

}];

五、总结

SDWebImage加载图片的流程:
1. 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage显示,然后 SDWebImageManager 根据 URL 开始处理图片。

2. 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

4. SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache等前端展示图片。

5. 如果内存缓存中没有,生成 NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存。

6. 根据 URLKey在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

9. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

10. 图片下载由 NSURLConnection来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

11. connection:didReceiveData: 中利用 ImageIO做了按图片下载进度加载效果。

12. connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

13. 图片解码处理在一个 NSOperationQueue完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

14. 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

15. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager告知图片下载完成

16. 通知所有的 downloadDelegates下载完成,回调给需要的地方展示图片。

17. 将图片保存到 SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

18. SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

19. SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

20. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。



原文链接:https://blog.csdn.net/qq_16146389/article/details/88355852


收起阅读 »

iOS -AFN实现原理&&面试

AFNetworking是封装的NSURLSession的网络请求。AFNetworking由五个模块组成:分别由NSURLSession,Security,Reachability,Serialization,UIKit五部分组成NSURLSession:网...
继续阅读 »

AFNetworking是封装的NSURLSession的网络请求。

AFNetworking由五个模块组成:

分别由NSURLSession,Security,Reachability,Serialization,UIKit五部分组成

NSURLSession:网络通信模块(核心模块) 对应 AFNetworking中的 AFURLSessionManager和对HTTP协议进行特化处理的AFHTTPSessionManager,AFHTTPSessionManager是继承于AFURLSessionmanager的
Security:网络通讯安全策略模块  对应 AFSecurityPolicy
Reachability:网络状态监听模块 对应AFNetworkReachabilityManager
Seriaalization:网络通信信息序列化、反序列化模块 对应 AFURLResponseSerialization
UIKit:对于IOSUIKit的扩展库

网络请求的过程:

创建NSURLSessionConfig对象--用创建的config对象配置初始化NSURLSession--创建NSURLSessionTask对象并resume执行,用delegate或者block回调返回数据。

AFURLSessionManager封装了上述网络交互功能

AFURLSessionManager请求过程

1.初始化AFURLSessionManager。

2.获取AFURLSessionManager的Task对象

3.启动Task

AFURLSessionManager会为每一个Task创建一个AFURLSessionmanagerTaskDelegate对象,manager会让其处理各个Task的具体事务,从而实现了manager对多个Task的管理

初始化好manager后,获取一个网络请求的Task,生成一个Task对象,并创建了一个AFURLSessionmanagerTaskDelegate并将其关联,设置Task的上传和下载delegate,通过KVO监听download进度和upload进度

NSURLSessionDelegate的响应

因为AFURLSessionmanager所管理的AFURLSession的delegate指向其自身,因此所有的NSURLSessiondelegate的回调地址都是AFURLSessionmanager,而AFURLSessionmanager又会根据是否需要具体处理会将AFdelegate所响应的delegate,传递到对应的AFdelegate去

面试相关:

AFN调用流程分析:

AFHTTPSessionManager: 发起网络请求(例如GET);
AFHTTPSessionManager内部调用dataTaskWithHTTPMethod:方法(内部处理requestSerializer);
dataTaskWithHTTPMethod内部调用父类AFURLSessionManager的dataTaskWithRequest: uploadProgress: downloadProgress: completionHandler方法;
AFURLSessionManager中的dataTaskWithRequest方法内部设置全局session和创建task;
AFURLSessionManager中的dataTaskWithRequest方法内部给task设置delegate(AFURLSessionManagerTaskDelegate);
taskDelegate代理的初始化: 绑定task / 存储task下载的数据 / 下载或上传进度 / 进度与task同步(KVO)
task对应的AFURLSessionManagerTaskDelegate实现对进度处理、Block调用、Task完成返回数据的拼装的功能等;
setDelegate: forTask: 加锁设置通过一个字典处理Task与之代理方法关联; 添加对Task开始、重启、挂起状态的通知的接收.
[downloadTask resume]后执行开始, 走代理回调方法(内部其实是NSURLSession的各种代理的实现);
task完成后走URLSession: task: didCompleteWithError: 回调对返回的数据进行封装;
同时移除对应的task; removeDelegateForTask: 加锁移除8中的字典和通知;

AFN请求过程梳理

首先我们是初始化了AFHTTPSessionManager类(往往创建单例)初始化时候指定请求回调的代理是父类(AFURLSessionManager)。之后当我们发出一个请求后,先创建一个AFURLSessionManagerTaskDelegate对象来保存请求结果回调。并把该对象放到一个全局字典中来保存(以task.taskIdentifier为key),再启动请求。当AFURLSessionManager类收到了请求结果后根据task.taskIdentifier从全局字典中取出当前请求的AFURLSessionManagerTaskDelegate对象。然后调用AFURLSessionManagerTaskDelegate的对象方法处理请求,完成回调。之后再从全局字典中移除该AFURLSessionManagerTaskDelegate对象。

AFN是怎样来解决循环引用的

首先我们用AFN时候往往是用单例,因此调用类不会直接持有该AFHTTPSessionManager对象。
该AFHTTPSessionManager对象持有block,该AFHTTPSessionManager对象持有全局字典,该全局字典持有AFURLSessionManagerTaskDelegate对象,该AFURLSessionManagerTaskDelegate对象持有block,这是一个循环引用。
当AFURLSessionManagerTaskDelegate对象block进行回调后,从全局字典中移除该对象。从而打破引用环。

1、AFN2.x为什么添加一条常驻线程?

AFN2.0里面把每一个网络请求的发起和解析都放在了一个线程里执行。正常来说,一个线程执行完任务后就退出了。开启runloop是为了防止线程退出。一方面避免每次请求都要创建新的线程;另一方面,因为connection的请求是异步的,如果不开启runloop,线程执行完代码后不会等待网络请求完的回调就退出了,这会导致网络回调的代理方法不执行。
这是一个单例,用NSThread创建了一个线程,并且为这个线程添加了一个runloop,并且加了一个NSMachPort,来防止runloop直接退出。 这条线程就是AF用来发起网络请求,并且接受网络请求回调的线程,仅仅就这一条线程

2、AFN3.x为什么不再需要常驻线程?

NSURLConnection的一大痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。
苹果也是明白了这一痛点,从iOS9.0开始 deprecated 了NSURLConnection。 替代方案就是NSURLSession。

3、为什么AF3.0中需要设置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0却不需要?

功能不一样:AF3.0的operationQueue是用来接收NSURLSessionDelegate回调的,鉴于一些多线程数据访问的安全性考虑,设置了maxConcurrentOperationCount = 1来达到串行回调的效果。
而AF2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。

AFNetworking3.0

在AFNetworking 3.0之前,底层是通过封装NSURLConnection来实现的。
在AFNetworking 3.0之后,也就是在iOS 9.0 之后,NSURLConnection被弃用,苹果推荐使用NSURLSession来管理网络请求,所以AFNetworking 3.0之后,底层是通过封装NSURLSession来实现的。

从AFNetworking 3.0中之后,下面三个方法被弃用了。
AFURLConnectionOperation
AFHTTPRequestOperation
AFHTTPRequestOperationManager

依次被下面三个类代替了,同时请求方法也跟着改变了,所以AFNetworking 3.0以后发生了很大的变化。
AFURLSessionManager
AFHTTPSessionManager
AFNetworkReachabilityManager

参考链接:https://blog.csdn.net/songzhuo1991/article/details/104883981

参考链接:https://blog.csdn.net/weixin_39638526/article/details/111748124

收起阅读 »

iOS -YYModel的底层实现原理

一. YYModel逻辑结构 实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型(YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给key赋value值,所以我们需要自己遍历容器(N...
继续阅读 »

一. YYModel逻辑结构 


实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型
(YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给key赋value值,所以我们需要自己遍历容器(NSArray,NSSet,NSDictionary),获取每一个值,然后KVC进行处理)。

1.Model 属性名和 JSON 中的 Key 不相同

// JSON:
{
"n":"Harry Pottery",
"p": 256,
"ext" : {
"desc" : "A book written by J.K.Rowing."
},
"ID" : 100010
}

// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end

你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。

在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。

2.Model 包含其他 Model

// JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}

// Model: 什么都不用做,转换会自动完成
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end

@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author; //Book 包含 Author 属性
@end
@implementation Book
@end

3.容器类属性

@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array
@property NSSet *borders; //Set
@property NSMutableDictionary *attachments; //Dict
@end

@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
@end

在实际使用过过程中,[Shadow class]Border.class@"Attachment"没有明显的区别。
这里仅仅是创建作者有说明,实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型。

YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给keyvalue值,所以我们需要自己遍历容器(NSArrayNSSetNSDictionary),获取每一个值,然后KVC)。

具体的代码实现如下:

NSDictionary *json =[self getJsonWithJsonName:@"ContainerModel"];
ContainerModel *containModel = [ContainerModel yy_modelWithDictionary:json];
NSDictionary *dataDict = [containModel valueForKey:@"data"];
//定义数组,接受key为list的数组
self.listArray = [dataDict valueForKey:@"list"];
//遍历数组
[self.listArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDictionary *listDict = obj;
//获取数组中得字典
List *listModel = [List yy_modelWithDictionary:listDict];
//获取count 和 id
NSString *count = [listModel valueForKey:@"count"];
NSString *id = [listModel valueForKey:@"id"];

4.黑名单与白名单

@interface User
@property NSString *name;
@property NSUInteger age;
@end

@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
return @[@"name"];
}
@end

5.数据校验与自定义转换

实际这个分类的目的比较简单和明确。
就是对判断是否为时间戳,然后对时间戳进行处理,调用
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
获取时间。

// JSON:
{
"name":"Harry",
"timestamp" : 1445534567 //时间戳
}

// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end

@implementation User
// JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
NSNumber *timestamp = dic[@"timestamp"];
if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
return YES;
}

// Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
if (!_createdAt) return NO;
dic[@"timestamp"] = @(n.timeIntervalSince1970);
return YES;
}
@end

  • 需要注意的时,如果用插件,对时间戳类型或默认创建为NSUInteger类型,需要将其更改为NSDate类型。

6.Coding/Copying/hash/equal/description

以下方法都是YYModel的简单封装,实际使用过程和系统方法区别不大。对其感兴趣的可以点进方法内部查看。

@interface YYShadow :NSObject 
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGSize size;
@end

@implementation YYShadow
// 直接添加以下代码即可自动完成
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self yy_modelEncodeWithCoder:aCoder];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
return [self yy_modelInitWithCoder:aDecoder];
}
- (id)copyWithZone:(NSZone *)zone {
return [self yy_modelCopy];
}
- (NSUInteger)hash {
return [self yy_modelHash];
}
- (BOOL)isEqual:(id)object {
return [self yy_modelIsEqual:object];
}
- (NSString *)description {
return [self yy_modelDescription];
}
@end


原文链接:https://blog.csdn.net/huhui168/article/details/80541387

收起阅读 »

iOS-数据结构初探

数据结构的分类数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成 简单来说:数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数...
继续阅读 »

数据结构的分类

数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成

简单来说:数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题时选取最合适的数据结构。

常用的数据结构有:数组,栈,链表,队列,树,图,堆,散列表等

1、数组

数组是可以再内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始

NSArray *array = [NSArray arrayWithObjects:@"1",@"2",@"3",@"4", nil];
// NSArray *array = @[@"1",@"2",@"3",@"4"];
NSLog(@"%@",array[0]);

优点:

  • 1、按照索引查询元素速度快
  • 2、按照索引遍历数组方便

缺点:

  • 1、数组的大小固定后就无法扩容了
  • 2、数组只能存储一种类型的数据
  • 3、添加,删除的操作慢,因为要移动其他的元素。

适用场景:

  • 频繁查询,对存储空间要求不大,很少增加和删除的情况。

2、栈

栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈

线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。

线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)

3、队列

队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队


4、链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

根据指针的指向,链表能形成不同的结构,例如单链表双向链表循环链表等。




双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。


链表的优点:

  • 链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
  • 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;

缺点:

  • 因为含有大量的指针域,占用空间较大;
  • 查找元素需要遍历链表来查找,非常耗时。

适用场景:

  • 数据量较小,需要频繁增加,删除操作的场景

5、树

树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 每个节点有零个或多个子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点
  • 除了根节点外,每个子节点可以分为多个不相交的子树;

在日常的应用中,我们讨论和用的更多的是树的其中一种结构,就是二叉树


二叉树是树的特殊一种,具有如下特点:

  • 1、每个结点最多有两颗子树,结点的度最大为2。
  • 2、左子树和右子树是有顺序的,次序不能颠倒。
  • 3、即使某结点只有一个子树,也要区分左右子树。

二叉树是一种比较有用的折中方案,它添加,删除元素都很快,并且在查找方面也有很多的算法优化,所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。

二叉树有很多扩展的数据结构,包括平衡二叉树红黑树B+树等,这些数据结构二叉树的基础上衍生了很多的功能,在实际应用中广泛用到,例如mysql的数据库索引结构用的就是B+树,还有HashMap的底层源码中用到了红黑树。这些二叉树的功能强大,但算法上比较复杂,想学习的话还是需要花时间去深入的。

6、散列表

散列表,也叫哈希表,是根据关键码值 (key和value) 直接进行访问的数据结构,通过keyvalue来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。

记录的存储位置=f(key)

  • 这里的对应关系f 成为散列函数,又称为哈希 (hash函数),而散列表就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字
  • 然后就将该数字对数组长度进行取余,取余结果就当作数组的下标
  • 将value存储在以该数字为下标的数组空间里
  • 这种存储空间可以充分利用数组的查找优势来查找元素,所以查找的速度很快。

哈希表在应用中也是比较常见的,就如Java中有些集合类就是借鉴了哈希原理构造的,例如HashMapHashTable等,利用hash表的优势,,对于集合的查找元素时非常方便的,然而,因为哈希表是基于数组衍生的数据结构,在添加删除元素方面是比较慢的,所以很多时候需要用到一种数组链表来做,也就是拉链法。拉链法是数组结合链表的一种结构,较早前的hashMap底层的存储就是采用这种结构,直到jdk1.8之后才换成了数组加红黑树的结构.iOSweak表(弱引用表)就是典型的哈希表


  • 左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,
  • 当然这个链表可能为空,也可能元素很多。
  • 我们根据元素的一些特征把元素分配到不同的链表中去,
  • 也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃。

7、堆

堆是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆斐波那契堆等。



堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。

(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2),
满足前者的表达式的成为小顶堆,满足后者表达式的为大顶堆,这两者的结构图可以用完全二叉树排列出来

8、图

图型结构也称图案,指个体目标重复排列的空间形式。图案反映了地物的空间分布特征,它可以是自然的,也可以是人为构造的 [1] 图形结构,简称“图”,是一种复杂的数据结构。图形结构中,每个结点的前驱结点数和后续结点数可以任意多个。


数据元素间的关系是任意的。其他数据结构(如树、线性表等)都有明确的条件限制,而图形结构中任意两个数据元素间均可相关联。常用来研究生产流程、施工计划、各种网络建设等问题。


转自:https://www.jianshu.com/p/4013774d929d
收起阅读 »

面试之链表

前言这一篇博客是很早之前写的,是关于一些链表和二叉树面试相关的问题,算是整理吧,网上这部分的答案也很多,希望能给大家一些帮助。注意:本文中一些异常情况都是没有做处理的,例如NULL等等,只是给出了基本的解决方案.大家参考一下.链表部分问题:定义并且创建一个链表...
继续阅读 »

前言
这一篇博客是很早之前写的,是关于一些链表和二叉树面试相关的问题,算是整理吧,网上这部分的答案也很多,希望能给大家一些帮助。
注意:本文中一些异常情况都是没有做处理的,例如NULL等等,只是给出了基本的解决方案.大家参考一下.

链表部分
问题:定义并且创建一个链表.
解题方案:
我们首先要如何定义一个结构体,下面的构造方案我是使用的递归的形式来构造一个结构体,注意不要忘记分配内存.其他的方面都比较简单,难度较低.
代码示例:

#include <stdio.h>

typedef struct ListNode {
int data;
struct ListNode*nextNode;
}ListNode;

ListNode* createListNodeAction(int *listArray, int index, int length) {
ListNode *listNode = (ListNode *) malloc(sizeof (ListNode) );
ListNode *nextNode = NULL;
int i = listArray[index];
listNode->data = i;
if (index != length - 1) {
nextNode = (ListNode*) malloc(sizeof (ListNode));
nextNode = createListNodeAction(listArray, index + 1, length);
}
listNode->nextNode = nextNode;
return listNode;
}

问题:不通过遍历删除链表中的非尾节点.

解题方案:
首先我们要知道我们如何通过遍历删除链表中的某个节点? 通过遍历我们可以知道要删除的链表节点前驱(也就是前一个节点),然后我们把前驱的nextNode指向要删除的节点的nextNode,释放要删除的节点即可.示意图如下所示.


那么我们对于上面的那个题目,我们该如何解决呢?由于前驱不通过遍历我们是拿不到的,所以我们只能通过覆盖的形式,用nextNode节点的属性覆盖掉需要删除的节点,然后释放nextNode节点,这样就完成了删除工作,由于前驱的nextNode指针属性不通过遍历修改不了,所以不能删除尾节点.否则就会有野指针问题出现.

void deleteListNodeNotTail(ListNode *deleteNode) {

ListNode *deleteNextNode = deleteNode->nextNode;
deleteNode->data = deleteNextNode->data;
deleteNode->nextNode = deleteNextNode->nextNode;
free(deleteNextNode);
}

问题:只遍历一次就找到链表中的中间节点.

解题方案:
撇开题目不谈,我们首先要清楚如何确定链表中的中间节点?由于链表没有长度的属性,所以暴力法的做法就是先遍历一次确定链表的长度,然后再次遍历链表找到中间节点.时间复杂度为O(logn+n).
那么如何通过一次遍历来找到链表中的中间节点呢?我们的解决方案是我们需要一快一慢两个移动节点fathNode和slowNode,fathNode的偏移速度是slowNode的两倍,,所以当fathNode == NULL,slowNode正好处于中心节点上.时间复杂度为O(logn).
代码示例:

ListNode* getListHalfNode(ListNode *listNode) {

ListNode *fathNode = listNode->nextNode;
ListNode *slowNode = listNode;

while (fathNode) {
fathNode = fathNode->nextNode->nextNode;
slowNode = slowNode->nextNode;
}
return slowNode;
}

问题:如何找到单向链表中的倒数第i个节点(i >= 1).

解题方案:
暴力法该如何解决这种问题呢?我们先遍历一遍确定链表的长度length,再次遍历链表取得下标位置在length-1-k的节点就是我们要的节点.时间复杂度为O(logn+n).
那没有有没有优化方式呢?这是有的,仍然借助上一个问题的解决方案,我们需要一快一慢两个移动节点fathNode和slowNode,fathNode先偏移i个位置,然后两个节点同时进行移动,所以当fathNode == NULL,slowNode正好处于倒数.时间复杂度为O(2logn).
代码示例:

ListNode* getListNodeWithLast(ListNode *listNode,int i) {

ListNode *fathNode = listNode;
ListNode *slowNode = listNode;

while (i) {
fathNode = fathNode->nextNode;
i--;
}

while (fathNode) {
fathNode = fathNode->nextNode;
slowNode = slowNode->nextNode;
}
return slowNode;
}

问题:删除倒数第i个结点(i>=1),不能用替换删除法.

解题方案:
上面我们已经了解了替换删除法,不需要知道前驱,我们就可以使用覆盖替换的方式删除节点,而这次我们可以是知道前驱节点的,而且结合上一次的快慢节点的方式,我们只需要先找到前驱节点即可.也就是fathNode节点需要先移动i + 1 次,具体代码如下所示.
代码示例:

void deleteListNodeWithLast(ListNode *listNode,int i) {

ListNode *fathNode = listNode;
ListNode *slowNode = listNode;

while (i + 1) {
fathNode = fathNode->nextNode;
i--;
}

while (fathNode) {
fathNode = fathNode->nextNode;
slowNode = slowNode->nextNode;
}

ListNode *deleteNode = slowNode->nextNode;
ListNode *deleteNextNode = deleteNode->nextNode;
slowNode->nextNode = deleteNextNode;
free(deleteNode);
}

问题:约瑟夫问题

约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解。
解题方案:
使用链表该如何解决约瑟夫问题呢?我们需要把链表做成一个环,也就是我们需要遍历一遍找到尾节点,并且制定尾节点的nextNode指针指向链表的第一个节点,这样我们就把链表做成了一个环.
然后我们假设每i次删除一个节点,这样返回的删除,直到只剩最后一个节点就是我们要求的解.
代码示例:

ListNode* JocephCircle(ListNode *firstNode, int k) {

ListNode *endNode = firstNode;
ListNode *resultNode = firstNode;
ListNode *deleteNode = NULL;

// 做环
while (endNode->nextNode) {
endNode = endNode->nextNode;
}
endNode->nextNode = firstNode;

// 自身的nextNode指向自身的时候,就只剩下一个元素了
while (resultNode->nextNode != resultNode) {

//删除节点 ,先找到前驱节点,然后找到删除节点
//由于先执行赋值操作,再进行i-1操作,所以k-1,由于是找删除节点的前驱节点,所以还需要-1.
int i = (k-1)-1;
while (i) {
resultNode = resultNode->nextNode;
i--;
}

// 重新指向并且释放删除节点
deleteNode = resultNode->nextNode;
resultNode->nextNode = resultNode->nextNode->nextNode;
free(deleteNode);
resultNode = resultNode->nextNode;
}

return resultNode;
}

问题:单链表的冒泡排序问题

解题方案:
仿照普通的数组遍历,这里两个while进行实现简单的冒泡排序.判断条件为nextNode节点是否为NULL,即可知道是否已经到达了单链表的尾节点.这个问题如果不做任何优化的话就如同下面代码演示的即可.其他优化方式就不过多阐述,上网查询即可.
代码示例:

void sortNodeListAction(ListNode *firstNode) {

ListNode *nowNode = firstNode;
ListNode *exchangeNode = (ListNode *)malloc(sizeof(ListNode));

while (nowNode->nextNode) {
ListNode *nowNextNode = nowNode;
while (nowNextNode) {
if (nowNextNode->data < nowNode->data) {
exchangeNode->data = nowNextNode->data;
nowNextNode->data = nowNode->data;
nowNode->data = exchangeNode->data;
}
nowNextNode = nowNextNode->nextNode;
if (!nowNextNode) {
continue;
}
}
nowNode = nowNode->nextNode;
}
free(exchangeNode);
}

问题:判断链表是否带环;若带环,求环的长度和入口点

解题方案:
这里我们要首先明白什么叫做带环,如下图所示,不管是哪种表现形式,我们都说当前链表是带环的链表.


我们了解了什么叫链表带环.在代码中,我们该如何判断当前的链表是否带环呢?网上有一种方案就是使用快慢节点解决,设置fathNode和slowNode,fathNode的偏移速度是slowNode的两倍,所以当fathNode == NULL,那么可以断定链表不带环,假设在某一个时刻fathNode==slowNode,说明两个节点重合,也就是说链表带环.
那么带环的链表我们该如何判断其环的长度呢?首先我们要知道fathNode偏移速度是slowNode的两倍,也就是说相同时间内,fathNode偏移距离是slowNode的2倍.
我们要说明两个节点交汇的情况,两者的情况肯定是慢节点在换上走不到一圈就会进行交汇,有人会问这是为什么呢?因为fathNode偏移速度是slowNode的两倍,所以在两者起点相同的情况下slowNode走完一圈fathNode走完两圈内,两者是必然相交的.
根据上面的两种情形,如下图所示.当两点相交时,我们有以下的结论,fathNode走过的路程为L + (C + A) + A,slowNode走过的路程为L + A, 我们得出 (L + A) x 2 = L + (C + A) + A;所以L = C.这时候我们继续定义一个新的节点enterNode从头开始出发,slowNode同时出发,两者速度相同,同时L = C;所以我们知道两者相交的节点必然是环的入口点.这时候enterNode再走到b点,就可以计算出环的长度了.

代码示例:

// 判断是否有环
bool isExistLoop(ListNode* firstNode) {
ListNode *fastNode;
ListNode * slowNode;
fastNode = slowNode = firstNode;
while (slowNode != NULL && fastNode -> next != NULL) {
slowNode = slowNode -> next ;
fastNode = fastNode -> next -> next ;
if (slowNode == fastNode)
return true ;
}
return false ;
}
// 判断环的长度
int getLoopLength(ListNode* firstNode){
ListNode* slowNode = firstNode;
ListNode* fastNode = firstNode;
while ( fastNode && fastNode ->next ){
slowNode = slowNode->next;
fastNode = fastNode->next->next;
if ( slowNode== fastNode) {
break;
}
}
slowNode= slowNode->next;
fastNode = fastNode->next->next;
int length = 1;
while ( fastNode != slowNode)
{
slowNode = slowNode->next;
fastNode = fastNode->next->next;
length ++;
}
return length;
}
// 找到环中的相遇节点
ListNode* getMeetingNode(ListNode* firstNode) {
ListNode* fastNode;
ListNode* slowNode;
slowNode = fastNode = firstNode;
while (slowNode != NULL && fastNode-> next != NULL) {
slowNode = slowNode-> next ;
fastNode = fastNode-> next -> next ;
if (slowNode == fastNode)
return slowNode;
}

//到达末尾仍然没有相遇,则不存在环
return NULL ;
}
// 找出环的入口节点
ListNode* getEntryNodeOfLoop(ListNode* firstNode) {
ListNode* meetingNode = getMeetingNode(firstNode); // 先找出环中的相遇节点
if (meetingNode == NULL)
return NULL;
ListNode* p1 = meetingNode;
ListNode* p2 = pHead;
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1;
}

如果可以使用字典或者集合的话,那就更简单了;数组也是可以解决,但是效率不是太高.需要多次遍历.

总结

OK,写到这里基本上就结束了,先整理这些后期会持续更新,欢迎大家指导批评,谢谢。。。

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

收起阅读 »

Flutter集成到Swift老项目 使用pod接入flutter

Xcode:Version 11.3.1 (11C504)Swift:5.0iOS项目地址Flutter项目创建cd some/path/flutter create --template module flutter_yyframework文件路径如下:cd...
继续阅读 »

Xcode:Version 11.3.1 (11C504)
Swift:5.0
iOS项目地址
Flutter项目创建

cd some/path/
flutter create --template module flutter_yyframework

文件路径如下:


cd 到你要混编的项目(YYFramework)同一个路径下 ,执行如下:

flutter create -t module flutter_yyframework

Podfile 文件

#注意路径和文件夹名字正确无误 最后有一个反斜杠
flutter_application_path = '/Users/houjianan/Documents/GitHub/iOS/flutter_yyframework/'
load File.join(flutter_application_path, 'YYFramework', 'Flutter', 'podhelper.rb')

target 'YYFramework' do
install_all_flutter_pods(flutter_application_path)

end

注:YYFramework 是iOS项目的文件名
添加好之后

pod install

注意,如下错误:[!] InvalidPodfilefile: No such file or directory @ rb_sysopen - ./flutter_yyframework/.ios/Flutter/podhelper.rb.
需要在flutter_yyframework文件夹下执行以下命令,把.ios和.android等flutter配置生成出来。(打开模拟器。链接真机都可以。)

open -a Simulator
flutter run

注意,如下错误是因为路径不对。

[!] Invalid `Podfile` file: cannot load such file -- path/to/flutter_yyframework/.ios/Flutter/podhelper.rb.

# from /Users/houjianan/Documents/GitHub/iOS/YYFramework/Podfile:7
# -------------------------------------------
# flutter_application_path = 'path/to/flutter_yyframework/'
> load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
#
# -------------------------------------------
houjianan:YYFramework> pod install
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing flutter_yyframework (0.0.1)
Generating Pods project
Integrating client project
Pod installation complete! There are 42 dependencies from the Podfile and 51 total pods installed.
houjianan:YYFramework>

iOS Swift代码

//
// AppDelegate.swift
// YYFramework
//
// Created by houjianan on 2018/8/11.
// Copyright © 2018年 houjianan. All rights reserved.
//

import UIKit
import SwiftTheme
import PLShortVideoKit
import Flutter
import FlutterPluginRegistrant // Used to connect plugins.

@UIApplicationMain
// 集成FlutterAppDelegate之后代理方法要override
class AppDelegate: FlutterAppDelegate {

lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(NSHomeDirectory())
flutter_run()
return true
}

}
//
// AppDelegate+Flutter.swift
// YYFramework
//
// Created by houjianan on 2020/1/20.
// Copyright © 2020 houjianan. All rights reserved.
//

import Foundation
import Flutter
import FlutterPluginRegistrant // Used to connect plugins.

extension AppDelegate {

func flutter_run() {
flutterEngine.run()
GeneratedPluginRegistrant.register(with: self.flutterEngine)
}
}
//
// GAFlutterRooterViewController.swift
// YYFramework
//
// Created by houjianan on 2020/1/20.
// Copyright © 2020 houjianan. All rights reserved.
//

import UIKit
import Flutter

class GAFlutterRooterViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

}

@IBAction func bAction(_ sender: Any) {
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}

@IBAction func cAction(_ sender: Any) {
let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)

flutterViewController.setInitialRoute("MyApp")

let channel = FlutterMethodChannel(name: "com.pages.your/native_get", binaryMessenger: flutterViewController as! FlutterBinaryMessenger)
channel.setMethodCallHandler { (call, result) in
print("method = ", call.method, "arguments = ", call.arguments ?? "argumentsNULL", result)

let method = call.method
if method == "FlutterPopIOS" {
print("FlutterPopIOS:返回来传的参数是 == ", call.arguments ?? "argumentsNULL")
self.navigationController?.popViewController(animated: true)
} else if method == "FlutterCickedActionPushIOSNewVC" {
print("FlutterCickedActionPushIOSNewVC:返回来传的参数是 == ", call.arguments ?? "argumentsNULL")
let vc = GAVerificationCodeViewController(nibName: "GAVerificationCodeViewController", bundle: nil)
self.navigationController?.pushViewController(vc, animated: true)
} else if method == "FlutterGetIOSArguments" {
let dic = ["a":"value"]
print("传参给Flutter:", dic)
result(dic)
} else {

}

}
self.navigationController?.pushViewController(flutterViewController, animated: true)
}
}

Flutter代码

import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:bot_toast/bot_toast.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return BotToastInit(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorObservers: [BotToastNavigatorObserver()],
home: MyHomePage(title: '1235777'),
),
);
}
}


class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
String _textString = "00";

void _incrementCounter() {
setState(() {
_counter++;
});
}

// 创建一个给native的channel (类似iOS的通知)
static const MethodChannel methodChannel = MethodChannel('com.pages.your/native_get');

_iOSPushToVC() async {
await methodChannel.invokeMethod('FlutterPopIOS', '参数');
}

void _backAction() {
_iOSPushToVC();
}

void _pushIOSNewVC() async {
Map<String, dynamic> map = {"code": "200", "data":[1,2,3]};

await methodChannel.invokeMethod('FlutterCickedActionPushIOSNewVC', map);
}

Future<void> _FlutterGetIOSArguments(para) async {
BotToast.showText(text:"_FlutterGetIOSArguments");
try {
final result = await methodChannel.invokeMethod('FlutterGetIOSArguments', para);


BotToast.showText(text:result["a"]);
_textString = result["a"];
} on PlatformException catch (error) {
print(error);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times1:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
FloatingActionButton(
onPressed: _backAction,
child: Icon(Icons.accessibility),
),
FloatingActionButton(
onPressed: _pushIOSNewVC,
child: Icon(Icons.accessibility),
),
FloatingActionButton(
onPressed:() {
_FlutterGetIOSArguments("flutter传值");
// 刷新界面
setState(() {});
},
child: Icon(Icons.accessibility),
),
Text(_textString),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

官网Integrate a Flutter module into your iOS project
很久之前写的一篇《Flutter和原生iOS交互》

过年了,有点时间玩Flutter!
Flutter统治全世界。

转自:https://www.jianshu.com/p/351bd8ecbc79

收起阅读 »

iOS 音频播放iOS13上远程控制设置控制方法崩溃

使用MPRemoteCommandCenter 处理远程音频事件的播放的时候,有些同学会用[pauseCommand addTarget:self action:@selector(remotePauseEvent)]这个方法来处理,但是在iOS13后苹果官方...
继续阅读 »

使用MPRemoteCommandCenter 处理远程音频事件的播放的时候,
有些同学会用[pauseCommand addTarget:self action:@selector(remotePauseEvent)]这个方法来处理,但是在iOS13后苹果官方在这个方法有要求了,官方文档这么写的

// Target-action style for adding handlers to commands.
// Actions receive an MPRemoteCommandEvent as the first parameter.
// Targets are not retained by addTarget:action:, and should be removed from the
// command when the target is deallocated.
//
// Your selector should return a MPRemoteCommandHandlerStatus value when
// possible. This allows the system to respond appropriately to commands that
// may not have been able to be executed in accordance with the application's
// current state
翻译一下其实意思就是 建议用addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler; 这个方法来为其添加本地事件处理,但是也可以用- (void)addTarget:(id)target action:(SEL)action;方法来处理,用- (void)addTarget:(id)target action:(SEL)action; 方法处理时候需要返回MPRemoteCommandHandlerStatus这个值.

意思就是这样了,根据这样的翻译可以很明确知道该怎么解决,要不换- (void)addTarget:(id)target action:(SEL)action;方法为- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler;要不就在- (void)addTarget:(id)target action:(SEL)action;的引用方法里添加返回值,例如:

- (MPRemoteCommandHandlerStatus)remotePauseEvent {

return MPRemoteCommandHandlerStatusSuccess;
}

参考至这里


转自:https://www.jianshu.com/p/40cd3e7b05bb

收起阅读 »

iOS _OBJC_CLASS_$_NSEntityDescription报错

最近项目中有使用到,MJ相关系列的库,结果出现了报错,如下:Undefined symbol: _OBJC_CLASS_$_NSEntityDescriptionUndefined symbol: _OBJC_CLASS_$_NSManagedObject通过...
继续阅读 »

最近项目中有使用到,MJ相关系列的库,结果出现了报错,如下:

Undefined symbol: _OBJC_CLASS_$_NSEntityDescription

Undefined symbol: _OBJC_CLASS_$_NSManagedObject


通过网上查资料,才知道,自己缺少了coredata库文件,所以才会报这个错误,在项目的这个地方引入,coredata库,即可解决此问题

引入coredata库

收起阅读 »

iOS之手写单例

一、 不严谨写法先附上不严谨的创建单例的写法SignalModel.h@interface SignalModel : NSObject+ (instancetype)shareInstance;@endSignalModel.m@implementation...
继续阅读 »

一、 不严谨写法

先附上不严谨的创建单例的写法

  • SignalModel.h
@interface SignalModel : NSObject
+ (instancetype)shareInstance;
@end
  • SignalModel.m
@implementation SignalModel

+ (instancetype)shareInstance {
static SignalModel *_instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[SignalModel alloc] init];
});
return _instance;
}

外界使用

SignalModel *signal1 = [[SignalModel alloc] init];
SignalModel *signal2 = [SignalModel shareInstance];
SignalModel *signal3 = [SignalModel shareInstance];
NSLog(@"\nsignal1 = %@\nsignal2 = %@\nsignal3 = %@\n",signal1,signal2,signal3);

打印结果


1. 通过上面的测试,可以看到通过shareInstance方法获取的对象是相同的,但是用alloc和init构造对象时,得到的对象却是不一样的。

2. 通过不同的方式获得不同的对象,是有问题的,所以要封锁初始化的方式,如alloc,copy,mutableCopy,new

摘抄的原理哈
创建对象的步骤分为申请内存(alloc)、初始化(init)这两个步骤,我们要确保对象的唯一性,因此在第一步这个阶段我们就要拦截它。当我们调用alloc方法时,OC内部会调用allocWithZone这个方法来申请内存,我们覆写这个方法,然后在这个方法中调用shareInstance方法返回单例对象,这样就可以达到我们的目的。拷贝对象也是同样的原理,覆写copyWithZone方法,然后在这个方法中调用shareInstance方法返回单例对象

二、正确写法

  • SignalModel.m
// 实现copy协议
@interface SignalModel()<NSCopying, NSMutableCopying>

@end

+ (instancetype)shareInstance {
static SignalModel *_instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[super allocWithZone:NULL] init];
});
return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self shareInstance];
}

- (id)copyWithZone:(NSZone *)zone {
return self;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}

1. shareInstance单例方法中,变量的初始化改成[[super allocWithZone:NULL] init]

2. 实现copyWithZone:和mutableCopyWithZone:方法

测试代码

SignalModel *signal1 = [[SignalModel alloc] init];
SignalModel *signal2 = [SignalModel shareInstance];
SignalModel *signal3 = [SignalModel shareInstance];
SignalModel *signal4 = [SignalModel new];
SignalModel *signal5 = [signal1 copy];
SignalModel *signal6 = [signal2 mutableCopy];

NSLog(@"\nsignal1 = %@\nsignal2 = %@\nsignal3 = %@\nsignal4 = %@\nsignal5 = %@\nsignal6 = %@",signal1,signal2,signal3,signal4,signal5,signal6);

打印结果


无论通过哪种方式创建出来的实例对象,其内存地址都是一样的,所以该种写法才是严谨的。

转自:https://juejin.cn/post/6844903806027694087


收起阅读 »

浅谈Constraints,Layout,Display的点点滴滴

浅谈Constraints,Layout,Display的点点滴滴神经骚栋关注赞赏支持前言这篇博客完全是因为 浅谈Masonry的使用技巧 才引出来的,如果不是内容太多,也不会单独写一篇博客来记录,在9102一整年中我基本与普通UI开发无缘,大部分工作是对La...
继续阅读 »

浅谈Constraints,Layout,Display的点点滴滴

神经骚栋

前言

这篇博客完全是因为 浅谈Masonry的使用技巧 才引出来的,如果不是内容太多,也不会单独写一篇博客来记录,在9102一整年中我基本与普通UI开发无缘,大部分工作是对Layout进行操作绘制,以及使用CoreGraphics框架绘制各种图形,所以对Layout和Display的系统方法还是比较了解,近期又开始使用Masonry,所以对Constraints相关系统方法需要有所了解,而且在 浅谈Masonry的使用技巧 这篇博客中的优化部分不得不提出Constraints相关系统方法对其的影响。那么我们依照惯例,从基础的API开始进行吧。这里我基本上是从苹果的API抄录过来的,各位大佬可以自行去苹果API中心查看。

基础API方法介绍

Constraints 部分

  • needsUpdateConstraints 这个方法主要是用来判断当前View是否需要调用 updateConstraints 方法,如果在这个方法调用之前有约束发生了改变,调用这个方法返回值就是YES,但是它不会触发updateConstraints 方法的执行,只用用来判别当前控件是否需要调用更新约束方法。

- (BOOL)needsUpdateConstraints;
  • setNeedsUpdateConstraints 这个方法主要是用来标记当前控件是否需要调用updateConstraints 方法,看好了,只是标记,而不是调用。 当给控件标记上需要刷新约束,如果程序没有直接强制刷新(调用updateConstraintsIfNeeded),那么会在系统RunLoop的下个周期开始时调用 updateConstraints 方法。

- (void)setNeedsUpdateConstraints;
  • updateConstraints 这个方法是更新约束方法,在自定义控件中我们可以重写这个方法来添加我们自己的约束,对于updateConstraints 调用时机只能会有两种,一个是系统RunLoop的下个周期当发现需要更新布局(needsUpdateConstraints的值为YES)的时候,会自动调用该方法,或者是大佬们手动调用 updateConstraintsIfNeeded 来进行直接刷新。

- (void)updateConstraints;
  • updateConstraintsIfNeeded 这个方法是直接强制立即调用 updateConstraints 方法,不需要等待Runloop下个周期,也不会管 needsUpdateConstraints 的返回值是否为YES,反正就是强制执行就对了。

- (void)updateConstraintsIfNeeded;
  • updateViewConstraints 这个方法是在iOS6之后添加到UIViewController中的,具体作用和updateConstraints 方法类似。调用时机是 self.view 的 needsUpdateConstraints 值为YES 或 [self.view updateConstraintsIfNeeded]; 都可。这个方法极大的方便了控制器本身的View的布局调整。

- (void)updateConstraintsIfNeeded;

Layout 部分

  • layoutSubviews 这个方法是动态调整子视图的布局,这个方法的调用时机是当前控件或者子控件的bounds 发生改变的时候就会调用。

- (void)layoutSubviews;
  • layoutIfNeeded 是立即强制执行layout操作的方法,但layoutSubviews 可能不会执行,因为如果控件或者子控件的bounds 没有发生改变时,layoutSubviews是不会执行的,所以说控件或者子控件的bounds 发生改变是 layoutIfNeeded 调起 layoutSubviews 的前提条件。

- (void)layoutIfNeeded;
  • setNeedsLayout 这个方法和上面的setNeedsUpdateConstraints作用类似,但它不是用来标记的,而是让布局失效的(可以看做间接导致了bounds的改变),所以如果在其调用下方调用 layoutIfNeeded 会立即调起 layoutSubviews 方法,或者等待Runloop下一个周期由系统调起 layoutSubviews。

- (void) setNeedsLayout;

Display部分

  • drawRect 这个方法主要是当View控件需要自定义绘制内容的时候,一般会写在这个方法中。绘制上下文对象需要通过 UIGraphicsGetCurrentContext() 函数来获取,官方文档 中都写的明明白白的了(内容太多了,懒癌发作,大家自行去看吧)。这里就不过多叙述了。

- (void)drawRect:(CGRect)rect;
  • drawInContext 这个方面主要是CALayer中自定义绘制内容的时候,一般都会写在这个方法中。ctx 这个参数是绘制上下文对象,不需要额外获取了。

- (void)drawInContext:(CGContextRef)ctx;
  • setNeedsDisplay 这个方面主要是标记UIView或CALayer是否要刷新,看好了,是标记!而不是直接刷新,其作用和 setNeedsUpdateConstraints 非常的类似。

- (void)setNeedsDisplay;
  • displayIfNeeded 这个方法主要让CALayer直接进行强制绘制,UIView中没有该方法。所以UIView的重新绘制只能先使用setNeedsDisplay来标记,等待系统RunLoop的下一个周期开始进行重绘。

- (void)displayIfNeeded;
  • needsDisplay 这个方法判别CALayer是否需要刷新,只是用来判断,没有别的作用。UIView没有该方法。

- (BOOL)needsDisplay;
  • display 官方建议不要直接调用这个方法,CALayer对象绘制适当的时机调用此方法来绘制CALayer对象其中的内容。

- (void)display;

Auto Layout Process 自动布局过程

那么三种是如何关联起来的呢?主要是通过 Auto Layout Process 来关联在一起的,网上这个资料很多,苹果官方的我没有找到的在哪,所以我找到了最开始的版本 ,具体内容如下所示。

与使用springs and struts(autoresizingMask)比较,Auto layout在view显示之前,多引入了两个步骤:updating constraints 和laying out views。每一个步骤都依赖于上一个。display依赖layout,而layout依赖updating constraints。 updating constraints→layout→display

第一步:updating constraints,被称为测量阶段,其从下向上(from subview to super view),为下一步layout准备信息。可以通过调用方法setNeedUpdateConstraints去触发此步。constraints的改变也会自动的触发此步。但是,当你自定义view的时候,如果一些改变可能会影响到布局的时候,通常需要自己去通知Auto layout,updateConstraintsIfNeeded。

自定义view的话,通常可以重写updateConstraints方法,在其中可以添加view需要的局部的contraints。

第二步:layout,其从上向下(from super view to subview),此步主要应用上一步的信息去设置view的center和bounds。可以通过调用setNeedsLayout去触发此步骤,此方法不会立即应用layout。如果想要系统立即的更新layout,可以调用layoutIfNeeded。另外,自定义view可以重写方法layoutSubViews来在layout的工程中得到更多的定制化效果。

第三步:display,此步时把view渲染到屏幕上,它与你是否使用Auto layout无关,其操作是从上向下(from super view to subview),通过调用setNeedsDisplay触发,

因为每一步都依赖前一步,因此一个display可能会触发layout,当有任何layout没有被处理的时候,同理,layout可能会触发updating constraints,当constraint system更新改变的时候。

需要注意的是,这三步不是单向的,constraint-based layout是一个迭代的过程,layout过程中,可能去改变constraints,有一次触发updating constraints,进行一轮layout过程。示意图如下所示。


原作者也提到了另外的一个坑,那就是如果你每一次调用自定义layoutSubviews都会导致另一个布局传递,那么你将会陷入一个无限循环中。 这其中主要原因还是constraint-based layout是一个迭代的过程,在上图的 updating constraints 和 layout 中成了一个死循环了。

视图渲染流程

由上一个模块我们可以得知一个View视图的调用顺序为 updating constraints→layout→display,那么对应到具体方法就是 updateConstraints→layoutSubViews→drawRect:

再详细的说一下,那就是,当我们修改View视图约束的时候,会触发 setNeedsUpdateConstraints 方法,然后触发 updateConstraints 方法,随后就紧接着触发 layoutSubViews,同时苹果官方已经为我们暴露了UIViewController中本身View视图的updateConstraints上层方法 updateViewConstraints,当UIViewController中本身View视图setNeedUpdate Constraints被调用的时候,这时候就会在合适的时机自动调用updateViewConstraints方法.

反观UIViewController的生命周期流程,我们可以具体到如下表格顺序.


触发时机分析

这个模块我们就触发时机再总结一下,其实在上面的基础API的方法中都介绍了,但是比较杂乱,

updateViewConstraints 与 updateConstraints
这两个的触发时机是一致的,那么就是当 调用 needsUpdateConstraints 值为YES 的时候,就肯定会调用updateViewConstraints 或者 updateConstraints.那么在View内部的这个判别布尔值又是由什么决定呢?情况一是添加,修改,删除约束的时机,二是手动调用 setNeedsUpdateConstraints 的时机.这两种时机都会造成布尔值发生改变从而调起 updateViewConstraints 或 updateConstraints .

layoutSubviews
layoutSubviews的触发时机只有一种情况,那就是 自身或者子视图的 bounds 发生了改变. .这也解释了当我们创建一个视图的时候如果使用的CGRectZero的时候实际上不会调用 layoutSubviews 方法.

drawRect 与 drawInContext
这两个方法的调用时机又和 updateViewConstraints 与 updateConstraints 非常的相似,只有当 调用 setNeedsDisplay 才会触发调用.但两者又有很大的区别.drawRect是UIView中的方法,drawInContext是CALayer中方法,drawRect调用时机智能是RunLoop的下一个周期开始,不能立即调用,但是drawInContext却可以通过直接调用displayIfNeeded开直接调用,不用等待RunLoop的下一个周期开始.而且 needsDisplay 只是CALayer中的方法,UIView没有此方法.

总结

OK,写到这里基本上系统的各种约束,布局,绘制API大家都了解的差不多了,这对我们后期代码时机的把握有着很好的帮助,欢迎各位大佬自己手动试验,欢迎大家在评论区指导批评.

转自:https://www.jianshu.com/p/983f2237cfa7

收起阅读 »

iOS 多线程之performSelector、死锁

1. performSelector//在当前线程延迟1s执行,响应了OC语言的动态性:延迟到运行时才绑定方法[self performSelector:@selector(aaa) withObject:nil afterDelay:1];// 回到主线程,...
继续阅读 »

1. performSelector

//在当前线程延迟1s执行,响应了OC语言的动态性:延迟到运行时才绑定方法
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
// 回到主线程,waitUntilDone:是否将该回调方法执行完再执行后面的代码
// 如果为YES:就必须等回调方法执行完成之后才能执行后面的代码,说白了就是阻塞当前的线程
// 如果是NO:就是不等回调方法结束,不会阻塞当前线程
[self performSelectorOnMainThread:@selector(aaa) withObject:nil waitUntilDone:YES];
// 开辟子线程
[self performSelectorInBackground:@selector(aaa) withObject:nil];
//在指定线程执行
[self performSelector:@selector(aaa) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];
  • 需要注意的是:如果是带afterDelay的延时函数,会在内部创建一个NSTimer,然后添加到当前线程的Runloop中。也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)

[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
  • performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行

  • 下面代码片段的test方法会去执行吗?

dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
});

这里的test方法是不会去执行的,原因在于- (void)performSelector: withObject: afterDelay:这个方法要创建提交任务到runloop上的,而gcd底层创建的线程是默认没有开启对应runloop的,所有这个方法就会失效。
而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行(将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的)

2. 死锁

  • 死锁就是队列引起的循环等待,一个比较常见的死锁例子:主队列同步

- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
}
  • 在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。
    想避免这种死锁,可以将同步改成异步dispatch_async或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决

  • 同样,下边的代码也会造成死锁:

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});

外面的函数无论是同步还是异步都会造成死锁。这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁。
解决方法也和上边一样,将里面的同步改成异步dispatch_async或者将serialQueue换成其他串行或并行队列,都可以解决

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

收起阅读 »

ios设计模式之简单工厂模式

最近一直在阅读OC编程之道(ios设计模式解析)一书(往期文章中我也将电子版的下载链接分享了出来)。其中包括23种设计模式和7种设计原则,如下图(此图为网络图片):在这里不过多的介绍设计模式和设计原则的问题了,感兴趣的同学可以自行去查阅资料,我在这里只介绍一种...
继续阅读 »

最近一直在阅读OC编程之道(ios设计模式解析)一书(往期文章中我也将电子版的下载链接分享了出来)。其中包括23种设计模式和7种设计原则,如下图(此图为网络图片):


  • 在这里不过多的介绍设计模式和设计原则的问题了,感兴趣的同学可以自行去查阅资料,我在这里只介绍一种设计模式及其代码,那就是简单工厂模式。

  • 何时使用工厂方法

     1、编译时无法准确预期要创建的对象的类;

     2、类想让其子类决定在运行时创建什么;

     3、类有若干辅助类为其子类,而你想将返回哪个子类这一信息局部化;

  • 在Cocoa Touch框架中应用的工厂方法

     工厂方法在Cocoa Touch框架中几乎随处可见,常见的两步对象创建法[[SomeClass alloc] init]。有时,我们已经注意到有一些便利方法返回类的实例。例如,NSNumber有很多numberWith *方法,其中两个是numberWithBool:和numberWithChar:。他们是类方法,也就是说我们向NSNumber发送[NSNumber numberWithBool:YES],以获得与传入参数同类型的各种NSNumber实例。与如何创建NSNumber的具体子类型的实例有关的所有细节,都由NSNumber的类工厂方法负责。[NSNumber numberWithBool:YES]的情况是,方法接受值YES,并把NSNumber的内部子类的一个实例初始化,让它能够反映传入的值YES。我们曾提到有个工厂方法模式的变体,抽象类用它生成具体子类。NSNumber中的这些numberWith *方法是这个变体的一个例子。它们不是用来被NSNumber的私有子类重载的,而是NSNumber创建合适对象的便利方式。

  • 简单的代码逻辑,提现一下工程方法的模式

     文章中我拿生活中日常饮用的饮料来做例子,我们首先会有一个父类,名字是:Drinks,代表饮料。还有三个继承Drinks的子类,名字分别为:可乐CocaCola芬达Fender矿泉水MineralWater。然后我们会有一个专门的工厂类DrinksFactory来管理生产何种饮料,DrinksFactory通过一个枚举和一个方法来生产。

  • 下面我将代码呈现给大家(gitHub工程地址:https://github.com/TianTeng6661/SimpleFactory.git,喜欢的给个星,谢谢)

     1、Drinks类

import UIKit
class Drinks: NSObject {
func drinksColor(){
NSLog("饮料颜色")
}
}

      2、CocaCola类

import UIKit
class CocaCola: Drinks {
override func drinksColor(){
NSLog("可口可乐是褐色")
}
}

     3、Fender类

import UIKit
class Fender: Drinks {
override func drinksColor(){
NSLog("芬达是橙色")
}
}

     4、MineralWater类

import UIKit
class MineralWater: Drinks {
override func drinksColor(){
NSLog("矿泉水是透明色")
}
}

     5、DrinksFactory类

import UIKit

enum DrinkType:Int {
case DrinkCocaCola = 0 //可口可乐
case DrinkFender = 1 //芬达
case DrinkMineralWater = 3 //矿泉水
}

class DrinksFactory: NSObject {

func createDrinksWithType(drinkstype:DrinkType) -> Drinks {
switch drinkstype {
case .DrinkCocaCola:do {
let color = CocaCola()
return color;
}
case .DrinkFender:do{
let fender = Fender()
return fender;
}
case .DrinkMineralWater:do{
let mineralWater = MineralWater()
return mineralWater;
}
}
}
}

当我们在需要生产的时候我们调用方法为

import UIKit

class ViewController: UIViewController {

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

let cocaCola:Drinks = DrinksFactory().createDrinksWithType(drinkstype: DrinkType.DrinkCocaCola)
print(cocaCola .drinksColor()) //可口可乐是褐色
let fender:Drinks = DrinksFactory().createDrinksWithType(drinkstype: DrinkType.DrinkFender)
print(fender .drinksColor()) //芬达是橙色
let mineralWater:Drinks = DrinksFactory().createDrinksWithType(drinkstype: DrinkType.DrinkMineralWater)
print(mineralWater .drinksColor()) //矿泉水是透明色

}
}

这样我们就简单实现了一个工厂方法,通过一个工厂类来管理所有饮料的生产,而且我们在需要多生产一种类型的饮料的时候,只需要创建此饮料的类,然后在工厂方法中实现就OK了。

我在这里只是抛砖引玉,只是在讲解模式,这里面我们还有好多可以扩展的,比如不同的饮料有不同的口味,配方等等,共有的属性我们可以写在父类来实现,但是每个子类也可以有自己特有的属性。

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

    

收起阅读 »

Cocoapods 1.8 版本改用 CDN 服务

Cocoapods 1.7.2 版本开始增加 CDN 支持但默认没有启用,1.8 版本的发布舍弃了原始完整克隆的 Specs 仓库改用 CDN 服务。CDN 利用的是免费且强大的 jsDelivr CDN 服务,该 CDN 网络在国内是有备案因此速度和稳定性都...
继续阅读 »


Cocoapods 1.7.2 版本开始增加 CDN 支持但默认没有启用,
1.8 版本的发布舍弃了原始完整克隆的 Specs 仓库改用 CDN 服务。

CDN 利用的是免费且强大的 jsDelivr CDN 服务,该 CDN 网络在国内是有备案因此速度和稳定性都会有很好的保证。该提案其实在去年已经有人使用 Cocoapods Plugin 的方式实现并向社区贡献 PR。


那么 CDN 支持相比之前的机制有啥优势呢?难道是把 Pods 的仓库和源码都托管到 CDN 网络了吗,其实并不是的。


友情提醒:本文只重点分析 Pods 下载的机制,不展开其他方面,以下只是 pod install 执行顺序中的一部分,如果你想了解 Cocoapods 都干了什么可以前往这篇文章查阅。


老的机制


第一步先检查本地 ~/.cocoapods/repo/master 目录是否存在,没有直接克隆 https://github.com/Cocoapods/Specs.git 仓库,这步在国内来说特别费时间正常下载下来目录应该是 2G+,如果有其他 source 源(比如私有源)会重复刚才的操作。


第二步安装 Podfile 每个 Pod 去在各个源中寻找对应的版本,从版本的 .podspec 文件解析获取组件的地址,这个可能是 http、git、svn、hg 中的任意一个,获取到之后开始下载(默认是在 ~/Library/Caches/CocoaPods 做缓存目录)


新的机制


第一步分析 Podfile 里面的 source ,如果没有走默认 Cocoapods 的配置(1.8 以上是 https://cdn.cocoapods.org ,之前的还是 Cocoapods/Spec), 如果本地不存在官方 cdn 的 repo 名字是 trunk 的保留字,自己无法创建。如果有自定义的 source 会追加上去 sources 列表。


$httpHEAD0Cache-Control:public,max-age=0,must-revalidateConnection:keep-aliveContent-Length:924280Content-Type:text/plain;charset=UTF-8Date:Sat,09Nov201907:06:15GMTEtag:"acf0d284f3a8e82e0d66ba1a91cd30b9-ssl"Server:NetlifyStrict-Transport-Security:max-age=31536000X-NF-Request-ID:50b466cd-ce9e-4326-b5bb-0d29a193ae4b-7809449">https://cdn.cocoapods.org/all_pods.txtHTTP/1.1200OKAccept-Ranges:bytesAge:0Cache-Control:public,max-age=0,must-revalidateConnection:keep-aliveContent-Length:924280Content-Type:text/plain;charset=UTF-8Date:Sat,09Nov201907:06:15GMTEtag:”acf0d284f3a8e82e0d66ba1a91cd30b9-ssl”Server:NetlifyStrict-Transport-Security:max-age=31536000X-NF-Request-ID:50b466cd-ce9e-4326-b5bb-0d29a193ae4b-7809449

第二步检查或下载每个 source,每个 source 会检查是否是 cdn 类型(使用 HEAD 请求检查是否包含 /all_pods.txt)文件:


cdn 类型,下面详细解释


其他类型,走原来的老的逻辑,不再赘述


第三步,下载 Cocoapods-version.yml 并缓存 etag,下载 /Cocoapods-version.yml 并取 headers 的第一个 etag 的值存为 /Cocoapods-version.yml.etag,如果存在 etag 会比对一样就不需要下载, 链接支持根目录和其他目录,支持 301 跳转。


Cocoapods-version.yml


—-min:1.0.0last:1.8.4prefix_lengths:-1-1-1


第四步,分析 Pod 并获取 pod 的版本信息,比如 Podfile 我增加了一个 pod “AFNetworking”,把 pod 名字做 MD5 后的值取 Cocoapods-version.yml 的 prefiexlength 数组长度的值单字母拆分用下划线分割按照规则拼成文件名 all_pods_versions({fragment}).txt (如果prefix_length 为 0 则只会去下载 /all_pods_versions.txt)


比如:prefix_lengths 数组大小为 3,AFNetworking MD5 后 a75d452377f396bdc4b623a5df25820 则匹配前三位 a75 拆分后 a_7_5 后查找 cdn url 路径的 /all_pods_versions_a_7_5.txt 下载下来后的内容:


Fuse/0.1.0/0.2.0/1.0.0/1.1.0/1.2.0


GXFlowView/1.0.0


JFCountryPicker/0.0.1/0.0.2


JVEmptyElement/0.1.0


第五步,下载 pod 的所有版本的 .podspec 文件,从上面的文件按照每行寻找第一段的名字,把后面的所有版本按照上面获取到的 prefix_lengths 的值(例如 AFNetworking 是 a, 7 , 5) /Specs/a/7/5/AFNetworking/{version}/AFNetworking.podspec.json 一次下载,并保存 etag 为 /Specs/a/7/5/AFNetworking/{version}/AFNetworking.podspec.json.etag,这个 etag 作用上面已经讲过,如果没有找到的话就会直接报错。


AddingspecrepotrunkwithCDNhttps://cdn.cocoapods.org/ CDN:trunkRelativepathdownloaded:CocoaPods-version.yml,saveETag:”031c25b97a0aca21900087e355dcf663-ssl” CDN:trunkRelativepath:CocoaPods-version.ymlexists!Returninglocalbecausecheckingisonlyperfomedinrepoupdate CDN:trunkRelativepathdownloaded:all_pods_versions_a_7_5.txt,saveETag:”5b32718ecbe82b0ae71ab3c77120213f-ssl” CDN:trunkRedirectingfromhttps://cdn.cocoapods.org/Specs/a/7/5/AFNetworking/0.10.0/AFNetworking.podspec.jsontohttps://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/a/7/5/AFNetworking/0.10.0/AFNetworking.podspec.json CDN:trunkRelativepathdownloaded:Specs/a/7/5/AFNetworking/0.10.0/AFNetworking.podspec.json,saveETag:W/“a5f00eb1fdfdcab00b89e96bb81d48c110f09220063fdcf0b269290bffc18cf5”

Cocoapods trunk 源的目录结构:


.cocoapods repo trunk .url#=> https://cdn.cocoapods.org/Cocoapods-version.yml# => 从 https://cdn.cocoapods.org/CocoaPods-version.yml 下载的文件Cocoapods-version.yml.etag# 上一个请求的第一个 etag 值存下来all_pods_versions_a_7_5.txt# 参考上面的备注all_pods_versions_a_7_5.txt.etag# 上一个请求的第一个 etag 值存下来


第六步和老的机制第二步一样同样最终还是会寻找 podspec 里面下载地址去下载, 也就是说真正 CDN 缓存加速的只有原有 Specs 必要的 podspec 文件,而不会加速 Pod 真正源地址,改机制只是减轻了本地更新官方 Specs 源的麻烦以及维护一个巨大的本地文件存储,这也是中心化机制的一个心结。


结语


这个机制大大减少了本地需要占一个较大存储的问题,尤其是初次 pod install 时间长的情况,但 Pod 库本身还是各自的 地址本质上无法解决安装 Pod 消耗时间过长的问题。


via icyleaf


链接:https://www.jianshu.com/p/79c004614d06


收起阅读 »

iOS - UIStackView 布局 详解

一、UIStackView简介概念:一个堆叠视图的容器,iOS9的新特性。用途:StackView及其子视图会自适应界面,减少我们设置约束的工作量。特点:类似ContainView,不会渲染到界面上。StackView中的子视图只能朝一个方向进行排布,要么水平...
继续阅读 »

一、UIStackView简介

概念:

一个堆叠视图的容器,iOS9的新特性。
用途:StackView及其子视图会自适应界面,减少我们设置约束的工作量。

特点:

  • 类似ContainView,不会渲染到界面上。
  • StackView中的子视图只能朝一个方向进行排布,要么水平要么垂直。
  • StackView支持多层嵌套
  • 约束比StackView的自适应优先级高,可以通过设置约束来调整StackView的布局
  • 支持属性动画
  • 不能滚动

属性:

axis轴: -> 用来设置子视图的排列方式(H/V)
aligement: -> 用来设置子视图的对齐方式
distribution -> 用来设置子视图的分布方式(fill-填充)
spacing -> 子视图之间的间距

二、属性详解

1. axis

主要设置UIStackView布局的方向:水平方向或垂直方向。

typedefNS_ENUM(NSInteger,UILayoutConstraintAxis) {
UILayoutConstraintAxisHorizontal =0, //水平
UILayoutConstraintAxisVertical =1 //垂直
};
2. alignment

主要设置非轴方向子视图的对齐方式。

typedef NS_ENUM(NSInteger, UIStackViewAlignment) {
UIStackViewAlignmentFill, // 子视图填充
UIStackViewAlignmentLeading, // 子视图左对齐(axis为垂直方向而言)
UIStackViewAlignmentTop = UIStackViewAlignmentLeading, // 子视图顶部对齐(axis为水平方向而言)
UIStackViewAlignmentFirstBaseline, // 按照第一个子视图的文字的第一行对齐,同时保证高度最大的子视图底部对齐(只在axis为水平方向有效)
UIStackViewAlignmentCenter, // 子视图居中对齐
UIStackViewAlignmentTrailing, // 子视图右对齐(axis为垂直方向而言)
UIStackViewAlignmentBottom = UIStackViewAlignmentTrailing, // 子视图底部对齐(axis为水平方向而言)
UIStackViewAlignmentLastBaseline, // 按照最后一个子视图的文字的最后一行对齐,同时保证高度最大的子视图顶部对齐(只在axis为水平方向有效)
} API_AVAILABLE(ios(9.0));

具体显示效果如下:







3. distribution

设置轴方向上子视图的分布比例(如果axis是水平方向,也即设置子视图的宽度,如果axis是垂直方向,则是设置子视图的高度)。

typedef NS_ENUM(NSInteger, UIStackViewDistribution) {
UIStackViewDistributionFill = 0,
UIStackViewDistributionFillEqually,
UIStackViewDistributionFillProportionally,
UIStackViewDistributionEqualSpacing,
UIStackViewDistributionEqualCentering,
} API_AVAILABLE(ios(9.0));

下面以
axis = UILayoutConstraintAxisHorizontal,
alignment = UIStackViewAlignmentCenter
为例:

往UIStackView中添加三个UIView:

  1. 第一个UIView设为40*100
  2. 第二个UIView设为80*80
  3. 第三个UIView设为120*60

通过实例来说明每个属性的区别:

(1)UIStackViewDistributionFill = 0,默认属性,轴方向上填充UIStackView。如果axis为水平方向,则所有子视图的宽度等于UIStackView的宽,所以如果只有一个子视图,则子视图的宽度就等于UIStackView的宽,如果有两个子视图,且优先级一样,则会拉伸或压缩某个子视图,使两个子视图的宽度之和等于UIStackView的宽……,如果axis是垂直方向,则所有子视图的高度等于UIStackView的高,必要时会拉伸或压缩某个子视图。

上面是在子视图优先级一致的情况下,如果子视图优先级不一致,则会按优先级从高到低设置子视图的位置,对优先级最低的子视图进行必要的拉伸或压缩。

设置distribution = UIStackViewDistributionFill后显示效果:


UIStackViewDistributionFill

如图所示,由于三个子视图的宽度之和不够UIStackView的宽度,优先级又一致,所以第三个子视图被拉伸了。当然,我们可以修改某个子视图的优先级来让其被拉伸。

(2)UIStackViewDistributionFillEqually,该属性设置后使所有子视图在轴方向上等宽或等高。即如果是水平方向,所有子视图都会被必要的拉伸或压缩,使得每个子视图的宽度一致,原来设置的子视图的宽度都会被忽略;如果是垂直方向,所有子视图的高度也会保持一致,如下所示:


UIStackViewDistributionFillEqually

(3)UIStackViewDistributionFillProportionally 该属性设置后会根据原先子视图的比例来拉伸或压缩子视图的宽或高,如实例中三个子视图原先设置的宽度是1:2:3,所以水平方向上显示时,会按照这个比例进行拉伸,如下图所示,拉伸后的宽度依然是1:2:3。


UIStackViewDistributionFillProportionally

(4)UIStackViewDistributionEqualSpacing 该属性会保持子视图的宽高,所有子视图中间的间隔保持一致。如下图所示,图中子视图的间隔(绿线所示的长度)都是一致的。


UIStackViewDistributionEqualSpacing

(5)UIStackViewDistributionEqualCentering 该属性是控制所有子视图的中心之间的距离保持一致,如下图所示,子视图中心点之间的间隔(绿线所示的长度)是一致的。


UIStackViewDistributionEqualCentering

4. spacing

该属性控制子视图之间的间隔大小,在distribution前三个属性值设置的情况下,子视图之间是没有间隔,我们可以通过spacing属性显式的设置,如下图在distribution=UIStackViewDistributionFillEqually情况下,设置子视图间隔为10,子视图之间间隔都为10,且子视图依然等宽。


三、subView和arrangedSubView

对于Stack View的子控件添加和移除,我们是这样描述的。

添加 --> (Stack View管理的subview)
addArrangedSubview:
insertArrangedSubview:atIndex: arrangedSubviews
数组是subviews属性的子集。
移除 --> (Stack View管理的subview)
removeArrangedSubview:–>移除是指移除Stack View内部子控件的约束,并没有真正的把控件从父视图上移除。
removeFromSuperview–>从视图层次结构中删除,从父视图上删除

四、知识点小结

1、Axis表示Stack View的subview是水平排布还是垂直排布。
2、Alignment控制subview对齐方式。
3、Distribution定义subview的分布方式。
4、Spacing 为subview间的最小间距。

五、使用技巧

**可以hidden指定子view,根据动态拉伸规则,灵活使用组件。

例如:


原文链接:https://blog.csdn.net/songzhuo1991/article/details/115626992

收起阅读 »

iOS -SEL、Method 和 IMP区别及使用

Runtime中,SEL、Method 和 IMP有什么区别,使用场景?SEL:定义: typedef struct objc_selector *SEL,代表方法的名称。仅以名字来识别。翻译成中文叫做选择子或者选择器,选择子代表方法在 Runtime期间的标...
继续阅读 »

Runtime中,SEL、Method 和 IMP有什么区别,使用场景?

SEL:定义: typedef struct objc_selector *SEL,代表方法的名称。
仅以名字来识别。翻译成中文叫做选择子或者选择器,选择子代表方法在 Runtime期间的标识符。为 SEL类型,虽然 SEL是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。

      在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C的 Runtime 运行系统。不论两个类是否存在依存关系,只要他们拥有相同的方法名,那么他们的SEL都是相同的。
     比如,有n个viewcontroller页面,每个页面都有一个viewdidload,每个页面的载入,肯定都是不尽相同的。但是我们可以通过打印,观察发现,这些viewdidload的SEL都是同一个
SEL sel = @selector(methodName); // 方法名字 NSLog(@"address = %p",sel);// log输出为 address = 0x1df807e29因此类方法定义时,尽量不要用相同的名字,就算是变量类型不同也不行。否则会引起重复,例如:
-(void)setWidth:(int)width; -(void)setWidth:(double)width;
IMP:定义:typedef id (*IMP)(id, SEL, ...),代表函数指针,即函数执行的入口。该函数使用标准的 C调用。第一个参数指向 self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;第二个参数代表方法的选择子;... 代表可选参数,前面的 id 代表返回值。

Method:定义:typedef struct objc_method *Method,Method对开发者来说是一种不透明的类型,被隐藏在我们平时书写的类或对象的方法背后。它是一个objc_method结构体指针,我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。 objc_method的定义为:

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

方法名 method_name 类型为 SEL,相同名字的方法即使在不同类中定义,它们的方法选择器也相同。

方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。

method_imp 指向方法的实现,本质上是一个函数的指针

SEL selector的简写,俗称方法选择器,实质存储的是方法的名称
IMP implement的简写,俗称方法实现,看源码得知它就是一个函数指针
Method 对上述两者的一个包装结构.



转自:https://www.jianshu.com/p/3db9622209e2
收起阅读 »

iOS-通过Runtime防止重复点击-UIButton、UITableView

Gesture有系统处理单机双击,暂不去自定义时间间隔了。只处理UIButton、UITableView(UICollectionView)1、思路:UIButton hook sendActionUITableView hook setDelegate(sw...
继续阅读 »

Gesture有系统处理单机双击,暂不去自定义时间间隔了。只处理UIButton、UITableView(UICollectionView)

1、思路:

UIButton hook sendAction
UITableView hook setDelegate(swift中没有此方法,改用OC)
Gesture hook initWithTarget:action:

注意:
1、hook button的sendAction方法其实hook的是UIControl的的sendAction,点击UIControl子类(导航栏返回按钮)会崩溃。因此,把UIButton hook换为UIControl hook即可,同时为了避免对其他UIControl子类(UISlider/UISwitch)造成影响和最小影响原则,默认关闭防止重复点击功能

[_UIButtonBarButton xx_sendActionWithAction:to:forEvent:]: unrecognized selector sent to instance 0x7f8e2a41b380'

2、OC中的方法是NSDate.date.timeIntervalSince1970,不是[[NSDate date] timeIntervalSince1970]

3、由于setDelegate方法可能被多次调用,所以要判断是否已经swizzling了,防止重复执行。基类A中hook了tableview之后,子类B、C分别setDelegate的话会调用两次method_exchange...(didSelectRowAtIndexPath)。具体表现为:基类为UITableViewController时无影响,基类为自定义的有UITableView的VC时,子类A正常,子类B异常。解决办法1:didSelectRow基类里面不写,子类里面自己去实现,或基类里面写了但子类各自去重写。解决方法2:更安全的运行时方法交换库 Aspects。

2、代码

+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalAppearSelector = @selector(setDelegate:);
SEL swizzingAppearSelector = @selector(my_setDelegate:);
method_exchangeImplementations(class_getInstanceMethod([self class], originalAppearSelector), class_getInstanceMethod([self class], swizzingAppearSelector));
});
}
-(void)my_setDelegate:(id<UITableViewDelegate>)delegate{
[self my_setDelegate:delegate];

SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
SEL sel_t = @selector(my_tableView:didSelectRowAtIndexPath:);

//如果没实现tableView:didSelectRowAtIndexPath:就不需要hook
if (![delegate respondsToSelector:sel]){
return;
}
BOOL addsuccess = class_addMethod([delegate class],
sel_t,
method_getImplementation(class_getInstanceMethod([self class], sel_t)),
nil);

//如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了
if (addsuccess) {
Method selMethod = class_getInstanceMethod([delegate class], sel);
Method sel_Method = class_getInstanceMethod([delegate class], sel_t);
method_exchangeImplementations(selMethod, sel_Method);
}
}

// 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法:
-(void)my_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

if(NSDate.date.timeIntervalSince1970 - tableView.acceptEventTime < tableView.accpetEventInterval) {
NSLog(@"点击太快了");
return;
}
if (tableView.accpetEventInterval > 0) {
tableView.acceptEventTime = NSDate.date.timeIntervalSince1970;
}
[self my_tableView:tableView didSelectRowAtIndexPath:indexPath];
}

3、注意点、风险点

1、避免交换父类方法
如果当前类未实现被交换的方法而父类实现了的情况下,此时父类的实现会被交换,若此父类的多个继承者都在交换时会导致方法被交换多次而混乱,同时当调用父类的方法时会因为找不到而发生崩溃。所以在交换前都应该先尝试为当前类添加被交换的函数的新的实现IMP,如果添加成功则说明类没有实现被交换的方法,则只需要替代分类交换方法的实现为原方法的实现,如果添加失败,则原类中实现了被交换的方法,则可以直接进行交换。
2、load方法的加载顺序和相互影响
一个类B可能有继承来的super类A,还有可能有自己的分类C,如果分类中也实现了load方法,它们的调用顺序是怎么样的呢?系统首先会调用super的load方法,然后再调用类B自身的load方法,再次才会调用类B的分类C的load方法,也即是说真个继承链包括分类扩展中的load方法都会被执行到,只是执行顺序需要关注一下。load方法不同于其他覆盖方法在分类中的体现,如果类B本身中的其他方法在分类C中被重写,则会优先执行分类C中的。但是load不同,都会被执行到,因为这是类加载设置的方法。
3、出问题难排查
文本长按-编辑-复制或剪切的点击事件,需要过滤


摘自链接:https://www.jianshu.com/p/5499c7a4cba3
收起阅读 »

iOS-Crash文件的解析

开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退。脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断。联想起老罗在发布Smartisan OS的时候说了,他准备了10个手机,如果一台有问题,就换一台...
继续阅读 »

开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退。脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断。联想起老罗在发布Smartisan OS的时候说了,他准备了10个手机,如果一台有问题,就换一台,如果10台后挂了他就不做手机了。好了不闲扯了,今天就跟大家一起聊聊iOSCrash文件的组成以及常用的分析工具。

一、Crash文件结构

当程序运行Crash的时候,系统会把运行的最后时刻的运行信息记录下来,存储到一个文件中,也就是我们所说的Crash文件。iOS的Crash日志通常由以下6各部分组成。

1、Process Information(进程信息)


2、Basic Information


3、Exception(非常重要


4、Thread Backtrace


发生Crash的线程的Crash调用栈,从上到下分别代表调用顺序,最上面的一个表示抛出异常的位置,依次往下可以看到API的调用顺序。上图的信息表明本次Crash出现xxxViewController的323行,出错的函数调用为orderCountLoadFailed。

5、Thread State


Crash时发生时刻,线程的状态,通常我们根据Crash栈即可获取到相关信息,这部分一般不用关心。

6、Binary Images


Crash时刻App加载的所有的库,其中第一行是Crash发生时我们App可执行文件的信息,可以看出为armv7,可执行文件的包得uuid位c0f……cd65,解析Crash的时候dsym文件的uuid必须和这个一样才能完成Crash的符号化解析。

二、常见的Crash类型

1、Watchdog timeout

Exception Code:0x8badf00d, 不太直观,可以读成“eat bad food”,意思是don‘t block main thread

紧接着下面会有一段描述:

Application Specific Information:

com.xxx.yyy   failed to resume in time

对于此类Crash,我们应该去审视自己App初始化时做的事情是否正确,是否在主线程请求了网络,或者其他耗时的事情卡住了正常初始化流程。

通常系统允许一个App从启动到可以相应用户事件的时间最多为5S,如果超过了5S,App就会被系统终止掉。在Launch,resume,suspend,quit时都会有相应的时间要求。在Highlight Thread里面我们可以看到被终止时调用到的位置,xxxAppDelegate加上行号。 

PS. 在连接Xcode调试时为了便于调试,系统会暂时禁用掉Watchdog,所以此类问题的发现需要使用正常的启动模式。

2、User force-quit

Exception Codes: 0xdeadfa11, deadfall

这个强制退出跟我们平时所说的kill掉后台任务操作还不太一样,通常在程序bug造成系统无法响应时可以采用长按电源键,当屏幕出现关机确认画面时按下Home键即可关闭当前程序。

3、Low Memory termination

跟一般的Crash结构不太一样,通常有Free pages,Wired Pages,Purgeable pages,largest process 组成,同事会列出当前时刻系统运行所有进程的信息。

关于Memory warning可以参看我之前写的一篇文章IOS 内存警告 Memory warning level

App在运行过程中,系统内存紧张时通常会先发警告,同时把后台挂起的程序终止掉,最终如果还是内存不够的话就会终止掉当前前台的进程。

当接受到内存警告的事后,我们应该释放尽可能多的内存,Crash其实也可以看做是对App的一种保护。

4、Crash due to bugs

因为程序bug导致的Crash通常千奇百怪,很难一概而论。大部分情况通过Crash日志就可以定位出问题,当然也不排除部分疑难杂症看半天都不值问题出在哪儿。这个就只能看功底了,一点点找,总是能发现蛛丝马迹。是在看不出来时还可以求助于Google大神,总有人遇到和你一样的Bug 

三、常见的Exception Type & Exception Code

1、Exception Type

1)EXC_BAD_ACCESS

此类型的Excpetion是我们最长碰到的Crash,通常用于访问了不改访问的内存导致。一般EXC_BAD_ACCESS后面的"()"还会带有补充信息。

SIGSEGV: 通常由于重复释放对象导致,这种类型在切换了ARC以后应该已经很少见到了。

SIGABRT:  收到Abort信号退出,通常Foundation库中的容器为了保护状态正常会做一些检测,例如插入nil到数组中等会遇到此类错误。

SEGV:(Segmentation  Violation),代表无效内存地址,比如空指针,未初始化指针,栈溢出等;

SIGBUS:总线错误,与 SIGSEGV 不同的是,SIGSEGV 访问的是无效地址,而 SIGBUS 访问的是有效地址,但总线访问异常(如地址对齐问题)

SIGILL:尝试执行非法的指令,可能不被识别或者没有权限

2)EXC_BAD_INSTRUCTION

此类异常通常由于线程执行非法指令导致

3)EXC_ARITHMETIC

除零错误会抛出此类异常

2、Exception Code


三、获取Crash的途径

1、本机

通过xCode连接测试机器,直接在Device中即可读取到该机器上发生的所有Crash log。

2、itunes connect

通过itunes connect后台获取到用户上报的Crash日志。

3、第三方的Crash收集系统

有很多优秀的第三方Crash收集系统大大的方便了我们收集Crash,甚至还带了符号化Crash日志的功能。比较常用的有CrashlyticsFlurry等。


 Crash日志记录的时候是将Crash发生时刻,函数的调用栈,以及线程等信息写入文件。一般都是直接写的16进制地址,如果不经过符号化的话,基本上很难获取到有用信息,下一篇我们将聊一聊Crash日志的符号化,通俗点讲就是让Crash日志变成我们可读的格式。


转自:https://www.cnblogs.com/smileEvday/p/Crash1.html

收起阅读 »

iOS上架unity工程包含UIWebView问题

在经过一系列的开发之后,来到了游戏上架的步骤,但是在上架的过程中,收到了被拒邮件ITMS-90809: Deprecated API Usage - New apps that use UIWebView are no longer accepted. Ins...
继续阅读 »

在经过一系列的开发之后,来到了游戏上架的步骤,但是在上架的过程中,收到了被拒邮件

ITMS-90809: Deprecated API Usage - New apps that use UIWebView are no longer accepted. 
Instead, use WKWebView for improved security and reliability.
Learn more (https://developer.apple.com/documentation/uikit/uiwebview).

Though you are not required to fix the following issues,
we wanted to make you aware of them:

收到这个邮件之后,我首先全局搜索了我的工程中,是否包含UIWebView,但是并没有找到对应的文件。此时,就需要通过命令行来搜索自己的工程中,到底哪些地方包含UIWebView

1、首先确定包含UIWebView的地方,在命令行中cd到你的工程文件夹下,输入以下命令:

grep -r UIWebView .



2、确定包含的UIWebView之后,我们需要处理掉lib.a中的UIWebView

由于这个是包含在游戏的lib.a的静态库里边,所以,我们要对这个库进行处理,可以通过Git上的自动脚本来处理:

处理lib.a静态库中的UIWebView

3、或者也可以自己手动处理(建议使用脚本)

1.新建URLUtility.mm文件,放在桌面
复制以下代码

#include <iostream>
#import <UIKit/UIKit.h>

using namespace std;

namespace core {
template <class type>
class StringStorageDefault {};
template <class type,class type2>
class basic_string {
public:
char *c_str(void);
};
}

void OpenURLInGame(core::basic_string< char,core::StringStorageDefault<char> > const&arg){}

void OpenURL(core::basic_string<char,core::StringStorageDefault<char> >const&arg){
const void *arg2= &arg;
UIApplication *app = [UIApplication sharedApplication];
NSString *urlStr = [NSString stringWithUTF8String:(char *)arg2];
NSURL *url = [NSURL URLWithString:urlStr];
[app openURL:url];
}

void OpenURL(std::string const&arg){
UIApplication *app = [UIApplication sharedApplication];
NSString *urlStr = [NSString stringWithUTF8String:arg.c_str()];
NSURL *url = [NSURL URLWithString:urlStr];
[app openURL:url];

}

2.查看Unity项目下libiPhone-lib.a架构

lipo -info libiPhone-lib.a

结果如下:
Architectures in the fat file:
/Users/xxx/Desktop/libiPhone-lib.a are: arm64 armv7
则该静态库包含两种框架

生成URLUtility.mm armv7架构对应的URLUtility.o

clang -c URLUtility.mm -arch armv7 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk

新建armv7文件夹,将生成的URLUtility.o放入该文件夹, 后面会用到
生成URLUtility.mm arm64架构对应的URLUtility.o

clang -c URLUtility.mm -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk

新建arm64文件夹,将新生成的URLUtility.o放入该文件夹,后面会用到

如果有更多架构只需要把armv7替换成对应的架构分别生成

拆分libiPhone-lib.a
该命令是拆分libiPhone-lib.a中armv7架构部分
输出到/Users/xxx/Desktop/armv7/libiPhone-armv7.a
拆分成armv7架构

lipo libiPhone-lib.a -thin armv7 -output /Users/xxx/Desktop/armv7/libiPhone-armv7.a

拆分成arm64架构

lipo libiPhone-lib.a -thin arm64 -output /Users/xxx/Desktop/arm64/libiPhone-arm64.a

替换libiPhone-lib.a中的URLUtility.o
将各自架构libiPhone-lib.a里的URLUtility.o替换为我们生成的

ar -d 是移除
ar -q是添加
移除并且替换armv7中的URLUtility.o

ar -d /Users/xxx/Desktop/armv7/libiPhone-armv7.a URLUtility.o
ar -q /Users/xxx/Desktop/armv7/libiPhone-armv7.a /Users/xxx/Desktop/armv7/URLUtility.o

移除并且替换arm64

ar -d /Users/xxx/Desktop/arm64/libiPhone-arm64.a URLUtility.o
ar -q /Users/xxx/Desktop/arm64/libiPhone-arm64.a /Users/xxx/Desktop/arm64/URLUtility.o

合并libiPhone-lib.a

该命令的意思是将libiPhone-arm64.a libiPhone-armv7.a合并成桌面上的libiPhone-lib2.a

lipo -create /Users/xxx/Desktop/arm64/libiPhone-arm64.a /Users/xxx/Desktop/armv7/libiPhone-armv7.a -output /Users/xxx/Desktop/libiPhone-lib2.a

然后将libiPhone-lib2.a替换进Unity项目中即可使用

3、替换之后,重新运行,打包工程,提交上架即可

收起阅读 »

iOS运行unity导出工程权限问题

最近公司新开发了一款游戏,分别导出了安卓工程和xcode工程,在运行的过程中,出现了权限问题提示:在这边提示我,有两个.sh文件是被禁止的,没有运行的权限。其实这个问题很好解决,1、首先找到你xcode所在工程的文件夹,找到这两个.sh文件2、打开你的命令行控...
继续阅读 »

最近公司新开发了一款游戏,分别导出了安卓工程和xcode工程,在运行的过程中,出现了权限问题提示:


在这边提示我,有两个.sh文件是被禁止的,没有运行的权限。

其实这个问题很好解决,

1、首先找到你xcode所在工程的文件夹,找到这两个.sh文件


2、打开你的命令行控制器,输入chmod 777 MapFileParser.sh(其实这个·sh文件,直接拖入到命令行就行了)


3、运行完命令之后,再次编译工程即可

收起阅读 »

iOS内存管理

将计算机上有限的物理内存分配给多个程序使用地址空间不隔离内存使用率低程序运行的地址不确定虚拟内存虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,每个进程只能访问自己的地址空间,这样就能有效的做到了进程的隔离。注:...
继续阅读 »

将计算机上有限的物理内存分配给多个程序使用

  • 地址空间不隔离

  • 内存使用率低

  • 程序运行的地址不确定

虚拟内存
虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,每个进程只能访问自己的地址空间,这样就能有效的做到了进程的隔离。
注: 虚拟储存的实现需要依赖硬件的支持,对于不同的CPU来说不同,但是几乎所有的硬件都采用MMU(Memory Management Unit)的部件来进行页映射。


分段
把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。


  • 因为程序A和程序B被映射到了两块不同的物理空间区域,它们之间没有任何重叠,如果A程序访问虚拟空间的地址超出了0x00A00000这个范围,那么硬件就会判断这是一个非法访问,拒绝这个请求,所以做到了地址隔离。

  • 对于每个程序来说,无论它们被分配到物理地址的那一个区域,都是透明的,它们不关心物理地址的变化,只要按照从地址0x0000000到0x00A00000来编写程序,放置变量,所以程序不再需要重定位

分页
当一个程序运行时,在某个时间段内,它只是频繁的用到了一小部分数据,程序的很多数据其实在一个时间段内都不会被用到。人们很自然的想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。


  • 进程1(VP0,VP5)和进程2(VP0,VP3,VP4,VP5)的虚拟地址映射到物理地址(PP0,PP2,PP2,PP3,PP4),可以看到有些虚拟空间被映射到同一个物理页,这样就实现了内存共享。

  • 还有一部分位于磁盘中,如果进程需要用到这两个页时,操作系统就会接管进程,负责将虚拟地址从磁盘中读出来并装入内存,然后再与物理地址建立映射关系。

  • 保护也是页映射的目的之一,每个页可以设置权限属性,而只有操作系统有权限修改这些属性,那么操作系统就可以保护自己和保护进程了。

PAE
原先的32位地址只能访问最多4GB的物理内存,但是自从扩展至36位地址线以后,Intel修改了页映射方式,使得新的映射方式可以访问到更多的物理内存,Intel把这个地址扩展方式叫做PAE(Physical Address Extension)、

AWE
应用程序可以根据需求来选择申请和映射,比如一个应用程序0x10000000 ~0x20000000这一段256MB的虚拟地址空间用来做窗口,程序可以从高4GB的物理空间申请多个大小为256MB的物理空间,编号成A、B、C的等,然后根据需要将这个窗口映射到不同物理空间块,用到A时映射到A,用到B、C时再映射过去,叫做AWE(Address Windowing Extension),而Linux等UNIX类操作系统则采用mmap()系统调用来实现

  • 程序执行做需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将进程运行所需要的指令和数据全部装入内存中,这就是简单的静态装入。

  • 由于内存的昂贵稀有,所需的内存大余物理内存,所以我们将程序最常用的部分驻留在内存中,不太常用的数据存放在磁盘里,这就是动态装入。覆盖装入和页映射就是两种很典型的动态装载方法。

覆盖装入(Overlay)


  • 这个树状结构从任何一个模块到树的根(main)模块都叫调用路径,当该模块被调用时,整个调用路径上的模块都必须在内存中,已确保执行完毕后能正确返回至模块。

  • 禁止跨树间调用。任意一个模块不允许跨过树状结构进行调用。

  • 由于跨模块间的调用需要经过覆盖管理器,已确保所有被调用到的模块都能够正确的驻留在内存,而且一旦模块没有在内存中,还需要从磁盘或者其他存储器读取响应的模块,所以覆盖装入的速度肯定比较慢,是典型的利用时间换取空间的方法

页映射(Paging)


  • 将内存和所有磁盘中的数据和指令按照page为单位划分为若干个页,以后所有的装载和操作的单位都是页,硬件规定页的大小为4096字节,8192KB等,那么512MB的物理内存就拥有512 * 1024 * 1024 /4096 = 131072 页。

  • 假设32的机器有16KB的内存,每个页4096字节,则共有4个页(F0、F1、F2、F3),假设程序需有的指令和数据总和为32KB,那么程序被分为8个页(P0~P7)。很明显16KB的内存无法同时将32KB的程序装入,那么我们将按照动态装入的原理来进行整个装入过程。

  • 如果只有4个页,那么程序能一直执行下去,但问题很明显不是,如果超过4个页,装载管理器必须作出抉择,放弃目前正在使用的4个内存中的其中一个。至于放弃那个页有多种算法:比如F0,先进先出;比如很少访问的F2,最少使用法

页错误(Page Fault)
一些存储在磁盘中的数据,在CPU执行这个地址指令时,发现页面是一个空的页面,于是他就认为这是一个页错误,CPU将控制权交给操作系统,操作系统有专门处理例程来处理,操作系统将查询这个数据结构,然后找到空页面所在的VMA,计算相应的页面在可执行文件中的偏移,然后再物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射,然后再交给进程去执行。

进程虚拟空间分布
ELF文件被映射时,是以页长度为单位的,每个段在映射时的长度应该是系统页长度的整倍数,如果不是,多余的部分页将占领一个页,造成了内存空间的大量浪费。而在ELF文件中,段的权限直邮为数不多的几种组合:

  • 以代码段为代表的权限为可读可执行的段

  • 以数据段和BSS段为代表的权限为可读可写的段

  • 以只读数据段为代表的权限为只读的段

那么对于相同的段,我们把他们合并在一起当成一个段来映射,ELF可执行文件引入一个概念叫做Segment,一个segment包含一个或多个section,这样很明显的减少了页面内部的碎片,节省了空间

段地址对齐
假设一个ELF执行文件,它有三个段需要装载,SEG0 、SEG1、SEG2,如图:


可以看到这种对齐方式在文件段的内部会有很多内部碎片,浪费磁盘空间,可执行文件总长度只有12014字节,却占了5个页。为了解决这个问题,UNIX系统采用了让那些个个段接壤的部分共用一个物理页面,将该物理页面映射两次,系统将它们映射两份到虚拟地址空间,其他的都按照正常的页粒度进行映射。

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


收起阅读 »

iOS 中事件的响应链和传递链

iOS事件链有两条:事件的响应链;Hit-Testing事件的传递链响应链:由离用户最近的view向系统传递。initial view –> super view –> ….. –> view controller –> window ...
继续阅读 »

iOS事件链有两条:事件的响应链;Hit-Testing事件的传递链

  • 响应链:由离用户最近的view向系统传递。initial view –> super view –> ….. –> view controller –> window –> Application –> AppDelegate

  • 传递链:由系统向离用户最近的view传递。UIKit –> active app's event queue –> window –> root view –> …… –> lowest view

在iOS中只有继承UIResponder的对象才能够接收并处理事件,UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象,首先我们通过一张图来简单了解一下事件的传递以及响应


1、传递链

  • 事件传递的两个核心方法

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
  • 第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件

  • 第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES

  • 其中UIView不接受事件处理的情况有

1. alpha <0.01
2. userInteractionEnabled = NO
3. hidden = YES
  • 事件传递的流程图


  • 流程描述

     1、我们点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow

     2、在UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图

     3、在hitTest:withEvent:方法中就会去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图

     4、遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用

     5、最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者

2、相应链

  • 响应者链流程图


  • 响应者链的事件传递过程总结如下

     1、如果view的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图

     2、在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给UIWindow对象进行处理

     3、如果UIWindow对象也不处理,则将事件传递给UIApplication对象

     4、如果UIApplication也不能处理该事件,则将该事件丢弃

3、实例场景

  • 在一个方形按钮中点击中间的圆形区域有效,而点击四角无效

  • 核心思想是在pointInside: withEvent:方法中修改对应的区域

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 如果控件不允许与用用户交互,那么返回nil
if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
return nil;
}

//判断当前视图是否在点击范围内
if ([self pointInside:point withEvent:event]) {
//遍历当前对象的子视图(倒序)
__block UIView *hit = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
CGPoint convertPoint = [self convertPoint:point toView:obj];
//调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
hit = [obj hitTest:convertPoint withEvent:event];
//如果找到了就停止遍历
if (hit) *stop = YES;
}];

//返回当前的视图对象
return hit?hit:self;
}else {
return nil;
}
}

// 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat x1 = point.x;
CGFloat y1 = point.y;

CGFloat x2 = self.frame.size.width / 2;
CGFloat y2 = self.frame.size.height / 2;

//判断是否在圆形区域内
double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
if (dis <= self.frame.size.width / 2) {
return YES;
}
else{
return NO;
}
}

转自:jianshu.com/p/cfcde82c67f6


收起阅读 »

iOS 中 如何从视频中提取音频

.h文件/**提取视频中的音频@param videoPath 视频路径@param completionHandle 完成回调*/+(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void ...
继续阅读 »

.h文件
/**

提取视频中的音频

@param videoPath 视频路径
@param completionHandle 完成回调
*/

+(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void (^)(NSURL *outputPath,BOOL isSucceed)) completionHandle;

.m文件
需要导入系统的#import <Photos/Photos.h>
/**

提取视频中的音频

@param videoPath 视频路径
@param completionHandle 完成回调
*/

+(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void (^)(NSURL *outputPath,BOOL isSucceed)) completionHandle{

AVAsset *videoAsset = [AVAsset assetWithURL:videoPath];
//1创建一个AVMutableComposition
AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];
//2 创建一个轨道,类型为AVMediaTypeAudio
AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

//获取videoPath的音频插入轨道
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];

//4创建输出路径
NSURL *outputURL = [self exporterPath:@"mp4"];

//5创建输出对象
AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetAppleM4A];
exporter.outputURL = outputURL ;
exporter.outputFileType = AVFileTypeAppleM4A;
exporter.shouldOptimizeForNetworkUse = YES;
[exporter exportAsynchronouslyWithCompletionHandler:^{
if (exporter.status == AVAssetExportSessionStatusCompleted) {
NSURL *outputURL = exporter.outputURL;
completionHandle(outputURL,YES);
}else {
NSLog(@"失败%@",exporter.error.description);
completionHandle(outputURL,NO);
}
}];

}
// 输出路径
+ (NSURL *)exporterPath:(NSString *)filename{
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
formatter.dateFormat = @"yyyyMMddHHmmss";
NSString *str = [formatter stringFromDate:[NSDate date]];
NSString *fileName = [NSString stringWithFormat:@"selfMusic%@.%@",str,filename];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = [paths firstObject];
//这个是录制视频时存储到本地的video
NSString *path = [NSString stringWithFormat:@"%@/KSYShortVideoCache",docPath];
//判断文件夹是否存在,不存在就创建
//创建附件存储目录
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString *outputFilePath = [path stringByAppendingPathComponent:fileName];
return [NSURL fileURLWithPath:outputFilePath];
}

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

收起阅读 »

静态拦截iOS对象方法调用的简易实现

最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用可以在运行时通过第三方库比如fishhook来Hook所有objc_ms...
继续阅读 »

最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。
这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用可以在运行时通过第三方库比如fishhook来Hook所有objc_msgSend调用来实现,也可以在编译后链接前通过静态插桩的方式来实现Hook拦截。
对于静态插桩的实现一般有如下两个方案:
1、助于LLVM语法树分析来实现代码插桩。
2、将源码编译为静态库,并通过修改静态库中.o目标文件的代码段来实现代码插桩。
上述的两个方法实现起来比较复杂,要么就要了解LLVM,要么就要熟悉目标文件中间字节码以及符号表相关的底层知识。
本文所介绍的是第三种静态Hook方案,也是依赖于静态库这个前提来实现对objc_msgSend函数进行Hook,从而实现在编译前链接后的OC对象方法调用插桩。
这个方案实现的原理很简单。因为静态库其实只是一个编译阶段的中间产物,静态库目标文件中的所有引用的外部符号会保存到一张字符串表中,所有函数调用都只是记录了函数名称在字符串表的索引位置,在链接时会才会根据符号名称来替换为真实的函数调用指令。因此我们可以将所有静态库字符串表中的objc_msgSend统一替换为另外一个长度相同的字符串:hook_msgSend(名字任意只要长度一致并唯一)即可。然后在主工程源代码中实现一个名字为hook_msgSend的函数即可。这个函数必须要和objc_msgSend的函数签名保持一致,这样在链接时所有静态库中的objc_msgSend调用都会统一转化为hook_msgSend调用。

下面的是具体的实现步骤:

1.在主工程中编写hook_msgSend的实现。
hook_msgSend的函数签名要和objc_msgSend保持一致,并且要在主工程代码中实现,而且必须要用汇编代码实现。具体实现的逻辑和目前很多文章中介绍的对objc_msgSend函数的Hook实现保持一致即可。
很多对objc_msgSend进行Hook的实现其实是不完整的,因此如果想完全掌握函数调用ABI规则的话请参考:《深入iOS系统底层之函数调用》

2. 将所有其他代码都统一编译为一个或多个静态库。
将源代码按功能编译为一个或多个静态库,并且主工程链接这些静态库。这种程序代码的组织方式已经很成熟了,最常用的方法是我们可以借助代码依赖集成工具cocoapods来实现,这里就不再赘述了。

3.在主工程的Build Phases 中添加Run Script脚本。
我们需要保证这个脚本一定要运行在链接所有静态库之前执行。因此可以放到Compile Sources 下面。

4.实现静态库符号替换的Run Script脚本。
这是最为关键的一步,我们可以实现一个符号替换的程序,然后在Run Script脚本中 执行这个符号替换程序。符号替换程序的输入参数就是主工程中所链接的所有静态库的路径。至于这个符号替换程序如何编写则没有限制,你可以用ruby编写也可以用python也可以用C语言编写。 无论用何种方法实现,你都需要首先了解一下静态库.a的文件结构。你可以从:《深入iOS系统底层之静态库》一文中掌握到一个静态库文件的组成结构。了解了静态库文件的组成结构后,你的符号替换程序要做的事情就可以按如下步骤实现:

一)、 打开静态库.a文件。
二)、找.a文件中定义的字符串表部分。字符串表的描述如下:

struct stringtab
{
int size; //字符串表的尺寸
char strings[0]; //字符串表的内容,每个字符串以\0分隔。
};

字符串表中的strings的内容就是一个个以\0分隔的字符串,这些字符串的内容其实就是这个目标文件所引用的所有外部和内部的符号名称。
三)、将字符串表中的objc_msgSend字符串替换为hook_msgSend字符串。
四)、保存并关闭静态库.a文件。

5.编译、链接并运行你的主工程程序。
采用本文中所介绍的静态Hook方法的好处是我们不必Hook所有的OC方法调用,而是可以有选择的进行特定对象和类的方法调用拦截。因此这种技术不仅可以应用代码重排统计上,还可以应用在其他的监控和统计应用中。因为这种机制可以避免程序在运行时进行objc_msgSend替换而产生的函数调用风暴问题。另外的一个点就是这个方法不局限于对objc_msgSend进行Hook,还可以对任意的其他函数进行Hook处理。因此这种技术也可以应用在其他方面。

转自:https://www.jianshu.com/p/843642c9df32

收起阅读 »