wxiubin / field Goto Github PK
View Code? Open in Web Editor NEWAbin' blog
Home Page: http://iosgg.cn
Abin' blog
Home Page: http://iosgg.cn
事情的起因是这样的,每周都要发送工作周报,标题是需要的固定的格式(方便规则收信),比如 【工作周报】 xxx 11.20-11.24,发送和抄送也是给固定的人,企业邮箱还没有模板,所以每次发邮件都要复制粘贴标题、发送人和抄送人,只有内容是手写,便萌发了用脚本发送邮件的想法。
用脚本只需要第一次使用的时候配置收件人、抄送人和标题的格式,之后就只需要:
# 配置项
from_addr = '[email protected]' # 发送方邮箱
password = 'passMhr22i6Uword' # 发送方邮箱的密码(企业邮箱-设置-客户端专用密码)
to_addr = '[email protected]' # 收件人邮箱
cc_addr = '[email protected]' # 抄送人邮箱
from_name = '王修斌' # 发件人姓名
# QQ 企业邮箱不用修改
smtp_addr = 'smtp.exmail.qq.com'
smtp_port = 465
def _build_msg(content,date):
msg = MIMEText(content, 'plain', 'utf-8')
msg['From'] = _format_addr('%s <%s>' % (from_name, from_addr) )
msg['To'] = _format_addr( 'zhuyue <%s>' % to_addr )
msg['CC'] = _format_addr( 'bplus-ios <%s>' % cc_addr )
msg['Subject'] = Header('【B+iOS周报】 %s %s' % (from_name, date), 'utf-8').encode()
return msg
def send_email(msg):
print
try:
print '初始化 SMTP...'
server = smtplib.SMTP_SSL(smtp_addr, smtp_port)
print '初始化 SMTP 成功'
# server.set_debuglevel(1)
print '开始登陆邮箱服务...'
server.login(from_addr, password)
print '开始发送邮件...'
server.sendmail(from_addr, [to_addr], msg.as_string())
print '邮件发送成功!'
except server.SMTPException,e:
print '邮件发送失败 %s' % e
finally:
server.quit()
def sendEmailCommand():
input_date = app.dateInput.get()
msg_content = app.content.get("0.0", "end")
send_email(_build_msg(msg_content, input_date))
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.createWidgets()
def createWidgets(self):
self.header = Frame()
self.dateLabel = Label(self.header, text='周报标题日期(如 11.20-11.24):')
self.dateLabel.grid(row=0,column=0)
self.dateInput = Entry(self.header)
self.dateInput.grid(row=0,column=1)
self.sendButton = Button(self.header, text='发送', command=sendEmailCommand)
self.sendButton.grid(row=0,column=2)
self.content = Text(self,padx=10)
self.content.insert(END, '本周任务:\n\n下周任务:\n\n风险评估:\n')
self.content.pack()
self.header.pack()
app = Application()
# 设置窗口标题:
app.master.title('发送周报邮件')
# 主消息循环:
app.mainloop()
cd 脚本目录
python GUI.py
或者修改名称 GUI.command
方便双击打开。
执行时提示权限不足可在终端执行 chmod +x GUI.command
终端输出:
初始化 SMTP...
初始化 SMTP 成功
开始登陆邮箱服务...
开始发送邮件...
邮件发送成功!
最终只需要在脚本的图形界面中输入时间段和内容,然后点击发送,美滋滋~ 以后再也不用 command+C
、command+V
了
我的博客:https://iosgg.cn/
https://iosgg.cn/2016/04/17/runtime4_method_swizzling/
什么是 AOP : (site: baike.baidu.com),引用百度百科中的解释就是:
https://iosgg.cn/2016/03/10/mac_apps/
这么长时间以来都是用Mac做开发主力机器,有一些常用的软件和小的技巧记录分享一下。
https://iosgg.cn/2016/10/11/swift_class_struct/
Swift 中的结构体的能力被大大加强,不仅可以拥有属性,还以有方法、构造函数、甚至是扩展和遵守协议。这样的结构体和类有很多相同点:
CSS 全称是Cascading Style Sheets,层叠样式表,被用来控制HTML标签的样式,美化网页
。CSS有两个重点: 属性 和 选择器 。
CCS的编写格式是以键值对的形式,如:
color : red;
background-color : blue;
font-size : 20px;
<body style="color : red;">
<style>
<body {
color : red;
}
</style>
<link rel="stylesheet" href="index.css">
根据标签名查找标签:
<div>div1<div>
找到标签,设置样式:
div {
color : red;
}
<p class="high">hello world</>
.high{
color : red;
}
<p id="first">Hello world</p>
#first{
color : red;
}
<div class="high">div high</div>
<div class="low">div low</div>
<p class="high">p high</p>
<p class="low">p low</p>
div, .high{
color : red;
}
并列选择,只要一个符合条件就可以
结果:
<div class="high">div high</div>
<div class="low">div low</div>
<p class="high">p high</p>
<p class="low">p low</p>
div .high{
color : red;
}
复合选择,必须全部符合条件就可以
结果:
<div>
<p>div -> p</p>
<span>
<p>div -> span -> p</p>
</span>
</div>
<p>div p</p>
div p {
color : red;
}
只要是在div 里面的 p 标签,都符合要求,结果:
<div>
<p>div -> p</p>
<span>
<p>div -> span -> p</p>
</span>
</div>
div p {
color : red;
}
必须是直接在div 里面的 p 标签,才符合要求,即儿子可以,孙子就不行
结果:
<div>
<p>div -> p</p>
<span>
<p>div -> span -> p</p>
</span>
</div>
<p>div p</p>
<p>p p</p>
div + p {
color : red;
}
只要是在div 里面的 p 标签,都符合要求,结果:
<div name="Jack">name = Jack</div>
<div name="Tom">name = Tom</div>
<div name="Bob" age="20">name = Bob, age = 20</div>
<div>no name</div>
div[name] {
color : red;
}
div[name][age] {
color : green;
}
div[name="Tom"] {
color : yellow;
}
属性 | 描述 |
---|---|
:active | 向被激活的的元素添加样式 |
... | ... |
...
选择器针对性越强、范围越小,优先级就越高
选择器的权值:
选择器 | 权值 |
---|---|
通配符 | 0 |
标签 | 1 |
类 | 10 |
属性 | 10 |
伪类 | 10 |
id | 100 |
important | 1000 |
Swift
中的结构体的能力被大大加强,不仅可以拥有属性,还以有方法、构造函数、甚至是扩展和遵守协议。这样的结构体和类有很多相同点:
当然,类和结构体也有很多不同的地方,类还有许多独有的附加功能:
结构体会提供一个默认的构造函数,这个构造函数是结构体所有的属性分别作为参数来构建:
struct MyPoint {
var x = 0
var y = 0
}
let point:MyPoint = MyPoint(x: 1, y: 2)
结构体和枚举都是值类型,值类型在赋值(给变量或者常量)和传递(作为参数给一个函数)的时候都会被拷贝,值类型实例的值属性也会被拷贝。
Swift
中的整型、浮点型、布尔型、字符串、字典、数组都是值类型,底层都是由结构体来实现的。
类是引用类型,引用类型在赋值和传递的时候,内容并不会被拷贝。因此赋值的实例和被赋值的实例其实是一份内容,内容在内存中也是一份。
值类型和引用类型的区别在于,值类型在赋值和传递的时候是深拷贝,而引用类型是浅拷贝。
深拷贝就是把内存地址中存放的内容也拷贝一份内存中的内容就会有两份;而浅拷贝只是拷贝了内存的地址,内存中的内容还是只有一份。
但需要注意的是,在 Swift
中,并不是值类型一旦被赋值和传递的时候就会被拷贝一份,只有当需要的时候,比如被赋值的实例去改变内容的时候才会真正的去拷贝。
那么,我们到底如何选择结构体或者类呢?如果你只是用来做以下功能是可以选择结构体:
比如 CGPoint
、CGRect
、CGSize
等都是结构体。
https://iosgg.cn/2016/02/22/CSS-with-H5/
CSS 全称是Cascading Style Sheets,层叠样式表,被用来控制HTML标签的样式,美化网页。CSS有两个重点: 属性 和 选择器 。
https://iosgg.cn/2017/07/12/objc-block/
Objc 中的 Block 其实是一个对象,之前也说过 Objc 中对象的结构
https://iosgg.cn/2016/08/08/shadowsocks_vps_gfw/
假设我们已经有一个 VPS,并且可以 ssh 登陆上去。
在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)
关联对象 可以给某个对象关联其他对象并用key来区分其他对象。需要注意的是,存储对象的时候要指明 存储策略,用来维护对象的内存管理语义。存储策略是 objc_AssociationPolicy 枚举定义,以下是存储策略对应的 @Property属性:
存储策略类型 | 对应的@Property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | weak |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
OBJC_ASSOCIATION_RETAIN | strong |
OBJC_ASSOCIATION_COPY | copy |
用下面的方法可以管理关联对象:
// 这个方法可以根据指定策略给对象关联对象值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 这个方法可以获取对象关联对象值
id objc_getAssociatedObject(id object, const void *key)
// 这个方法可以删除指定对象的全部关联对象值
void objc_removeAssociatedObjects(id object)
对于关联对象这个OC特性,我们可以把对象想象成一个 NSDictionary,关联对象需要一个 key( 类型是 opaque pointer,无类型的指针 ) 来区分,我们可以把要添加的变量名作为 key ,把变量的值作为关联的对象来存储到 ”对象“ 这个 NSDictionary 中。
所以,关联对象的
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
方法类似于字典的 [dict setObject: forKey:]
方法。
在存储和获取关联对象时需要用一个相等的 key ,因为是给 Class 的实例对象关联对象,所以一般用静态变量来做 key 。
说的再多,不如上段代码!
比如说,我们给 NSString 实例加上个 NSDate 类型的 date 变量。什么?给字符串加个日期变量是要干袅?我要给字符串过个生日不行吗! 别闹,举个栗子嘛!(捂脸逃跑~~~)
首先,我们先给 NSString 新建个名为 RT 的 category。
在头文件中有个 NSDate 类型的 date 属性:
// NSString+RT.h
// runtime
#import <Foundation/Foundation.h>
//
@interface NSString (RT)
//
@property (nonatomic, strong) NSDate *date;
//
@end
在分类中的属性只会生成 get 和 set 方法,并不会生成变量。
所以我们需要重写 get 和 set 方法,关联对象以变相实现添加变量,在现实文件中:
// NSString+RT.m
// runtime
#import <objc/runtime.h>
#import "NSString+RT.h"
//
@implementation NSString (RT)
//
static void *runtime_date_key = "date";
- (NSDate *)date{
return objc_getAssociatedObject(self, runtime_date_key);
}
//
- (void)setDate:(NSDate *)date{
objc_setAssociatedObject(self, runtime_date_key, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
需要注意的是,关联对象用到的 key 是个无类型的指针,一般来说是静态来修饰。
另外,给对象关联的只能是对象,如果是 int、 float 等类型需要 NSNumber 进行包装。
因为 date 是强引用和非原子属性,所以关联策略用 OBJC_ASSOCIATION_RETAIN_NONATOMIC
然后执行代码:
NSString *string = @"runtimeTestString";
string.date = [NSDate date];
NSLog(@"string.date = %@",string.date);
输出结果:
2016-04-12 21:27:31.099 runtime[2837:103727] string.date = 2016-04-12 13:27:31 +0000
注意:
https://iosgg.cn/2016/10/11/swift_class_struct/
Swift 中的结构体的能力被大大加强,不仅可以拥有属性,还以有方法、构造函数、甚至是扩展和遵守协议。这样的结构体和类有很多相同点:
这么长时间以来都是用Mac做开发主力机器,有一些常用的软件和小的技巧记录分享一下。
.xip
文件,双击检验后就会丢出来个Xcode,如果失败最大的可能就是磁盘空间不足(一般需要近20G剩余空间,具体多少没测试)。 腾讯Bugly团队镜像python -m SimpleHTTPServer
// http://0.0.0.0:8000
python -m SimpleHTTPServer 8080
// http://0.0.0.0:8080
当出现**"xx.app"已损坏,打不开。您应该将它移到废纸篓**提示的时候,多数是需要去选择 系统偏好设置
- 安全性与隐私
- 任何来源
。
macOs sierra版本系统可能没有该选项,需要在终端中输入 sudo spctl --master-disable
命令来打开选项。如果想关闭则再次输入命令就可以了。
用终端输入命令:
显示隐藏文件:defaults write com.apple.finder AppleShowAllFiles -bool true
隐藏隐藏文件:defaults write com.apple.finder AppleShowAllFiles -bool false
有的人说进程就像是人的脑袋,线程就是脑袋上的头发~~。其实这么比方不算错,但是更简单的来说,用迅雷下载文件,迅雷这个程序就是一个进程,下载的文件就是一个线程,同时下载三个文件就是多线程。一个进程可以只包含一个线程去处理事务,也可以有多个线程。
多线程可以大大提高软件的执行效率和资源(CPU、内存)利用率,因为CPU只可以处理一个线程(多核CPU另说),而多线程可以让CPU同时处理多个任务(其实CPU同一时间还是只处理一个线程,但是如果切换的够快,就可以了认为同时处理多个任务)。但是多线程也有缺点:当线程过多,会消耗大量的CPU资源,而且,每开一条线程也是需要耗费资源的(iOS主线程占用1M内存空间,子线程占用512KB)。
iOS程序在启动后会自动开启一个线程,称为 主线程 或者 UI线程 ,用来显示、刷新UI界面,处理点击、滚动等事件,所以耗费时间的事件(比如网络、磁盘操作)尽量不要放在主线程,否则会阻塞主线程造成界面卡顿。
iOS开发中的多线程实现方案有四种:
技术方案 | 简介 | 语言 | 生命周期管理 |
---|---|---|---|
pthread | 一套通用的多线程API,适用于Unix\Linux\Windows等系统,跨平台\可移植,使用难度大 | C | 程序员管理 |
NSThread | 使用更加面向对象,简单易用,可直接操作线程对象 | Objective-C | 程序员手动实例化 |
GCD | 旨在替代NSThread等线程技术,充分利用设备的多核 | C | 自动管理 |
NSOperation | 基于GCD(底层是GCD),比GCD多了一些更简单实用的功能,使用更加面向对象 | Objective-C | 自动管理 |
多线程中GCD我使用比较多,以GCD为例,多线程有两个核心概念:
任务就是你开辟多线程要来做什么?而每个线程都是要加到一个队列中去的,队列决定任务用什么方式来执行。
线程执行任务方式分为:
同步执行只能在当前线程执行,不能开辟新的线程。而且是必须、立即执行。而异步执行可以开辟新的线程。
队列分为:
并发队列可以让多个线程同时执行(必须是异步),串行队列则是让任务一个接一个的执行。打个比方说,串行队列就是单车道,再多的车也得一个一个的跑(--:我俩车强行并着跑不行? --:来人,拖出去砍了!),而串行是多车道,可以几辆车同时并着跑。那么到底是几车道?并发队列有个最大并发数,一般可以手动设置。
那么,线程加入到队列中,到底会怎么执行?
并发队列 | 串行队列(非主队列) | 主队列(只有主线程,串行队列) | |
---|---|---|---|
同步 | 不开启新的线程,串行 | 不开启新的线程,串行 | 不开启新的线程,串行 |
异步 | 开启新的线程,并发 | 开启新的线程,串行 | 不开启新的线程,串行 |
注意:
上面提到线程会阻塞,那么什么是阻塞?除了阻塞之外线程还有其他什么状态?
一般来说,线程有五个状态:
使用下面代码可以创建一个线程:
int pthread_create(pthread_t * __restrict, const pthread_attr_t * __restrict,void *(*)(void *), void * __restrict)
可以看到这个方法有四个参数,主要参数有 pthread_t * __restrict ,因为该方法是C语言,所以这个参数不是一个对象,而是一个 pthread_t 的地址,还有 *void ()(void ) 是一个无返回值的函数指针。
使用代码:
void * run(void *param)
{
NSLog(@"currentThread--%@", [NSThread currentThread]);
return NULL;
}
- (void)createThread{
pthread_t thread;
pthread_create(&thread, NULL, run, NULL);
}
控制台输出:
currentThread--<NSThread: 0x7fff38602fb0>{number = 2, name = (null)}
number = 1 的线程是主线程,不为一的时候都是子线程。
NSThread创建线程一般有三种方式:
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
//
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
//
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
示例代码:
- (void)createThread{
// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"我是参数"];
thread.name = @"我是线程名字啊";
// 启动线程
[thread start];
// 或者 [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"我是参数"];
// 或者 [self performSelectorInBackground:@selector(run:) withObject:@"我是参数"];
}
- (void)run:(NSString *)param{
NSLog(@"-----run-----%@--%@", param, [NSThread currentThread]);
}
控制台输出:
-----run-----我是参数--<NSThread: 0x7ff8a2f0c940>{number = 2, name = 我是线程名字啊}
苹果官方对GCD说:
开发者要做的只是定义执行的任务并追加到适当的 Dispatch Queue 中。
在GCD中我们要做的只是两件事:定义任务;把任务加到队列中。
GCD 的队列有两种:
Dispatch Queue 种类 | 说明 |
---|---|
Serial Dispatch Queue | 等待现在执行中处理结束(串行队列) |
Concurrent Dispatch Queue | 不等待现在执行中处理结束(并行队列) |
GCD中的队列都是 dispatch_queue_t 类型,获取/创建方法:
// 1. 手动创建队列
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 1.1 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_SERIAL);
// 1.2 创建并行队列
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_CONCURRENT);
// 2. 获取系统标准提供的 Dispatch Queue
// 2.1 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 2.2 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
需要说明的是,手动创建队列时候的两个关键参数,*const char label 指定队列名称,最好起一个有意义的名字,当然如果你想调试的时候刺激一下,也可以设置为 NULL,而 dispatch_queue_attr_t attr 参数文档有说明:
/*!
* @const DISPATCH_QUEUE_SERIAL
* @discussion A dispatch queue that invokes blocks serially in FIFO order.
*/
#define DISPATCH_QUEUE_SERIAL NULL
/*!
* @const DISPATCH_QUEUE_CONCURRENT
* @discussion A dispatch queue that may invoke blocks concurrently and supports
* barrier blocks submitted with the dispatch barrier API.
*/
#define DISPATCH_QUEUE_CONCURRENT \
DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
_dispatch_queue_attr_concurrent)
创建完队列之后就是定义任务了,有两种方式:
// 创建一个同步执行任务
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 创建一个异步执行任务
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
完整的示例代码:
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue.asyncSerial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"异步 + 串行 - %@",[NSThread currentThread]);
});
我们可能在实际开发中会遇到这样的需求:在两个任务完成后再执行某一任务。虽然这种情况可以用串行队列来解决,但是我们有更加高效的方法。
直接上代码,在代码的注释中讲解:
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建任务组
// dispatch_group_t :A group of blocks submitted to queues for asynchronous invocation
dispatch_group_t group = dispatch_group_create();
// 在任务组中添加一个任务
dispatch_group_async(group, queue, ^{
//
});
// 在任务组中添加另一个任务
dispatch_group_async(group, queue, ^{
//
});
// 当任务组中的任务执行完毕之后再执行一下任务
dispatch_group_notify(group, queue, ^{
//
});
从字面意思就可以看出来这个变量的用处,即阻碍任务执行,它并不是阻碍某一个任务的执行,而是在代码中,在它之前定义的任务会比它先执行,在它之后定义的任务则会在它执行完之后在开始执行。就像一个栏栅。
使用代码:
dispatch_queue_t queue = dispatch_queue_create("com.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"----1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----2-----%@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"----barrier-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----3-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----4-----%@", [NSThread currentThread]);
});
控制台输出:
----1-----<NSThread: 0x7fdc60c0fd90>{number = 2, name = (null)}
----2-----<NSThread: 0x7fdc60c11500>{number = 3, name = (null)}
----barrier-----<NSThread: 0x7fdc60c11500>{number = 3, name = (null)}
----3-----<NSThread: 0x7fdc60c11500>{number = 3, name = (null)}
----4-----<NSThread: 0x7fdc60c0fd90>{number = 2, name = (null)}
dispatch_apply 的用法类似于对数组元素进行 for循环 遍历,但是 dispatch_apply 的遍历是无序的。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
[array addObject:@(i)];
}
// array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
NSLog(@"--------apply begin--------");
dispatch_apply(array.count, queue, ^(size_t index) {
NSLog(@"%@---%zu", [NSThread currentThread], index);
});
NSLog(@"--------apply done --------");
控制台输出:
--------apply begin--------
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---0
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---4
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---5
<NSThread: 0x7ffa7bda77c0>{number = 2, name = (null)}---1
<NSThread: 0x7ffa7be1fd00>{number = 4, name = (null)}---3
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---6
<NSThread: 0x7ffa7be1a920>{number = 3, name = (null)}---2
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---8
<NSThread: 0x7ffa7be1fd00>{number = 4, name = (null)}---9
<NSThread: 0x7ffa7bda77c0>{number = 2, name = (null)}---7
--------apply done --------
可以看到,遍历的时候自动开启多线程,可以无序并发执行多个任务,但是有一点可以确定,就是 NSLog(@"--------apply done --------"); 这段代码一定是在所有任务执行完之后才会去执行。
除了上面的那些,GCD还有其他的用法
NSOperation 和 NSOperationQueue 配合使用也能实现并发多线程,但是需要注意的是 NSOperation 是个抽象类,想要封装操作需要使用其子类。
系统为我们提供了两个子类:
当然,我们也可以自定义其子类,只是需要重写 main() 方法。
先看下系统提供两个子类的初始化方法:
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
两个子类初始化方法不一样的地方就是一个用 实例对象 和 方法选择器 来确定执行一个方法,另外一个是用block闭包保存执行一段代码块。
另外 NSBlockOperation 还有一个实例方法 - (void)addExecutionBlock:(void (^)(void))block; ,只要调用这个方法以至于封装的操作数大于一个就会开启新的线程异步操作。
最后调用NSOperation的start方法启动任务。
NSOperation 默认是执行同步任务,但是我们可以把它加入到 NSOperationQueue 中编程异步操作。
- (void)addOperation:(NSOperation *)op;
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
- (void)addOperationWithBlock:(void (^)(void))block;
之前提到过多线程并发队列可以设置最大并发数,以及队列的取消、暂停、恢复操作:
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 设置最大并发操作数
queue.maxConcurrentOperationCount = 2; // 并发队列
queue.maxConcurrentOperationCount = 1; // 串行队列
// 恢复队列,继续执行
queue.suspended = NO;
// 暂停(挂起)队列,暂停执行
queue.suspended = YES;
// 取消队列
[queue cancelAllOperations];
多线程使用的时候,可能会多条线程同时访问/赋值某一变量,如不加限制的话多相处同时访问会出问题。具体情况可以搜索一下相关资料,多线程的 买票问题 很是经典。
iOS线程安全解决方法一般有以下几种:
在iOS中线程安全问题一般是关键字 @synchronized 用加锁来完成。
示例代码:
@synchronized(self) {
// 这里是安全的,同一时间只有一个线程能到这里哦~~
}
需要注意的是 synchronized 后面括号里的 self 是个 token ,该 token 不能使用局部变量,应该是全局变量或者在线程并发期间一直存在的对象。因为线程判断该加锁的代码有没有线程在访问是通过该 token 来确定的。
https://iosgg.cn/wiki/2016-03-10-mac_apps/
这么长时间以来都是用Mac做开发主力机器,有一些常用的软件和小的技巧记录分享一下。
References: The Swift Programming Language
某程序员对书法十分感兴趣,退休后决定在这方面有所建树。于是花重金购买了上等的文房四宝。一日,饭后突生雅兴,一番磨墨拟纸,并点上了上好的檀香,颇有王羲之风范,又具颜真卿气势,定神片刻,泼墨挥毫,郑重地写下一行字:
hello world
。
print("Hello, World")
定义变量用 var
,常量用 let
。常量只能赋值一次,但不用必须在声明的时候去赋值。
var myVariable = 42
myVariable = 50
let myConstant = 42
编译器会自动推断常量和变量的类型,但是如果推断不出来(比如说没有初始值等),就需要声明类型。
let helloTalk : String
helloTalk = "helloTalk"
let helloTalk = "helloTalk"
Swift的值不会隐式被转为其他类型
let widthFloat = 93.33 // 自动推断 为 Double
let width : Int = widthFloat // 把 Double 赋值给 Int,会报错
let widthLabel = label + String(width)
// 把值转换成字符串还可以这样: \(ValueName)
let widthString = "width: \(width)."
创建、定义一个数组或者字典
let emptyArray = [String]()
let emptyDictionary = [String: Float]()
// 或者
let emptyArray = []
let emptyDictionary = [:]
数组和字典都是集合类型,对于这种类型的 let
和 var
修饰并非是像普通值的可否赋值。
比如说,用 let
修饰的数组是不能添加和移除数组中的元素,数组中的元素个数、位置均不可变,但是用 var
修饰的数组可以添加/删除元素。
枚举是为一组相关的值定义了一个共同类型,在 Swift
中,枚举是“一等公民”。枚举成员的原始值不仅可以是整形,还可以是字符、字符串、浮点型;此外,枚举还支持属性、方法、甚至是构造函数、扩展和遵守协议。
用 enum
关键词来创建枚举:
enum Direction {
case north
case south
case east
case west
}
// 或者用逗号分隔写作一行:
enum Direction {
case north, south, east, west
}
let direction: Direction = .west
原始值
enum Direction: String {
case north = "北"
case south = "南"
case east = "东"
case west = "西"
}
let north = Direction.north // the sanme as : let north = Direction(rawValue: "北")
north.rawValue // "北"
关联值
Swift 的枚举可以存储各个类型的关联值,而且每个成员的类型可以都不一样。所以对于一个网络请求可以有这样的抽象:一个网络请求的结果,可以是成功或者失败的,如果成功则返回的是我们想要的数据,不成功返回错误原因,那么可以写成这样:
enum Result<Value> {
case success(Value)
case failure(Error)
}
Value
是泛型语法,可以是任何你需要的类型。
for-in
let persons = ["person1","person2"]
for personString in persons {
print(personString)
}
// 遍历字典
let dict = [
"name" : "Joke",
"age" : 16
] as [String : Any]
for (key,value) in dict {
print("\(key) : \(value)")
}
if-else
if 2>1 {
print("2 大于 1")
}else{
print("2 还是大于 1 啊")
}
if
后面必须是布尔表达式,如果是一个值的话不会隐式的与 0 比较。
switch
let vegetable = "red pepper"
switch vegetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
print("Everything tastes good in soup.")
}
switch
不仅支持基本数据类型。另外Swift中的switch
语法可以省略break
。但是不能省略 default
,会报 Switch must be exhaustive, consider adding a default clause
的编译错误。
while
var i = 1
while i<100 {
i += 1
}
// 或者
var i = 1
repeat{
i += 1
}while i < 100
记得刚从事工作的时候,经常性的会遇到困难而不知所措。幸而在网上能找到很多牛人前辈们的技术博客能将问题和开发中的知识深入浅出的剖析,给予了我很大的帮助。于是,那会我便下定决心以后有机会一定要写自己的博客。
在CSDN和博客园分别写了几篇总觉得体验不好,不是样式不满意就是广告太多。所以才有这次自己折腾几天用 Hexo 搭建了自己专属的技术博客。
写这个博客主要作为学习以用,记录自己在生活、学习和工作中的点点滴滴。
既可以温故而知新,也可查漏补缺,方便自己回头查找使用方便,或能帮助他人也是好的。
没有链接的博客是孤独的
https://iosgg.cn/2017/10/10/send-week-report-email/
事情的起因是这样的,每周都要发送工作周报,标题是需要的固定的格式(方便规则收信),比如 【工作周报】 xxx 11.20-11.24,发送和抄送也是给固定的人,企业邮箱还没有模板,所以每次发邮件都要复制粘贴标题、发送人和抄送人,只有内容是手写,便萌发了用脚本发送邮件的想法。
https://iosgg.cn/2016/04/12/runtime2_association/
在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)
调用方法(函数)是语言经常使用的功能,在 Objective-C 中专业一点的叫法是 传递消息(pass a message)。Objective-C 的方法调用都是 动态绑定 ,而C语言中函数调用方式是 静态绑定 ( static binding ),也就是说,在编译时期就能决定和知道在运行时所调用的函数。
以下面代码为例:
void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
if(type == 0){
sayHello();
}else{
sayGoodBye();
}
}
基本上,上面的代码在编译的时候编译器就知道 sayHello 和 sayGoodBye 两个函数的存在,函
数地址是硬编码在指令之中的。但是如果换一种写法:
void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
void (*something) ();
if(type == 0){
something = sayHello;
}else{
something = sayGoodBye;
}
something();
}
这就得使用 动态绑定 ,待调用的函数地址需要到运行时才能读取出来。
在 Objective-C 中,对某一个对象传递消息,会用动态绑定机制来决定到底是调用哪个方法。而Objective-C是 C 的超集,底层是由 C语言实现,但是对象接收消息后会调用哪个方法都是在运行期决定。
给对象发送消息可以这么来写:
id object = [list objectAtIndex:1];
在这行代码中, list 称为 接收者, objectAtIndex 叫做 选择器, 选择器和参数合起来称为消息。当编译器看到这行代码的时候,会换成标准的C语言函数调用:
void objc_msgSend(id self, SEL cmd, ...);
id lastObject = objc_msgSend(list, @selector(objectAtIndex:), parameter);
objc_msgSend 这个函数可以接收两个及两个以上的参数,第一个参数是接收者,第二个参数是选择器,后面的参数是保持顺序的原来消息传递的参数,objc_msgSend会依据接收者和选择器来决定调用哪个方法,首先在接收者的方法列表中寻找,如果找不到就会沿着继承体系去向上一层一层的寻找,如果仍旧找不到就会执行消息转发(message forwarding) 。
当消息第一次传递之后,objc_msgSend 会将匹配结果进行缓存,下次会直接调用方法。消息传递除了objc_msgSend之外在特殊情况下还会有其他的方法来处理:
总结:
消息转发可以参看我的博客 runtime系列(三) Objective-C 的消息转发机制与动态添加方法
AFNetworking 3.1.0
AFNetworking 主要的几个类或协议:
AFURLSessionManager
AFHTTPSessionManager
AFNetworkReachabilityManager
AFSecurityPolicy
<AFURLRequestSerialization>
<AFURLResponseSerialization>
AFHTTPSessionManager
是我们使用 AFNetworking 框架最外层或者是可以直接使用的类,它对于框架核心类 AFURLSessionManager
进行了常用的封装。比如设置 BaseURL 和请求序列化,AFHTTPSessionManager
重要的两个属性:
@property (readonly, nonatomic, strong, nullable) NSURL *baseURL;
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer
当发送一个 GET 请求时,框架会依次调用以下三个方法:
[AFHTTPSessionManager GET:parameters:progress:success:failure:]
[AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:]
[AFURLSessionManager dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:]
第一个方法是 AFHTTPSessionManager
对 GET
、POST
、PUT
、PATCH
、DELETE
等请求做的一个封装,它们的区别就是请求方法的不同,不同的请求会传入不同的参数给第二个方法,在第二个方法中会对传入的参数进行序列化。具体的序列化方法可以查看 AFHTTPRequestSerializer
。然后会把序列化之后的 NSMutableURLRequest
作为参数调用第三方方法,这时就进入了框架的核心类: AFURLSessionManager
。
AFURLSessionManager
持有一个 NSURLSession
类型的 session
属性,并作为该属性的 delegate
,遵守实现了 NSURLSessionDelegate
, NSURLSessionTaskDelegate
, NSURLSessionDataDelegate
, NSURLSessionDownloadDelegate
四个代理方法,如果任何子类继承重写这些方法,都必须先调用父类的实现。
initWithSessionConfiguration:
是 AFURLSessionManager
的指定初始化方法,并且在session
初始化指定为其的代理:
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
这样的话当一个网络请求的数据有返回或者状态被更改的时候,可以在代理方法中及时获知。AFURLSessionManager
的 session
属性可能会实例化多个 NSURLSessionDataTask
对象,但是 AFURLSessionManager
并不会直接维护它们,而是通过一个可变字典以 task.taskIdentifier
为 key 保存一个私有类 AFURLSessionManagerTaskDelegate
的实例,这个实例会保存这次网络请求的回调、响应数据和进度,两者是一对一的关系。当 AFURLSessionManager
所实现的协议方法被调用时,会及时更新 AFURLSessionManagerTaskDelegate
实例的数据。
在实现文件中,框架还对 NSURLSessionDataTask
的 resume
和 suspend
方法进行了 Hook,在方法执行的时候发送通知。
除此之外,AFURLSessionManager
还管理者 AFSecurityPolicy
和 AFNetworkReachabilityManager
,来保证请求的安全和查看网络连接情况,它有一个 AFJSONResponseSerializer
的实例来序列化 HTTP 响应。
<AFURLRequestSerialization>
和 <AFURLResponseSerialization>
只是两个协议,并且分别只有一个方法。
@protocol AFURLRequestSerialization <NSObject, NSSecureCoding, NSCopying>
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
@end
@protocol AFURLResponseSerialization <NSObject, NSSecureCoding, NSCopying>
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
data:(nullable NSData *)data
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
@end
实现 <AFURLRequestSerialization>
协议的有 AFHTTPRequestSerializer
及其两个子类: AFJSONRequestSerializer
和 AFPropertyListRequestSerializer
,AFHTTPRequestSerializer
除了实现协议之外还封装了对 TTPHeader 和 HTTPBody 操作。
实现 <AFURLResponseSerialization>
协议的有 AFHTTPResponseSerializer
及AFJSONResponseSerializer
等数个子类。
AFHTTPResponseSerializer
有两个属性分来判断接受的 StatusCode 和 ContentType:
@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;
@property (nonatomic, copy, nullable) NSSet <NSString *> *acceptableContentTypes
而 AFJSONResponseSerializer
是 AFHTTPSessionManager
类默认的响应序列化类型。
AFNetworkReachabilityManager
作用是监听网络,是一个比较独立的模块,可以拿出来单独使用。
指定构造方法: initWithReachability:
,AFNetworkReachabilityManager
会持有这个 SCNetworkReachabilityRef
属性。
调用 startMonitoring
开始监听:
- (void)startMonitoring {
// 先停止监听
[self stopMonitoring];
// 不符合条件,直接返回
if (!self.networkReachability) {
return;
}
// 创建一个block作为 SCNetworkReachabilityContext 初始化的第二个参数,参数名:info 参数类型:无类型指针
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
// 创建监听上下文
SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
// 设置监听回调,回调的是 AFNetworkReachabilityCallback 函数,并且callback会被作为参数一起回调
SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
// 开始监听
SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
// 获取当前的网络状态并回调
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
SCNetworkReachabilityFlags flags;
if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
AFPostReachabilityStatusChange(flags, callback);
}
});
}
我的博客:https://iosgg.cn/
Git 配置变量存放在以下三个不同的地方:(摘自1.5 起步 - 初次运行 Git 前的配置)
/etc/gitconfig
文件:系统中对所有用户都普遍适用的配置。若使用 git config
时用 --system
选项,读写的就是这个文件。~/.gitconfig
文件:用户目录下的配置文件只适用于该用户。若使用 git config 时用 --global
选项,读写的就是这个文件。.git/config
文件):这里的配置仅仅针对当前项目有效。每一个级别的配置都会覆盖上层的相同配置,所以 .git/config
里的配置会覆盖 /etc/gitconfig
中的同名变量。$ git config --global user.name "Sanyucz"
$ git config --global user.email [email protected]
要检查已有的配置信息,可以使用 git config --list 命令
版本库(仓库repository
)的创建和文件添加与提交
// 初始化仓库
git init
// fileName 是要添加的文件 添加所有可以用通配符 *
git add <fileName>
// 提交到本地,message是提交的信息
git commit -m "<message>"
// 查看仓库状态: git status
git status
// 如果要查看具体内容:git diff
git diff readme.txt
// 查看conmmit的历史记录:git log
git log
// 如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline 参数:
git log --pretty=oneline
// 查询历史命令:git reflog
git reflog
// 查看工作区和仓库文件的区别
git diff HEAD -- < fileName >
// 回退上一版本:
git reset --hard HEAD^
// 回退指定版本:3628164 是用 git log 查询出来的版本数值
git reset --hard 3628164
// 放弃修改文件
git checkout -- file
//可以把已被添加到暂存区的 'file' 移到工作区
git reset HEAD file
// 克隆远程仓库
git clone <url>
// 添加远程仓库
git remote add origin https://github.com/sanyucz/cocoachina-ios-app.git
// 删除远程仓库
git remote rm origin
// 创建dev分支,然后切换到dev分支:
git checkout -b dev
// 和下面两条命令等同:
git branch dev
git checkout dev
// 查看当前分支:
git branch
// 把 'dev' 分支合并到主分支然后再删除 'dev' 分支
git merge dev
git branch -d dev
// 删除远程分支:(其实是把一个空分支push到远程某个分支)
git push origin :dev
// 如果你想把当前的修改都放弃,可以用下面的命令回到合并之前的状态:
git reset --hard HEAD
// 如果你已经把合并后的代码提交,但还是想把它们撒销:
git reset --hard ORIG_HEAD
// 再如果你不光把代码提交,你提交之后又做了新的修改,又不想放弃修改
// HEAD 合并的那个 版本号
git revert -m 1 HEAD
git tag v1.0 // 打标签到当前分支最新的 commit 上
git tag v0.9 6224937 // 打标签到当 head 为 6224937 的 commit 上
git tag // 查看标签
git show <tagname> // 查看标签信息
git tag -a v0.1 -m "version 0.1 released" 3628164 // -a指定标签名,-m指定说明文字
git tag -d v0.1 // 删除标签
git push origin <tagname> // 推送某个标签到远程
git push origin --tags // 推送到远程的本地标签
git tag -d v0.9 // 加上下个命令删除远程的标签
git push origin :refs/tags/v0.9
一般可以:co
表示 checkout
,ci
表示 commit
,br
表示 branch
git config --global alias.st status
// 一个丧心病狂的别名配置:
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
/etc/gitconfig
文件:系统中对所有用户都普遍适用的配置。若使用 git config
时用 --system
选项,读写的就是这个文件。~/.gitconfig
文件:用户目录下的配置文件只适用于该用户。若使用 git config
时用 --global
选项,读写的就是这个文件。.git/config
文件):这里的配置仅仅针对当前项目有效。每一个级别的配置都会覆盖上层的相同配置,所以 .git/config
里的配置会覆盖 / etc/gitconfig
中的同名变量。git config --global user.name "John Doe"
git config --global user.email [email protected]
廖大的图实在是太通俗易懂了,无耻盗图ing..
Git的版本库里面有个 stage
(暂存区),我们平时工作的文档目录是工作区,对文件有了修改执行 git add
命令就会把变化的文件添加到版本库的暂存区,再执行 git commit
才会把文件从暂存区添加到当前分支。
stage
(暂存区)在物理上的路径是 .git/index
,文件存储的是每次的修改。
而 HEAD
其实是一个指向当前分支的指针,
当创建了 dev
分支的时候,是创建了一个 dev
指针,并且直接把 HEAD
指向 dev
指针。
当合并分支的时候直接把主分支 master
指向 dev
分支。
在上上一篇博客 runtime系列(一) objc_msgSend 中介绍了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。本篇博客就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )。
开发可能经常会遇到这种情况:
2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff9f2d94f2 __exceptionPreprocess + 178
1 libobjc.A.dylib 0x00007fff90db3f7e objc_exception_throw + 48
2 CoreFoundation 0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x00007fff9f249571 ___forwarding___ + 1009
4 CoreFoundation 0x00007fff9f2490f8 _CF_forwarding_prep_0 + 120
5 runtime 0x0000000100001c1c main + 124
6 libdyld.dylib 0x00007fff91df85ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
这个异常信息是由 NSObject 的 doesNotRecognizeSelector: 方法抛出来的,本来是给 AutoDictionary 的一个实例对象发送消息,但是该对象并没有 setDate: 方法,所以消息转发给了 NSObject ,最后抛出异常。
先看下消息处理机制流程图:
消息转发分为两阶段三步,第一阶段先看接受消息的对象能不能自己处理这个无法解读的消息,这一步可以动态的添加方法去解读接受这个消息;第二阶段是先看看对象自己不能处理这个消息,能不能交给其他对象来进行处理,在这一步如果仍然无法解读消息,那么就会走最后一步:把和消息有关的所有细节封装到一个 NSInvocation 中,再询问一次对象是否能解决。
看下三个方法:
// 询问对象是否自己处理,是返回YES,一般会在这个方法里面动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 这一步询问对象把消息交给哪个对象来进行处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
// 如果走到这一步的话,就把消息的所有信息封装成 NSInvocation 对象进行 "最后通牒"
- (void)forwardInvocation:(NSInvocation *)anInvocation;
来一段代码示例:
新建一个 AutoDictionary 类,添加一个 NSDate 类型的 date 属性,在实现文件里面用 @dynamic date; 禁止自动生成存取方法,这样当代码中给 AutoDictionary 实例对象的 date属性赋值时就会出现消息无法解读的现象。
.h 文件:
@interface AutoDictionary : NSObject
@property (nonatomic, strong) NSDate *date;
@end
.m 实现文件代码内容:
@interface AutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
/**
* 该类仅在实现文件 实现了
* - (NSDate *)date
* - (void)setDate:(NSDate *)date
* 两个方法,用于处理 AutoDictionary 无法解读的消息
*/
@property (nonatomic, strong) MethodCreator *methodCreator;
@end
@implementation AutoDictionary
//
@dynamic date;
//
- (instancetype)init{
if (self = [super init]) {
self.backingStore = [NSMutableDictionary dictionary];
self.methodCreator = [MethodCreator new];
}
return self;
}
#pragma mark - 消息转发机制 :1.动态添加方法 2.后备消息接收者 3.封装NSInvocation,最后通牒
// 3. 封装NSInvocation,最后通牒
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//
}
// 2. 无法接受消息,选择由谁来接受
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.methodCreator;
}
// 1. 动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selString = NSStringFromSelector(sel);
if ([selString hasPrefix:@"set"]) {
class_addMethod(self, sel, (IMP)autoDictSetter, "");
}else{
class_addMethod(self, sel, (IMP)autoDictGetter, "");
}
return YES;
}
id autoDictGetter (id self, SEL _cmd){
AutoDictionary *dict = self;
NSString *key = NSStringFromSelector(_cmd);
return [dict.backingStore objectForKey:key];
}
void autoDictSetter (id self, SEL _cmd, id value){
AutoDictionary *dict = self;
NSString *selString = NSStringFromSelector(_cmd);
NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)];
key = [key lowercaseStringWithLocale:[NSLocale currentLocale]];
if (value) {
[dict.backingStore setObject:value forKey:key];
}else{
[dict.backingStore removeObjectForKey:key];
}
}
@end
测试代码:
AutoDictionary *dict = [AutoDictionary new];
dict.date = [NSDate date];
NSLog(@"dict.date = %@",dict.date);
函数的关键字是 func
,函数定义的格式是:
func funcName(para:paraType) -> returnType{
// code
}
其中参数的那部分的详细结构是用小括号括起来,参数名,冒号,参数类型: (number:Int)
。
在默认情况下,函数的参数标签使用参数名,或者用 _
不使用参数标签,也可以自定义标签,我们定义一个奇葩的函数:
func 求和(数字1 num1:Float, 数字2 num2:Float) -> Float {
return num1 + num2
}
求和 就是方法名,数字1 就是自定义的参数标签。调用时会显示标签:
let sum = 求和(数字1: 2, 数字2: 4)
swift
还可以用元组返回多个返回值:
func compare(numarray:([Int])) -> (min:Int, max:Int) {
var min = numarray[0]
var max = numarray[0]
for num in numarray {
if num > max {
max = num
}else if num < min{
min = num
}
}
return (min, max)
}
调用时获取返回值:
compare(numarray: [1, 2, 3, 4, 5]).max
swift
语法中函数可以嵌套函数,用于分割太长或者太复杂的函数:
// 不要在意逻辑,只是为了示例一下。。。
func sumWithArray(numArray:([Int])) -> Int{
var sum = 0
func add(num1:Int, num2:Int) -> Int{
return num1 + num2
}
sum = add(num1: numArray[0], num2: numArray[1])
return sum
}
函数还可以用一个函数做为返回值
func makeMethod() -> ((Int)->(Int)) {
func addOne(num:Int)->(Int){
return num+1
}
return addOne
}
函数调用:
print("makeMethod()(1993): ",makeMethod()(1993))
// makeMethod()(1993): 1994
/**
makeMethod() 返回的是一个函数,继续传入参数 1993,最后返回 1994
*/
函数可以把一个函数当做返回值返回,也可以当做一个参数来传入:
func sumOfMaxMin(numarray:([Int]),compare:(_ numarray:([Int]))->(min:Int, max:Int)) -> (Int) {
return compare(numarray).max + compare(numarray).min
}
可以看到, sumOfMaxMin
函数有两个参数:numarray:([Int])
和 compare:(_ numarray:([Int]))->(min:Int, max:Int)
。其中 compare
是一个函数。
在调用的时候:
var sumOfMaxMinValue = sumOfMaxMin(numarray: [1, 2, 3, 4, 5],compare: compare)
compare
是上个例子中的函数。当然,我们也可以不传入一个现成已经定义和实现的函数:
var sumOfMaxMinValue = sumOfMaxMin(numarray: [1, 2, 3, 4, 5]) { (numarray:([Int])) -> (min: Int, max: Int) in
var min = numarray[0]
var max = numarray[0]
for num in numarray {
if num > max {
max = num
}else if num < min{
min = num
}
}
return (min, max)
}
大家伙看到这里,肯定会一拍大腿:哎呦,这玩意不就是闭包嘛!
(The Swift Programming Language)函数实际上是一种特殊的闭包:它是一段能之后被调取的代码。闭包中的代码能访问闭包所建作用域中能得到的变量和函数,即使闭包是在一个不同的作用域被执行的
我们可以使用{}来创建一个匿名闭包。使用in将参数和返回值类型声明与闭包函数体进行分离。
let numArray:([Int]) = [1, 2, 3, 4, 5]
var newNumArray:([Int]) = numArray.map({
(num:Int) -> Int in
let newNum = num * 3
return newNum
})
如果闭包的类型已知,那么可以省略参数和返回值的类型
let numArray:([Int]) = [1, 2, 3, 4, 5]
var newNumArray:([Int]) = numArray.map({
num in
let newNum = num * 3
return newNum
})
单个语句闭包会把它语句的值当做结果返回
let numArray:([Int]) = [1, 2, 3, 4, 5]
var newNumArray:([Int]) = numArray.map({
num in
num * 3
})
如果把上面的闭包写成一行的话
let numArray:([Int]) = [1, 2, 3, 4, 5]
var newNumArray:([Int]) = numArray.map({num in num * 3})
我们可以通过参数位置而不是参数名字来引用参数,那么上面的代码就变成这样
let numArray:([Int]) = [1, 2, 3, 4, 5]
var newNumArray:([Int]) = numArray.map({$0 * 3})
当一个闭包是传给函数的唯一参数,我们可以完全忽略括号
let numArray:([Int]) = [1, 2, 3, 4, 5]
var newNumArray:([Int]) = numArray.map{$0 * 3}
https://iosgg.cn/2016/04/16/runtime3_message_forwarding/
在上上一篇博客 runtime系列(一) objc_msgSend 中介绍了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。本篇博客就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )。
https://iosgg.cn/2017/06/10/afnnetworking-source-read/
AFNetworking 3.1.0
https://iosgg.cn/2017/02/17/dont_abuse_lazy_load/
写代码是一种习惯的养成,一种生活的态度。
https://iosgg.cn/2016/04/08/runtime0_object_class/
Objc 中任何对象都可以称之为 id 类型,那么看下在 objc.h 对 id 类型的定义:
https://iosgg.cn/2016/04/10/runtime1_objc_msgSend/
调用方法(函数)是语言经常使用的功能,在 Objective-C 中专业一点的叫法是 传递消息(pass a message)。Objective-C 的方法调用都是 动态绑定 ,而C语言中函数调用方式是 静态绑定 ( static binding ),也就是说,在编译时期就能决定和知道在运行时所调用的函数。
https://iosgg.cn/wiki/2016-08-08-shadowsocks_vps_gfw/
假设我们已经有一个 VPS,并且可以 ssh 登陆上去。
什么是 AOP
: (site: baike.baidu.com),引用百度百科中的解释就是:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能:
日志记录,性能统计,安全控制,事务处理,异常处理等等主要意图:
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
iOS 开发中的 AOP
在 Objective-C 中,类的方法列表会把选择器的名称映射到方法的实现上,这样 动态消息转发系统 就可以以此找到需要调用的方法。这些方法是以函数指针的形式来表示,这种指针叫做 IMP。
如下:
id (*IMP) (id, SEL, ...)
Objective-C
的 runtime
机制以此提供了获取和交换映射IMP
的的接口:
// 获取方法
Method class_getInstanceMethod(Class cls, SEL name);
// 交换两个方法
void method_exchangeImplementations(Method m1, Method m2)
我们可以通过上面两个方法来进行选择器和所映射的IMP
进行交换:
来,直接上代码示例,比如我们的要实现功能是在每个控制器的- viewDidLoad
方法里面log一下,一般有三种实现方式:
Method Swizzling
进行 hook,以达到 AOP
编程的**第一种实现的代码是在每个类的里面都这么写:
- (void)viewDidLoad {
[super viewDidLoad];
DDLog();
}
第二种是只在基类里面写。然后所有的控制器都继承这个基类。
最后一种是最佳的解决方案:
@implementation UIViewController (Log)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(log_viewDidLoad);
//
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)log_viewDidLoad{
[self log_viewDidLoad];
DDLog(...);
}
@end
注意:
+ (void)load
?因为父类、子类和分类的该方法是分别调用,互不影响,而且是在类被加载的时候必定会调用的方法。https://iosgg.cn/2017/04/08/weak_ref_array/
类定义的关键字是 class
,我们用 class
+ 类名 + ":" + 父类,比如定义一个 Person
类,类中去声明和定义变量和函数:
class Person: NSObject {
var name: String
public func sayHello(){
print("Hello ~")
}
}
类的扩展
extension Person { // 给人加一个飞的功能...
func fly() {}
}
存储属性就是存储在类或者结构体一个实例里的一个常量和变量,用 var
或者 let
修饰。
但是如果一个结构体实例被声明为常量,那么即便这个结构体的某个属性是变量,也是不能去改变的:
let point = CGPoint(x: 1, y: 1)
point.x = 10 // 编译是不会通过的
延迟存储属性
lazy var nameLabel:UILabel = {
let label = UILabel()
// ...
return label
}()
其实就是把一个立即执行的闭包的返回值赋值给属性,以达到懒加载的目的。
属性观察器
willSet
在新的值被设置之前调用
didSet
在新的值被设置之后立即调用
需要注意的是:当为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者。
计算属性提供了一个 getter
和一个可选的 setter
。属性的 getter
和 setter
的关键词是 get
和 set
,在 setter
中新值是 newValue
class Person: NSObject {
var birthYear: Int
var age : Int{
set{
self.age = newValue
self.birthYear = 2016 - newValue
}
}
}
只读计算属性
当一个属性只有 getter
而没有 setter
时,那么它就是一个只读计算属性
var person = Person()
person.age = 10
我们可以给 Person
自定义一个构造函数,构造函数中需要给所有的存储型属性一个赋值或者默认值:
init(name: String) {
self.name = name
}
这样我们就可以用 name
来实例化一个对象: var person = Person(name:"Tom")
如果所有属性都有默认值,我们没有自定义的构造,系统会生成一个默认的构造函数:var person = Person()
相应的,如果我们自定义了一个构造函数,那么系统便不会为该类生成默认的构造函数。
但是,我们可以把自定义的构造函数写到类的扩展(extension
)里,而不是类的原始定义里面。
与构造函数对应的是析构函数:deinit
指定构造器和便利构造器
指定构造器里面会初始化类实例所有的属性,所以为了保证继承的属性也能被初始化,子类的指定构造器都会调用父类的指定构造器,而类的便利构造器总是会调用本类的指定构造器。
也就是说:指定构造器需要向上调用,便利构造器需要横向调用
所以,我们可以把父类的指定构造函数重写成便利构造,却不能重写父类的便利构造。即便重写了父类的便利构造函数,但是由于我们不能直接调用父类的便利构造函数,所以不用给函数以 override
修饰。
必要构造器 如果某个构造函数被 required
修饰,那么该函数就是必要构造函数,子类继承该类时都必须要实现改构造函数。在子类重写父类的构造函数的时候,也要加 required
来修饰,以确定继承链上子类的子类也遵守。
除了构造和析构函数,类还有实例的私有函数、公共函数和静态函数。
Swift
中的访问控制有 模块 和 源文件 两个概念。用 "import" 导入的就是模块。
对于类而言的修饰词与权限:
修饰词 | 权限 |
---|---|
open |
修饰的类可以随意继承与访问 |
public |
修饰的类只能在本模块内被继承,但是可以随便访问 |
internal |
默认 - 模块内拥有访问权限 |
fileprivate |
是文件外部不能被访问 |
private |
是文件内部不能被访问 |
final |
是文件内部也不能被继承 |
当然了,对于一个 internal
的类,其属性和方法的级别是不会超出类本身的,比如不可能是 public
。
函数也是同样的修饰词和权限,只是类的继承对应函数的重载权限。
函数除了以上几个,常用的修饰词还有 static
和 override
。
static
修饰的是类方法。override
修饰的是重写父类的方法。
以后会持续更新的~
博客在于内容而不在于形式,反反复复折腾体验了几个主题。以后或许便会稳定不在折腾。
前段时间用Github上Pages服务配合Hexo搭建了自己的博客,后来整理下记录下过程。
后来将主题换为 next (最终是使用fexo),所以更新一下,方便后来人,也便于自己再用到来查看
从写博客到在网页上展示的过程是这样的,用MarkDown写下自己的博客内容,通过Hexo生成静态的HTML网页,然后push(git源代码管理工具提交)到 coding/Github 上你的代码仓库,再通过代码托管平台的Pages服务发布到网上。
步骤:
github_username.github.io
的仓库,比如我的Github用户名是 sanyucz
,所以仓库名称就是 sanyucz.github.io
,这个名字是要严格遵守的,搭建完成后也是在浏览器中直接通过 sanyucz.github.io
来访问自己的博客。同样的 coding 上直接新建一个用户名的仓库,比如我的就是 sanyucz
,搭建完成之后可以通过 sanyucz.coding.me
来访问。当 Hexo 也安装好之后执行以下命令来搭建本地博客:
```
$ hexo init <folder> // <folder> 是博客的本地路径
$ cd <folder> // cd 切换目录到博客的本地路径
$ npm install
```
当执行完之后,如果成功了的话,那么目录下的结构应该是这样的:
```
.
├── _config.yml // 博客的配置文件
├── package.json
├── scaffolds
├── scripts
├── source // 博客的内容
| ├── _drafts
| └── _posts
└── themes // 博客的主题
```
继续在终端中输入 hexo s 会看到以下输出:
```
INFO Start processing
INFO Hexo is running at http://0.0.0.0:4000/. Press Ctrl+C to stop.
```
这时候在浏览器中输入 http://0.0.0.0:4000/ 就能看到自己的博客了,
停止服务摁下 Ctrl + C
。
```
// Hexo 常用的命令
hexo s // hexo server 开启本地 Hexo 服务
hexo g // hexo generate 生成本地 html js
hexo d // hexo deploy 发布到远程仓库
hexo c // hexo clean 删除generate生成的文件和缓存
hexo n // hexo new,用于新建一篇文章 eg: hexo new "我的第一篇博客"
```
Hexo 的配置文件有两个,其中一个是在博客目录下的 _config.yml
,另外一个在 blog/theme/your_theme/_config.yml
,从位置就可以看出来,在博客目录下的主要是配置博客站点信息,比如所博客的title,作者等。在主题目录下多是配置主题样式相关,关于主题,下面再讲。
# Hexo Configuration
## Docs: https://hexo.io/docs/configuration.html
## Source: https://github.com/hexojs/hexo/
# Site
title: 王修斌的技术博客 // 博客标题
subtitle: 与肝胆人共事,无字句处读书 // 博客子标题
description: 爱生活、爱编程,爱学习,爱折腾。 // 博客的描叙
author: sanyucz // 作者
language: zh-CN // 默认语言
timezone:
# URL
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: https://sanyucz.github.io/ // 博客的地址
root: /
permalink: :year/:month/:day/:title/ // 时间格式
permalink_defaults:
# Pagination
## Set per_page to 0 to disable pagination
per_page: 10 // 博客列表每页显示博客的数量
pagination_dir: page
# Extensions
## Plugins: http://hexo.io/plugins/
#RSS订阅
plugin:
- hexo-generator-feed
- hexo-generator-baidu-sitemap
# Extensions
## Plugins: https://hexo.io/plugins/
# theme: hexo-theme-next
# theme: hexo-theme-material
theme: jacman // 博客的主题
stylus:
compress: true
// 博客评论 一般是多说和disqus,disqus是世界主流,在国内大多用多说,原因你懂得,这里两个都用了
duoshuo_shortname: sanyucz
disqus_shortname: sanyucz
# Deployment
## Docs: https://hexo.io/docs/deployment.html
// 远程仓库配置 这里我是推送到 coding 和 github 两个平台的仓库,所以配置了两个
deploy:
type: git
repo:
github: [email protected]:sanyucz/sanyucz.github.io.git,master
coding: [email protected]:sanyucz/sanyucz.git,master
# branch: master
# message: github
npm install hexo-generator-index --save
npm install hexo-generator-archive --save
npm install hexo-generator-category --save
npm install hexo-generator-tag --save
npm install hexo-server --save
npm install hexo-deployer-git --save
npm install hexo-deployer-heroku --save
npm install hexo-deployer-rsync --save
npm install hexo-deployer-openshift --save
npm install [email protected] --save
npm install [email protected] --save
npm install hexo-generator-feed@1 --save
npm install hexo-generator-sitemap@1 --save
插件的作用各不相同,有的是用来生成RSS订阅,有的是压缩 js/CSS。
各种插件的安装推荐查看我所使用的 Jacman 主题的作者写的文章:如何使用 Jacman 主题
Hexo 自带的主题太过于简单,这里推荐使用 Next。简洁,但不简单。
下载下来重命名为 next
并放到 blog/theme 下面,修改 站点配置文件 blog/_config.yml
中的 theme
字段: theme: next
,注意冒号之后要有空格。
从万网(阿里域名)买了三年的top域名,三年也不过就一包烟钱而已,买过域名之后需要解析,解析是为了访问域名可以直接访问我们的博客。
在阿里域名注册并登陆之后,点击 控制台-域名-解析,解析如下:
![/images/aliyuming.png)
因为国内访问 Github 很慢,所以我默认访问的是coding上的博客。如果只在Github上面托管了博客,那么只需要解析一条默认的就好,需要注意的是域名后面有个 ‘ . ’。解析成功之后等待几分钟就可以通过域名来访问博客啦。
DTraceProviderBindings
错误{ [Error: Cannot find module './build/Release/DTraceProviderBindings'] code: 'MODULE_NOT_FOUND' }
{ [Error: Cannot find module './build/default/DTraceProviderBindings'] code: 'MODULE_NOT_FOUND' }
{ [Error: Cannot find module './build/Debug/DTraceProviderBindings'] code: 'MODULE_NOT_FOUND' }
试过官网建议 npm install hexo --no-optional
,不行。
网上说的删除插件~~~,什么鬼?
最后我直接把引起错误的代码给注释了,根据报错信息,定位到dtrace-provider.js
,
注释其中内容:
var builds = ['Release', 'default', 'Debug'];
for (var i in builds) {
try {
var binding = require('./build/' + builds[i] + '/DTraceProviderBindings');
DTraceProvider = binding.DTraceProvider;
break;
} catch (e) {
// if the platform looks like it _should_ have DTrace
// available, log a failure to load the bindings.
if (process.platform == 'darwin' ||
process.platform == 'sunos' ||
process.platform == 'freebsd') {
console.error(e);
}
}
}
OK,继续搞起!
虽然博客已经搭建起来,并且有了一个好看的主题,但是很多时候还是显得过于千篇一律。
尝试了多种的优化方式,我觉得最简单、效果最好的就是,只在Github上托管的博客,最好在coding上也托管一份,毕竟coding是国内的,访问速度上要快很多。
另外再推荐一个压缩页面的插件:gulp
,具体使用请另行百度。
其他方式:
突然想起前段时间做的需求:根据视图的大小来自动调整字体的大小。系统有给定的根据字体大小来自动调整视图(Label、Button)的大小,但是反过来则不行,网上也没有比较好的解决办法。
效果如下:
所以,自己来分享一个办法:从系统的字体大小开始,计算文字所占用的 Size,超出则字体小一号再计算,直至合适;当文字尺寸小于所能占用的最大 Size,则字体大一号在继续计算,直至找到合适的大小。
所以,就需要系统的一个计算字体大小的方法:
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes context:(nullable NSStringDrawingContext *)context
参数:
options
类型的详细解释:
typedef NS_OPTIONS(NSInteger, NSStringDrawingOptions) {
NSStringDrawingUsesLineFragmentOrigin = 1 << 0,
// 整个文本将以每行组成的矩形为单位计算整个文本的尺寸
// The specified origin is the line fragment origin, not the base line origin
NSStringDrawingUsesFontLeading = 1 << 1,
// 使用字体的行间距来计算文本占用的范围,即每一行的底部到下一行的底部的距离计算
// Uses the font leading for calculating line heights
NSStringDrawingUsesDeviceMetrics = 1 << 3,
// 将文字以图像符号计算文本占用范围,而不是以字符计算。也即是以每一个字体所占用的空间来计算文本范围
// Uses image glyph bounds instead of typographic bounds
NSStringDrawingTruncatesLastVisibleLine
// 当文本不能适合的放进指定的边界之内,则自动在最后一行添加省略符号。如果NSStringDrawingUsesLineFragmentOrigin没有设置,则该选项不生效
// Truncates and adds the ellipsis character to the last visible line if the text doesn't fit into the bounds specified. Ignored if NSStringDrawingUsesLineFragmentOrigin is not also set.
}
在工程考虑性能,部分控件采用的是自己绘制,所以代码应该如下:
- (void)drawRect:(CGRect)rect text:(NSString *)text color:(UIColor *)color{
// Draw 的最大 Size
CGSize maxSize = CGSizeMake(rect.size.width, rect.size.height);
int currentFontSize = [UIFont systemFontSize];
CGSize requiredSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize: currentFontSize]} context:nil].size;
// 小于最大尺寸 FontSize += 1
if(requiredSize.height < maxSize.height && requiredSize.width < maxSize.width)
{
while (requiredSize.height <= maxSize.height && requiredSize.width < maxSize.width) {
currentFontSize++;
requiredSize=[text boundingRectWithSize:maxSize options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:currentFontSize]} context:nil].size;
}
currentFontSize--;
}
else // 大于等于 FontSize -= 1
{
while (requiredSize.height >= maxSize.height || requiredSize.width >= maxSize.width) {
currentFontSize--;
requiredSize=[text boundingRectWithSize:maxSize options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:currentFontSize]} context:nil].size;
}
}
// 绘制自适应文本
[text drawAtPoint:CGPointMake(rect.origin.x + rect.size.width/2 - requiredSize.width/2,
rect.origin.y + rect.size.height/2 - requiredSize.height/2)
withAttributes:@{
NSFontAttributeName:[UIFont systemFontOfSize: currentFontSize],
NSForegroundColorAttributeName:color
}];
}
demo:https://github.com/sanyucz/example/tree/master/DrawAutoLayoutFont
世界上可以分成10种人,懂二进位的人,以及不懂二进位的人
写代码是一种习惯的养成,一种生活的态度。
有一次同事看着我写的代码说,你为什么要这么写啊?
我看了一下,是在 ViewController 和 Cell 里初始化视图,还有数据模型数组的时候,我都是用的懒加载(Lazy-Load)。
为什么这么喜欢用懒加载来实例化一个属性呢?
以前学 iOS 开发的时候,
nil
;基本上每个属性我都希望去懒加载实现它,这会给我一种错觉:这样写更好,性能更高!
其实,这是一个不好的习惯,随着编程时间越发的长,越是觉得之前有些偏激。
iOS 中懒加载的写法一般为重写 getter
方法,判断属性是否为 nil
,是的话去初始化,否就直接返回:
- (UIView *)layerView {
if (!_layerView) {
_layerView = [UIView new];
}
return _layerView;
}
但是我们有更清晰、简洁的写法
一般来说,如果 layerView
是控制器的属性,我们一般都会在 viewDidLoad
方法中去加载视图;如果是一个视图,我们一般会在 initWithFrame:
加载子视图,我们只需要安安静静的用以下代码来初始化即可:
_layerView = [UIView new];
根本无需使用懒加载,因为如果你不是一个人在开发的话,你永远不会知道你的队友会在 get
方法里面做什么。
而且这样写更简洁,更清晰。当属性很多的时候也可以使用以下方式来初始化:
self.layerView = ({
[UIView new];
});
用懒加载至少六行代码,现在只需要一行或者三行就可以做到。
我们不能使用懒加载来防止那些可能出现的错误
很大一部分人用懒加载是为了保证数组和字典等集合类型在使用中永远不会是空值,这是错误的做法,因为可变集合类型被初始化之后,在正确的使用中如果不会被置 nil
,那么也无需使用懒加载。如果因此而引发的问题,也可以帮我们提前找到原因。
对于耗时或性能很大的操作,我们可以使用惰性计算而不是懒加载
比如,我重构项目遇到的一个需求:请求股票列表返回的数据会告诉我总共会有上千条数据,并且不做分页,就是全部展示,滑到第几条就去请求第几条的数据。
上千条数据不做分页,我们也不可能全部请求回来,即便能全部请求回来也不可能在一个方法里去做这样的操作:
NSMutableArray *dataArray = [NSMutableArray array];
for (int i = 0; i < 100000; ++i) {
[dataArray addObject:data];
}
但是,对于 TableView 来讲,上千条数据,当然需要 Array 的 count 返回是一千。这个时候我们可以用惰性计算来解决这个问题:有多少条数据,我们就让数组返回的 count 是多少,但是只有真正的向数组取这个下标的对象的时候,我们才去处理!
那我们继承 NSArray 来写(真正的写一个 NSArray 还需要重写其他几个方法,在此不细说):
typedef id(^HTLazyArrayItemBlock)(NSInteger index);
@interface HTLazyArray : NSArray
- (instancetype)initWithItemBlock:(HTLazyArrayItemBlock)block count:(NSInteger)count;
@end
#import "HTLazyArray.h"
@interface HTLazyArray()
@property (nonatomic, copy) HTLazyArrayItemBlock block;
@end
@implementation HTLazyArray {
NSInteger _ct;
}
- (instancetype)initWithItemBlock:(HTLazyArrayItemBlock)block count:(NSInteger)count {
if (self = [super init]) {
_ct = count;
self.block = block;
}
return self;
}
#pragma mark - override
- (NSUInteger)count {
return _ct;
}
- (id)objectAtIndex:(NSUInteger)index {
return self.block(index);
}
@end
我们初始化的时候传入一个 count,被 TableView 的代理方法访问的时候,有则返回数据模型,没有就先返回 nil
,待到网络请求到数据再进行刷新。这样做的性能损耗微乎其微。
self.lazyArray = [[HTLazyArray alloc]initWithItemBlock:^id(NSInteger index) {
HTQuoteAHCellModel *model = weakSefl.cache[@(index)];
return model;
} count:dataTotalCount];
那么,我们到底什么时候该用懒加载呢?
懒加载的使用需要看具体的场景,比如一个很可能不会被使用的属性,使用懒加载确实可以避免无所谓的性能损耗;
还有就是 null_resettable
修饰的属性,该属性意为:setter nullable,但是 getter nonnull,典型的就是控制器的 view
属性:“你可以不要我,把我置空;但只要你需要我,我就是在的”。诸如此类都可以使用懒加载。
https://iosgg.cn/wiki/2016-02-22-CSS-with-H5/
CSS 全称是Cascading Style Sheets,层叠样式表,被用来控制HTML标签的样式,美化网页。CSS有两个重点: 属性 和 选择器 。
https://iosgg.cn/wiki/Swift/2016-10-06-swift_functions_closures/
函数的关键字是 func ,函数定义的格式是:
https://iosgg.cn/2016/05/22/multithreading_iOS/
iOS开发深入之后还是要接触多线程和runloop的,写篇博客讲解、记录下iOS开发中的多线程技术。
https://iosgg.cn/wiki/Swift/2016-10-05-swift_basic_grammar/
References: The Swift Programming Language
Objc 中任何对象都可以称之为 id
类型,那么看下在 objc.h
对 id
类型的定义:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
注释中的描述是 一个指向类的实例的指针,那么是不是意味一个类的实例即对象就是一个 objc_object
结构体呢?再看源码:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
通过阅读 runtime 的源码可以得出以下结构:
注:
class_ro_t
中存放的是编译时可以确定的属性、方法和协议等
objc_object
结构体,结构体中第一个变量是 isa_t
,存放着该对象所属类的信息;objc_class
结构体,继承自 objc_object
,所以类也是一个对象,另外还有两个变量进行方法缓存和数据存放,比如变量、方法(实例方法,以 -
开头的方法)和所遵守的协议。isa
变量中存放的类是元类(meta-class),类是一个对象,对象的类型就是元类,元类存放着类的方法(类方法,以 +
开头的方法)。isa
变量中存放的类是根元类(一般是 NSObject
)。isa
指向其自身做个试验,新建一个 BBObject
类,添加一个属性 bb_name
,头文件如下:
@interface BBObject : NSObject
@property (nonatomic, strong) NSString *bb_name;
@end
那么以下代码会输出什么呢:
NSString *name = @"这是 bb_name";
void *cls = (__bridge void *)([BBObject class]);
void *bb_obj = &cls;
NSLog(@"%@",[(__bridge BBObject*)bb_obj bb_name]);
输出的是:2017-12-16 19:55:33.477716+0800 runtime[45876:7116185] 这是 bb_name
。
因为 Objc 中一个对象就是 首地址指向一个类的连续空间,为什么是连续空间?那是因为对象还有自己属性变量的值要存储,这也是为什么没有给 bb_obj
的 bb_name
属性赋值,却打印出 name
值的原因,在 iOS 中,栈的地址是由高到低,堆的地址是由低到高,在这段代码中栈中依次压入了 name
、bb_obj
,而 bb_obj
对象自身的属性是根据自身首地址进行偏移去获取,所以会取到 name
的值。
使用 clang -rewrite-objc BBObject.m
可以把得到重写后的 C++ 文件,在其中也可以看到其中获取属性就是自身地址加偏移量:
static NSString * _I_BBObject_bb_name(BBObject * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_BBObject$_bb_name));
}
我的博客:https://iosgg.cn/
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.