0%

什么是自动引用计数?

自动引用计数(ARC,Automatic Reference Counting)是指内存管理中对引用采取自动计数的技术。

内存管理的思考方式

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。

自己生成的对象,自己持有

使用以下名称开头的方法名意味着自己生成的对象自己持有:

  • alloc
  • new
  • copy
  • mutableCopy

另外,下列名称也意味着自己生成并持有对象。

  • allocMyObject
  • newThatObject
  • copyThis
  • mutableCopyYourObject
1
2
3
/** 自己生成并持有对象 */
id obj = [[NSObject alloc] init];
/** 自己持有对象 */

使用[NSObject new] [[NSObject alloc] init]是完全一致的。

1
2
3
/** 自己生成并持有对象 */
id obj = [NSObject new]
/** 自己持有对象 */

非自己生成的对象,自己也能持有

用alloc/new/copy/mutableCopy以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。

1
2
3
/** 取得非自己生成并持有的对象 */
id obj = [NSMutableArray array];
/** 取得的对象存在,但自己不持有对象 */

源代码中,NSMutableArray类对象被赋值给变量obj,但变量obj自己并不持有该对象,使用retain方法可以持有对象。

1
2
3
4
5
/** 取得非自己生成并持有的对象 */
id obj = [NSMutableArray array];
/** 取得的对象存在,但自己不持有对象 */
[obj retain];
/** 自己持有对象 */

通过retain方法,非自己生成的对象跟用 alloc/new/copy/mutableCopy 方法生成并持有的对象一样,成为了自己所持有的。

不再需要自己持有的对象时释放

自己持有的对象,一旦不再需要,持有者有义务释放该对象,释放使用release方法。

1
2
3
4
5
/** 自己生成并持有对象 */
id obj = [[NSObject alloc] init];
/** 自己持有对象 */
[obj release];
/** 释放对象,指向对象的指针仍然被保留在变量obj中,貌似能够访问,但对象一经释放绝不可访问 */

用alloc方法由自己生成并持有的对象就通过release方法释放了。自己生成而非自己所持有的对象,若用retain方法变为自己持有,也同样可以用release方法释放。

1
2
3
4
5
6
7
/** 取得非自己生成并持有的对象 */
id obj = [NSMutableArray array];
/** 取得的对象存在,但自己不持有对象 */
[obj retain];
/** 自己持有对象 */
[obj release];
/** 释放对象,对象不可再被访问 */

用 alloc/new/copy/mutableCopy 方法生成并持有的对象,或者用retain方法持有的对象,一旦不再需要,务必要用release方法进行释放。

如果要用某个方法生成对象,并将其返回给该方法的调用方,那么它的源代码又是如何呢?

1
2
3
4
5
6
-(id)allocObject {
/** 自己生成并持有对象 */
id obj = [[NSObject alloc] init];
/** 自己持有对象 */
return obj;
}

如上例所示,原封不动的返回alloc方法生成并持有的对象,就能让调用方也持有该对象。请注意allocObject这个名称是符合前文命名规则的。

1
2
3
/** 取得非自己生成并持有的对象 */
id obj1 = [obj0 allocObject];
/** 自己持有对象 */

allocObject名称符合前文的命名规则,因此它与用alloc方法生成并持有对象的情况完全相同,所以使用allocObject方法也意味着”自己生成并持有对象”。

那么,调用[NSMutable array]方法使取得的对象存在,但自己不持有对象,又是如何实现的呢?根据上下文命名规则,不能使用alloc/new/copy/mutableCopy 开头的方法名,因此要使用object这个方法名。

1
2
3
4
5
6
7
- (id)object {
id obj = [[NSObject alloc] init];
/** 自己持有对象 */
[obj autorelease];
/** 取得的对象存在,但自己不持有对象 */
return obj;
}

上例中,我们使用了autorelease方法。用该方法可以使取得的对象存在,但自己不持有对象。autorelease 提供这样的功能,使对象在超出指定的生存范围时,能够自动并正确地释放(调用release 方法)。

使用NSMutableArray类的array类方法等可以取得谁都不持有的对象,这些方法都是通过 autorelease 而实现的。此外,根据上下文的命名规则,这些用来取得谁都不持有的对象的方法名不能以alloc/new/copy/mutableCopy 开头。

1
2
id obj1 = [obj0 object];
/** 取得的对象存在,但自己不持有对象 */

当然也可以通过 retain 方法将调用 autorelease 方法取得的对象变为自己持有。

1
2
3
4
id obj1 = [obj0 object];
/** 取得的对象存在,但自己不持有对象 */
[obj1 retain];
/** 自己持有对象 */

非自己持有的对象无法释放

对于用 alloc/new/copy/mutableCopy 方法生成并持有的对象,或是用 retain 方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此外得到的对象绝对不能释放。倘若在应用程序中释放了非自己持有的对象就会造成崩溃。例如自己生成并持有对象后,在释放完不再需要的对象之后再次释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 自己生成并持有对象 */
id obj = [[NSObject alloc] init];
/** 自己持有对象 */
[obj release];
/** 对象已释放 */
[obj release];
/**
释放之后再次释放非自己持有的对象
应用程序崩溃

崩溃情况:
再度废弃已经废弃了的对象时崩溃
访问已经废弃的对象时崩溃
*/

或者在”取得的对象存在,但自己不持有对象”时释放。

1
2
3
4
5
6
7
id obj1 = [obj0 object];
/** 取得的对象存在,但自己不持有对象 */
[obj1 release];
/**
释放了非自己持有的对象
这肯定会导致应用程序崩溃
*/

参考链接

《iOS与OS X多线程和内存管理》

RVM是一个命令行工具,它能帮你安装多个版本的Ruby,并切换不同的版本。

安装RVM步骤

1.安装rvm

1
2
3
4
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
$ curl -sSL https://get.rvm.io | bash -s stable
# 如果上面的连接失败,可以尝试:
$ curl -L https://raw.githubusercontent.com/wayneeseguin/rvm/master/binscripts/rvm-installer | bash -s stable

2.载入rvm环境(新开Terminal,会自动重新载入)

1
$ source ~/.rvm/scripts/rvm

3.修改rvm下载的Ruby源,到RubyChina的镜像:

1
$ echo "ruby_url=https://cache.ruby-china.com/pub/ruby" > ~/.rvm/user/db

4.检查rvm是否安装正确

1
2
$ rvm -v
rvm 1.22.17 (stable) by Wayne E. Seguin <wayneeseguin@gmail.com>, Michal Papis <mpapis@gmail.com> [https://rvm.io/]

RVM常用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby -v # 查看ruby 版本
$ rvm list known # 列出已知的 ruby 版本
$ rvm install 2.3.0 # 选择指定 ruby 版本进行更新
$ rvm get stable # 更新 rvm
$ rvm use 2.2.2 # 切换到指定 ruby 版本
$ rvm use 2.2.2 --default # 设置指定 ruby 版本为默认版本
$ rvm list # 查询已安装的 ruby 版本
$ rvm remove 1.9.2 # 卸载移除 指定 ruby 版本

$ curl -L https://get.rvm.io | bash -s stable # 安装 rvm 环境
$ curl -sSL https://get.rvm.io | bash -s stable --ruby # 默认安装 rvm 最新版本
$ curl -sSL https://get.rvm.io | bash -s stable --ruby=2.3.0 # 安装 rvm 指定版本
$ source ~/.rvm/scripts/rvm # 载入 rvm

Gem常用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$ gem -v # 查看 gem 版本
$ gem source # 查看 gem 配置源
$ gem source -l # 查看 gem 配置源目录
$ gem sources -a url # 添加 gem 配置源(url 需换成网址)
$ gem sources --add url # 添加 gem 配置源(url 需换成网址)
$ gem sources -r url # 删除 gem 配置源(url 需换成网址)
$ gem sources --remove url # 删除 gem 配置源(url 需换成网址)
$ gem update # 更新 所有包
$ gem update --system # 更新 Ruby Gems 软件

$ gem install rake # 安装 rake,从本地或远程服务器
$ gem install rake --remote # 安装 rake,从远程服务器
$ gem install watir -v 1.6.2 # 安装 指定版本的 watir
$ gem install watir --version 1.6.2 # 安装 指定版本的 watir
$ gem uninstall rake # 卸载 rake 包
$ gem list d # 列出 本地以 d 打头的包
$ gem query -n ''[0-9]'' --local # 查找 本地含有数字的包
$ gem search log --both # 查找 从本地和远程服务器上查找含有 log 字符串的包
$ gem search log --remoter # 查找 只从远程服务器上查找含有 log 字符串的包
$ gem search -r log # 查找 只从远程服务器上查找含有log字符串的包

$ gem help # 提醒式的帮助
$ gem help install # 列出 install 命令 帮助
$ gem help examples # 列出 gem 命令使用一些例子
$ gem build rake.gemspec # 把 rake.gemspec 编译成 rake.gem
$ gem check -v pkg/rake-0.4.0.gem # 检测 rake 是否有效
$ gem cleanup # 清除 所有包旧版本,保留最新版本
$ gem contents rake # 显示 rake 包中所包含的文件
$ gem dependency rails -v 0.10.1 # 列出 与 rails 相互依赖的包
$ gem environment # 查看 gem 的环境

$ sudo gem -v # 查看 gem 版本(以管理员权限)
$ sudo gem install cocoa pods # 安装 CocoaPods(以管理员权限)
$ sudo gem install cocoapods # 安装 CocoaPods(以管理员权限)
$ sudo gem install cocoapods --pre # 安装 CocoaPods 至预览版(以管理员权限)
$ sudo gem install cocoapods -v 1.4.0 # 安装 CocoaPods 指定版本(以管理员权限)
$ sudo gem update cocoapods # 更新 CocoaPods 至最新版(以管理员权限)
$ sudo gem update cocoapods --pre # 更新 CocoaPods 至预览版(以管理员权限)
$ sudo gem uninstall cocoapods -v 1.4.0 # 移除 CocoaPods 指定版本(以管理员权限)

# 在Catalina系统上执行上述命令会报无权限访问错误,需要使用下面的命令
$ sudo gem install -n /usr/local/bin cocoapods -v 1.9.1

# 如果安装失败的话,可以把需要安装的gem文件下载到本地然后在本地安装,使用命令
$ sudo gem install -n /usr/local/bin ~/Downloads/cocoapods-1.9.1.gem


# Cocoapods相关命令
# 更新Pod
$ pod update --verbose --no-repo-update

# 清除Pod的缓存
$ pod cache clean --all

# 清除Pod库AAA的缓存
$ pod cache clean AAA

如果命令无法修改Gem源,可以手动修改Gem源,Gem的配置文件位置在~/.gemrc,编辑~/.gemrc文件,在:sources:配置行下添加https://gems.ruby-china.com/,这样就配置了ruby-china的Gem源。最终的配置文件如下:

1
2
3
4
5
6
7
8
9
---
:backtrace: false
:bulk_threshold: 1000
:sources:
- https://gems.ruby-china.com
:update_sources: true
:verbose: true
:ssl_verify_mode: 0
concurrent_downloads: 8

mac系统升级到Catalina时,使用cocoa pod时报错:-bash: /usr/local/bin/pod: /
System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/bin/ruby: bad interpreter: No such file or directory”.

解决方案:sudo gem install -n /usr/local/bin cocoapods -v 1.4.0

Fastlane安装步骤

指定版本安装fastlane

1
2
3
$ set -ex
$ gem uninstall fastlane --all --executables
$ gem install fastlane --version 2.28.3 --no-document

mac系统升级到Catalina时,安装Fastlane会报错,请尝试:

1
2
3
$ set -ex
$ gem uninstall -n /usr/local/bin fastlane --all --executables
$ gem install -n /usr/local/bin fastlane --version 2.28.3 --no-document

如果尝试多次仍然无法安装Fastlane,可以尝试:

1
2
3
#brew cask安装fastlane的执行命令:
$ brew cask uninstall fastlane
$ brew cask install fastlane

Fastlane基本命令

初始化fastlane

1
$ fastlane init

给fastlane安装插件

1
2
$ fastlane add_plugin git_pull_branch
$ fastlane add_plugin fastlane-plugin-ipa_install_plist_generate

使用fastlane进行重签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
desc "重签名"
lane :ad_hoc_resign do

# 包的地址
ipa_path = "/Users/kris/Desktop/wantong/Payload.ipa"
signing_identity = "iPhone Distribution: XXXXX Asia Limited"
provision_profile = "/Users/kris/Desktop/wantong/embedded.mobileprovision"

result = resign(
ipa: ipa_path,
signing_identity: signing_identity,
provisioning_profile: provision_profile,
)

puts result

end

将homebrew的源换成阿里云,提高下载速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.替换brew.git:
$ cd "$(brew --repo)"
$ git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

# 2.替换homebrew-core.git:
$ cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
$ git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

# 3.替换homebrew-cask.git:
$ cd "$(brew --repo)"/Library/Taps/homebrew/homebrew-cask
$ git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-cask.git

# 4.替换homebrew-bottles:
$ echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
$ source ~/.zshrc

给Git添加代理访问,提高Git的访问速度

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.编辑PAC用户自定义规则,添加以下地址:
github.com
githubusercontent.com
githubassets.com

# 2.编辑~/.ssh/config文件,在文件中添加以下命令:
Host github.com
HostName github.com
User git
ProxyCommand nc -v -x 127.0.0.1:1086 %h %p

# 3.让配置立即生效:
$ source ~/.ssh/config

给终端添加代理访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1.编辑~/.bash_profile文件
$ vim ~/.bash_profile

# 2.在配置文件中加入如下内容,并保存
export http_proxy=http://127.0.0.1:1087
export https_proxy=http://127.0.0.1:1087

# 3.让配置立即生效
$ source ~/.bash_profile

# 4.测试代理是否成功
$ curl cip.cc
IP : 192.3.249.219
地址 : 美国 华盛顿州 西雅图
运营商 : colocrossing.com

数据二 : 美国

数据三 : 美国华盛顿西雅图

URL : http://www.cip.cc/192.3.249.219

解决真机连接异常的问题

1
2
# 杀掉USB服务
$ sudo killall -STOP -c usbd

参考链接

安装配置RVM环境
安装指定版本的cocoapods
手动安装RVM
解决Homebrew下载更新极慢的问题
Mac上的各种代理设置
Mac安装Ruby版本管理器RVM

VPS相关环境信息

Linux Centos 7.1503.01 X86 64 Minimal Gen2 V1

安装ShadowsocksR的命令

1
2
# wget -N --no-check-certificate https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/ssrmu.sh && chmod +x ssrmu.sh && bash 
# ./ssrmu.sh

安装BBR加速的命令(方法1)

1
2
3
4
# yum -y install wget
# wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh
# chmod +x bbr.sh
# ./bbr.sh

安装BBR加速的命令(方法2)

1
2
3
4
5
# wget -N --no-check-certificate "https://raw.githubusercontent.com/chiakge/Linux-NetSpeed/master/tcp.sh"

# chmod +x tcp.sh

# ./tcp.sh

操作方法:先安装内核,重启vps让内核生效,再启动对应的加速即可。数字1的BBR/BBR魔改内核对应数字4、5、6的BBR加速、BBR魔改加速和暴力BBR魔改版加速。数字2的BBRplus内核对应数字7的BBRplus加速。数字3的锐速加速内核对应数字8的锐速加速。

以安装暴力BBR魔改版加速为例,我们先安装对应的内核,输入数字1

内核安装完成后,输入y进行重启,重启才能让内核生效

重启完成后,输入5启动使用BBR魔改版加速

检测BBR是否已经安装好了

1
2
3
4
5
6
7
## 在终端输入如下命令,如果返回的输出有bbr,则说明已经安装完成
# sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic bbr

## 在终端输入如下命令,如果返回的输出有bbr,则说明已经安装完成
# lsmod | grep bbr
tcp_bbr 20480 7

首次安装完成如果无法连接成功,请尝试检测服务器防火墙是否没有新增规则,导致端口在外网无法访问。检测方法请使用站长工具>端口扫描
如果查询出在外网无法访问当前的服务器端口,请在服务器的防火墙中添加规则,开启特殊端口访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## 查看防火墙规则
# firewall-cmd --list-all

## 查询端口是否开放
# firewall-cmd --query-port=80/tcp

## 开放80端口
# firewall-cmd --permanent --add-port=80/tcp

## 移除端口
# firewall-cmd --permanent --remove-port=80/tcp

## 重启防火墙(修改配置后要重启防火墙)
# firewall-cmd --reload

IP和端口是否畅通测试

此工具可以检测全球范围内,到你的服务器的ping值大概是多少:站长工具>ping检测
通过站长工具检测国内到服务器的IP和端口是否畅通:站长工具>端口扫描
通过此网站可以检测在国外,你的服务器IP和端口是否畅通:国外站长工具>端口扫描

也可以使用命令检测远程服务器端口是否开启:

1
# nc -v host port

端口未打开返回状态为非0

ShadowsocksR客户端下载地址

https://github.com/shadowsocksr-backup/shadowsocks-rss

顺便附上Shadowsocks客户端的下载地址

注意 ShadowsocksR 和 Shadowsocks 的客户端是不一样的,无法互相连通。
https://lvii.gitbooks.io/outman/content/ss.mac.html

参考链接

SSR科学上网搭建脚本+virmach主机购买地址分享
国外VPS搭建SSR多用户教程【中文一键安装版】
CentOS6开启BBR加速
linux 检测远程端口是否打开
站长工具>ping检测
站长工具>端口扫描
国外站长工具>端口扫描
科学上网漫游指南(shadowsocks服务端和客户端搭建指南)
shadowsocksr-backup/shadowsocks-rss
Centos7.3防火墙配置
自建ss服务器教程
全球ping
在线tracert,查询IP所在地
免费域名
免费域名解析
自建V2Ray教程
V2Ray客户端下载地址

什么是VPS?

Virtual Private Server简称VPS。是在一台真实服务器上使用虚拟机技术虚拟成多个小主机,用VZ或VM在一台服务器上虚拟出多个类似独立服务器的部分,每个部分都可以做单独的操作系统,管理方法同服务器一样。通俗讲就是物理机上开出的虚拟机。

什么是虚拟主机?

Virtual hosts (Vhost)虚拟主机是通过物理服务器,VPS或者云服务器中划分出来的一个小空间,不管是独立服务器还是VPS或者云服务器都可以做为虚拟主机的母体。但一般不选择VPS作为虚拟主机的服务器。

VPS和虚拟主机有什么区别?

1.使用方式不同。VPS一般通过网站的控制台或者SSH远程登录操作,而虚拟主机只能通过FTP上传的方式或网站控制台来操作。因为使用方式的不同,用户只能在特定的虚拟空间里面操作,使用方式比较简单,但是也相对局限。
2.适用人群不同。VPS一般适用于网站的站长,搭建博客、网站、或CMS等。而VPS则适用于相对专业的人员,VPS可以自己安装操作系统、自己配置环境,操作起来比较灵活,也比较复杂。

VPS虚拟化的分类

目前市场上的虚拟化技术主要分为4类:

1.XEN

XEN的典型代表是早期的AWS和老牌VPS服务商Linode,不过这两家都迁移到了KVM。目前主流服务商已经全面放弃XEN。

2.KVM

目前的主流全虚拟化技术,全面替代了XEN。XEN算是Linux的一个应用,而KVM是Linux的一个模块。

3.Hyper-V

微软自家虚拟技术,Windows专享。

4.OpenVZ

OpenVZ本质上并不是虚拟化,而是容器。相对于XEN和KVM而言它的性能损失是最小的(几乎可以忽略不计),并且内存/CPU/硬盘伸缩不用重启(XEN和KVM是需要重启后才能正确配置的)。
而OpenVZ最大的缺点在于虚拟隔离化非常低,并且硬件层面权限较低甚至不能拥有自己的独立Linux内核。
同时OpenVZ最大的问题并不在于技术层面,而在于绝大部分服务商使用OpenVZ的目的是内存可以超售。超售是什么概念呢?简单说一台32G内存的物理机可以开出64台内存1G的VPS。

其他

除了上面说到的以外还有一些非主流的虚拟技术,比如VMWare这种商业解决方案。

VPS可以做哪些事情?

1.搭建Shadowsocks服务器,科学上网。
2.建立自己的博客站点,绑定免费的域名,使用免费的Cloudfare进行CDN加速,实现自己的博客站点。
3.建立下载服务器。
4.搭建自己的服务器,比如FTP服务器,Git服务器等等,自己托管自己的私有库代码。
5.注册美版Apple ID,下载美区的App。
6.搭建Google镜像代理网站,方便资料查选和Bug修复。

如何选购VPS?

1.支付方式,尽量支持支付宝或微信,国外很多VPS已经支持支付宝支付了,但是更多的是只支持信用卡或者Paypal,由于很多是信用卡直接续费的,所以尽量不要选择信用卡支付,或者设定好闹钟,提醒自己解绑。
2.是否支持退款,购买国外的VPS,要注意商家是否支持退款,很多商家都是支持的。因为如果分到的VPS的IP已经被国家防火墙收录,这个VPS基本就失去意义了,这时候如果有退款保障可以申请退款,重新购买。
3.是否支持换IP,由于国家防火墙会不断的收录新的IP,如果IP不幸被墙,若支持换IP,那就太好了。
4.是否支持月付,一般的VPS商家都是支持月付的。如果你无法肯定VPS的稳定性,可以先用月付,购买一个短期的服务。
5.购买之前请先搜一下优惠码,很多网站都有优惠码,很容易就找到。

使用心得

  1. 我使用过Vultr的VPS,网上查询Vultr的VPS是按小时收费的,只要你的机器存在一小时,就按1小时扣费,所以如果你不用的话,请立即删掉你的机器。并且Vultr需要充值,充值后才能使用,这个有点坑。我只记得我充了5美元,半个月就没了。可能操作失误,但是这个扣费确实很快,使用下来,费用高昂。
  2. 搬瓦工19.9美元一年的VPS刚停一个多月,因为无法续费了,搬瓦工发邮件给我,说19.9美元一年的VPS的架构是OpenVZ的,现在已经被淘汰了,多给我半个月的时间让我备份数据,然而我只想续费。等再去搬瓦工官网去看的时候,只剩49.9美元一年的VPS了,可见搬瓦工的价格也上涨了。
  3. 综合筛选发现,全球看下来,美国的服务器会相对便宜一些,香港的、台湾的、 欧洲的、韩国的、日本的都不便宜。

推荐服务器

Virmach – 廉价入门

低价VPS

50KVM – 靠谱国人商家

搬瓦工

经济实惠的个人VPS

下面列出的VPS非常便宜,而且性能稳定,但是,最好只将他们作为研究学习使用,而不要拿来建设网站。

VPS品牌 价格/年 CPU 内存 硬盘
RackNerd $22.99 1核 512M 30G
BuyVM $20.00 1核 512M 10G
Hostodo $19.99 1核 512M 8G
HostFlyte $12 1核 512M 10G
VirMach $12 共享 384M 10G

参考链接:

3分钟看懂vps、虚拟空间、服务器之间的区别
阿里云服务器与VPS和虚拟主机有什么区别?
如何选购VPS
适合新手入门的VPS机器
vultr收费方式是怎么样的?vultr是怎么计费的?vultr扣费模式解析
VPS选购指南及其他
xooof风云博客
VPS评测

什么是SVG?

SVG是一种用XML定义的语言,用来描述二维矢量及矢量/栅格图形。

SVG有哪些优势?

参与定义 SVG 的组织有:太阳微系统、Adobe、苹果公司、IBM 以及柯达。
与其他图像格式相比,使用 SVG 的优势在于:
●SVG 可被非常多的工具读取和修改(比如记事本)
●SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强。
●SVG 是可伸缩的
●SVG 图像可在任何的分辨率下被高质量地打印
●SVG 可在图像质量不下降的情况下被放大
●SVG 图像中的文本是可选的,同时也是可搜索的(很适合制作地图)
●SVG 可以与 JavaScript 技术一起运行
●SVG 是开放的标准
●SVG 文件是纯粹的 XML
●SVG 的主要竞争者是 Flash。

SVG在iOS中有哪些劣势?

●Xcode 6以后支持矢量图,原生支持pdf格式
●原生不支持SVG格式(因为svg本质是xml),如果要加载SVG格式,使用WebView或第三方框架
●原生中如果使用SVG格式的图片,不能放在Assets.xcassets中,要像本地文件或者网络文件一样给个地址,存放起来有点麻烦
●集成起来麻烦
●只支持放大缩小,颜色修改需要另外的分类来支持
●不支持带滤镜的SVG
●UIButton使用SVG时,需要设置Button的Type为Custom,否则改变颜色的时候,没有效果。

iOS使用第三方框架加载svg格式

iOS中使用 SVGKit(Objective-C库)和 Macaw(Swift库)。

iOS中使用PDF格式的图片

在Asset.xcassets中添加一张图片,选择Attributes inspector,scales设置成SingleScale,原来3个占位图的位置会变成1个占位图的位置。在这个占位图中拖入一个pdf格式的图片。iOS系统会根据PDF格式的图片自动生成@1x、@2x和@3x图,对应适配不同的屏幕。

Demo

参考链接:

百度百科SVG格式
Xcode项目中使用矢量图(pdf,svg)
矢量图在iOS中的应用细节
iOS开发APP瘦身之PDF图片资源加载框架

1.什么是vsftpd

vsftpd是一款在Linux发行版中最受推崇的FTP服务器程序。特点是小巧轻快,安全易用。
vsftpd 的名字代表”very secure FTP daemon”, 安全是它的开发者 Chris Evans 考虑的首要问题之一。在这个 FTP 服务器设计开发的最开始的时候,高安全性就是一个目标。

2.查看当前服务器是否安装过

1
$ rpm -qa|grep vsftp

如果显示版本号,说明安装成功,如果没有安装文件,什么都不显示,说明没有安装或者其他情况。也可以通过which vsftp检查是否有ftp的安装文件。

3.安装FTP

1
$ yum -y install vsftpd

安装完后,有/etc/vsftpd/vsftpd.conf 文件,是vsftp的配置文件。还新建了一个ftp用户和ftp的组,指向home目录为/var/ftp,默认是nologin(不能登录系统)。可以用cat /etc/passwd 命令查看用户。

4.启动ftp,执行命令

1
$ service vsftpd start

5.查看ftp的状态

1
$ service vsftpd status

6.安装ftp客户端组件(用来验证登录vsftpd)

1
# yum -y install ftp

执行命令尝试登录

1
# ftp localhost

输入用户名ftp,密码随便(因为默认是允许匿名的)
登录成功,就代表ftp服务可用了。
但是,外网是访问不了的,所以还要继续配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@localhost ~]# ftp localhost
Trying ::1...
ftp: connect to address ::1拒绝连接
Trying 127.0.0.1...
Connected to localhost (127.0.0.1).
220 (vsFTPd 2.2.2)
Name (localhost:root): ftp
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
227 Entering Passive Mode (127,0,0,1,177,184).
150 Here comes the directory listing.
drwxr-xr-x 2 0 0 4096 Mar 22 2017 pub
226 Directory send OK.
ftp>

7.添加一个ftp用户

1
# useradd ftpuser

此用户就是用来登录ftp服务器用的。
这样一个用户建完,可以用这个登录,记得用普通登录不要用匿名了。登录后默认的路径为 /home/ftpuser.

8.给ftp用户添加密码

1
# passwd ftpuser

输入两次密码后修改密码。

9.关闭匿名登录

1
# vim /etc/vsftpd/vsftpd.conf

设置anonymous_enable=NO。

10.重启ftp服务

1
# service vsftpd restart

11.开启passive模式

默认是开启的,但是要指定一个端口范围,打开vsftpd.conf文件,在后面加上

1
2
pasv_min_port=30000   
pasv_max_port=30999

表示端口范围为30000~30999,这个可以随意改。改完重启一下vsftpd。

12.修改ftp默认的端口号

常规下21端口容易遭到别人的扫描、带来了一定程度的不安全。所以,最好的就是把21端口修改掉。默认修改为6069。

12.1 修改vsftp的配置文件

1
$ vi /etc/vsftpd/vsftpd.conf

在原来的基础上加上:

1
2
3
4
5
6
7
8
9
10
11
listen_port=6069

pasv_enable=YES

pasv_min_port=30000

pasv_max_port=30999

pasv_promiscuous=YES

ftpd_banner=Welcome to Wander FTP service

12.2 修改/etc/services

1
$ vi /etc/services

修改成:

1
2
ftp             6069/tcp
ftp             6069/udp

12.3 修改防火墙规则

如果防火墙开启、并且做了端口限制、请添加相应的规则

1
2
3
4
5
$ vi /etc/sysconfig/iptables

-A INPUT -p tcp -m state --state NEW -m tcp --dport 30000:30999 -j ACCEPT
-A INPUT -p udp -m state --state NEW -m udp --dport 6069 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 6069 -j ACCEPT

12.4 重启服务

1
2
# service iptables restart
# service vsftpd restart

然后就可以通过客户端6069端口连接ftp(选择被动模式连接)。

13.配置 vsftpd 使用 SSL / TLS

13.1 检查 vsftpd 是否支持 ssl 模块

1
# ldd $(which vsftpd) | grep ssl

13.2 建立专门给vsftpd使用的凭证数据。CentOS有一个建立凭证的地方/etc/pki/tls/certs/。

1
# cd /etc/pki/tls/certs

生成密钥文件

1
# make vsftpd.pem

复制证书到vsftpd目录

1
2
# cp -a vsftpd.pem /etc/vsftpd/
# ll /etc/vsftpd/vsftpd.pem

证书配置解析:

1
2
3
4
5
6
7
Country Name (2 letter code) [XX]:CN                            国家名称(2个字母代码)[XX]:CN
State or Province Name (full name) []:CHINA 国家或省名(全称)[]:中国
Locality Name (eg, city) [Default City]:CHINA 地名(如城市)[默认城市]:中国
Organization Name (eg, company) [Default Company Ltd]:ORG 组织名称(如公司)[默认公司有限公司]:ORG
Organizational Unit Name (eg, section) []:ORG 组织单元名称(例如,节)[]:ORG
Common Name (eg, your name or your server's hostname) []:NAME 常用名称(例如,您的名称或服务器的主机名)[]:Name
Email Address []:EMAIL@163.com 电子邮件地址[]:EMAIL@163.com

13.3 修改 vsftpd.conf 配置文件

停止服务

1
# service vsftpd stop

在最后加上以下配置

1
2
3
4
5
6
7
8
ssl_enable=YES
allow_anon_ssl=YES
force_anon_data_ssl=YES
force_anon_logins_ssl=YES
force_local_data_ssl=YES
force_local_logins_ssl=YES
ssl_tlsv1=YES
rsa_cert_file=/etc/vsftpd/vsftpd.pem

配置解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
###SSL#####
# 是否启用SSL,默认值:NO
ssl_enable=YES
#
# 仅适用于 ssl_enable 活动。如果设置为YES,匿名用户将被允许使用安全的SSL连接
默认值:NO
allow_anon_ssl=YES
#
# 仅适用于 ssl_enable 激活。如果激活,所有匿名登录将被强制使用安全的SSL连接,以便在数据连接上发送和接收数据。默认值:NO
force_anon_data_ssl=YES
#
# 仅适用于 ssl_enable 激活。如果激活,所有匿名登录将被强制使用安全的SSL连接以发送密码。默认值:NO
force_anon_logins_ssl=YES
#
# 仅适用于 ssl_enable 激活。如果激活,所有非匿名登录将被强制使用安全的SSL连接,以便在数据连接上发送和接收数据。默认值:YES
force_local_data_ssl=YES
#
# 仅适用于 ssl_enable 激活。如果激活,所有非匿名登录将被强制使用安全的SSL连接以发送密码。默认值:YES
force_local_logins_ssl=YES
#
# 仅适用于 ssl_enable 激活。如果启用,此选项将允许TLS v1协议连接。TLS v1连接是首选。默认值:YES
ssl_tlsv1=YES
#
# 仅适用于 ssl_enable 激活。如果启用,此选项将允许SSL v2协议连接。TLS v1连接是首选。默认值:NO
#ssl_sslv2=NO
#
# 仅适用于 ssl_enable 激活。如果启用,此选项将允许SSL v3协议连接。TLS v1连接是首选。默认值:NO
#ssl_sslv3=NO
#
# 选项指定用于SSL加密连接的RSA证书的位置。默认值:/usr/share/ssl/certs/vsftpd.pem
rsa_cert_file=/etc/vsftpd/vsftpd.pem

13.4 最后保存,重启服务就OK了

启动服务

1
service vsftpd start

重启服务

1
service vsftpd restart

13.5 连接 Vsftpd

在连接的时候勾选信任证书就行。

参考链接:

Linux安装ftp组件(8步完成)
小白教程:linux下安装FTP的过程和使用的整体过程
Linux 搭建FTP服务器
centos启用ftp功能
修改ftp默认端口
”不安全的服务器,不支持 FTP over TLS“ 配置 vsftpd 使用 SSL / TLS

1.使用ssh命令登录Linux服务器

1
ssh root@104.224.156.100 -p 2022

2.安装Git

在CentOS上的安装命令:

1
2
$ yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-devel
$ yum install git

在Debian上的安装命令:

1
2
sudo apt update
sudo apt install git

3.接下来我们 创建一个git用户组和用户,用来运行git服务:

1
2
$ groupadd git
$ useradd git -g git

4.客户端生成ssh公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
192:git poet$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/poet/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/poet/.ssh/id_rsa.
Your public key has been saved in /Users/poet/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:Rky+UE83syyOMjaKYHeoGduGBaPqnU+rtrcyl7TMdL8 poet@192.168.0.101
The key's randomart image is:
+---[RSA 2048]----+
| o . + |
| = o o + |
| o . + o o |
|. o . o + . |
|oo + .= S . |
|o.X..= * |
|.=.o*.+ . |
|. o+oB. . |
| ..=O+. E. |
+----[SHA256]-----+
192:git poet$ cat /Users/poet/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVX+tOytAUJq3pKNvIFX2HETKvalq4rlqIFJ04KtKIDykzbct5FxK1jR92oIP+ylEsQGz6dJ3FqIsXSn200UIbGvdCvSX5uSFFu8psV4eX1J7DsA6gcLMXElTEHDr82U6MwFmOR6Vb8RkSwsNzRqv8uDdxrmayvbUnQL3VK+/VNcfTdPl2O4YyrYp/DBtFe8b61QvlmU6x4UaX40Sg/r9/JyFMSEDduse9bvVkqg3xkTpaZqwquY7384Ou2iIr4bzlb5mAjW222foM6YBuBhxlovu5kVhLx4GpPsCcxPBJe8lgiGyAGG4yvJ1CH7Rv658eIFqVUyVeOJGP871pLkR poet@192.168.0.101

5.创建证书登录,如果没有该文件就创建它:

1
2
3
4
5
$ cd /home/git/
$ mkdir .ssh
$ chmod 755 .ssh
$ touch .ssh/authorized_keys
$ chmod 644 .ssh/authorized_keys

6.将生成客户端公钥并复制到服务器上

收集所有需要登录的用户的公钥,公钥位于id_rsa.pub文件中,把我们的公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个。
下面我们复制id_rsa.pub里的公钥到服务器的authorized_keys文件中。
首先使用ssh连接远程的服务器,然后进入目录/home/git,然后执行如下命令:

1
[root@localhost git]# vim .ssh/authorized_keys

然后将上面生成的公钥复制到服务器的公钥文件中。复制万保存,查看公钥文件中的内容:

1
2
[root@localhost git]# cat .ssh/authorized_keys
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVX+tOytAUJq3pKNvIFX2HETKvalq4rlqIFJ04KtKIDykzbct5FxK1jR92oIP+ylEsQGz6dJ3FqIsXSn200UIbGvdCvSX5uSFFu8psV4eX1J7DsA6gcLMXElTEHDr82U6MwFmOR6Vb8RkSwsNzRqv8uDdxrmayvbUnQL3VK+/VNcfTdPl2O4YyrYp/DBtFe8b61QvlmU6x4UaX40Sg/r9/JyFMSEDduse9bvVkqg3xkTpaZqwquY7384Ou2iIr4bzlb5mAjW222foM6YBuBhxlovu5kVhLx4GpPsCcxPBJe8lgiGyAGG4yvJ1CH7Rv658eIFqVUyVeOJGP871pLkR poet@192.168.0.101

7.初始化Git仓库

首先我们选定一个目录作为Git仓库,假定是/home/gitrepo/helloword.git,在/home/gitrepo目录下输入命令:

1
2
3
4
5
6
7
$ cd /home
$ mkdir gitrepo
$ chown git:git gitrepo/
$ cd gitrepo

$ git init --bare helloword.git
Initialized empty Git repository in /home/gitrepo/helloword.git/

8.以上命令Git创建一个空仓库,服务器上的Git仓库通常都以.git结尾。然后,把仓库所属用户改为git:

1
$ chown -R git:git helloword.git

9.克隆仓库

1
2
3
4
$ git clone git@104.224.156.100:/home/gitrepo/helloword.git
Cloning into 'helloword'...
warning: You appear to have cloned an empty repository.
Checking connectivity... done.

104.224.156.100 为 Git 所在服务器 ip ,你需要将其修改为你自己的 Git 服务 ip。

但是我克隆的时候却报错了:

1
2
3
4
5
6
7
8
192:git poet$ git clone git@104.224.156.100:2022:/home/gitrepo/helloword.git
Cloning into 'helloword'...
ssh: connect to host 104.224.156.100 port 22: Connection refused
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
192:git poet$

10.克隆仓库的时候出了点小差错,因为ssh的端口号被我改了,而Git是依赖于ssh,所以Git的连接请求也会失败。第9步的命令应该换一下:

1
$ git clone ssh://git@104.224.156.100:2022/home/gitrepo/helloword.git

如果你添加到服务器的公钥有效果,则上述命令会自动把项目克隆下来,但是如果公钥无效或者生成公钥的时候设置了密码,则克隆的步骤中会提示输入密码。

这样我们的 Git 服务器安装就完成。

11.如果需要继续添加新的仓库,请到服务器的/home/gitrepo目录下输入命令:

1
2
$ git init --bare second.git
Initialized empty Git repository in /home/gitrepo/second.git/

以上命令Git创建一个空仓库,服务器上的Git仓库通常都以.git结尾。然后,把仓库所属用户改为git:

1
$ chown -R git:git second.git

然后你就可以在客户端使用git clone正常的克隆项目了。具体操作命令如下:

1
$ git clone ssh://git@104.224.156.100:2022/home/gitrepo/second.git

12.克隆了空项目,想在本地试下git是否好用,可以试试下面的命令。

创建Readme.txt文件

1
$ touch Readme.txt

查看当前项目的Git仓库的状态

1
$ git status -s

将Readme.txt文件添加到本地的Git仓库

1
$ git add Readme.txt

继续查看本地的Git仓库的状态

1
$ git status -s

将操作提交到本地的Git仓库

1
$ git commit -m "add Readme.txt file"

如果你没有设置本地Git的全局用户名和邮箱,有时候会报错,设置方法如下

1
2
$ git config  --global user.name 你的目标用户名
$ git config --global user.email 你的目标邮箱名

将操作推送到远程的Git仓库的master分支

1
$ git push origin master

在另外一个客户端拉取Git远程仓库master分支的内容

1
$ git pull

在本地按时间倒序查看提交的log记录

1
$ git log

13.禁用shell登录

网上流传在搭建服务器的过程中创建的git用户可以免密登录远程的ssh,试了一把,果不其然登录了。

1
2
3
4
5
192:~ poet$ ssh git@104.224.156.100 -p 2022
[git@localhost ~]$ ls
[git@localhost ~]$ ls -a
. .. .bash_logout .bash_profile .bashrc .ssh
[git@localhost ~]$

出于安全考虑,第三步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行:

1
2
3
[root@git ~]# cat /etc/passwd | grep git

git:x:1001:1001:git version control:/home/git:/bin/bash

改为:

1
2
3
[root@git ~]# vim /etc/passwd

git:x:1001:1001:git version control:/home/git:/usr/bin/git-shell

这样,git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。

修改完再试一把,发现已经连接不了远程ssh了:

1
2
3
4
5
6
7
8
9
192:~ poet$ ssh git@104.224.156.100 -p 2022
Last login: Fri Aug 23 07:56:10 2019 from 222.93.190.219
fatal: What do you think I am? A shell?
Connection to 104.224.156.100 closed.
192:~ poet$ ssh git@104.224.156.100 -p 2022
Last login: Fri Aug 23 07:56:39 2019 from 222.93.190.219
fatal: What do you think I am? A shell?
Connection to 104.224.156.100 closed.
192:~ poet$

14.Git报错收集

  • git clone does not appear to be a git repository
    错误示例:

    1
    2
    3
    4
    5
    6
    poetmacbook-pro:git kris$ git clone ssh://git@10.211.55.4:/home/gitrepo/IMBasic.git
    正克隆到 'IMBasic'...
    fatal: '/home/gitrepo/IMBasic.git' does not appear to be a git repository
    fatal: 无法读取远程仓库。

    请确认您有正确的访问权限并且仓库存在。

    问题产生的原因:git仓库的路径不对,请确认/home/gitrepo/IMBasic.git是否确实存在,还要注意此处应该使用服务器上的绝对路径,不能使用相对路径。

15.关联远程仓库

如果已经关联过远程仓库,可以先删除关联的远程Git仓库

1
$ git remote rm origin

再添加关联的远程仓库

1
$ git remote add origin git@github.com:xxx/xxx.git

或者直接一条命令:

1
$ git remote set-url origin git@github.com:xxx/xxx.git

16.显示remote信息

1
$ git remote show origin

参考链接:
Git 服务器搭建
Git 服务器搭建与客户端安装
git 从远程git服务上拉代码 git服务器非默认端口
Git常用命令总结及其用法说明
如何在Debian 10 Linux上安装Git

MAC OSX上安装MySQL有多种方法:

    1. 使用DMG文件安装,安装完在系统偏好设置中有相应的管理面板可以控制MySQL服务器开启和关闭,详情可访问官网下载地址
    1. 使用Homebrew安装,打开终端,输入命令brew install mysql ,这个安装过程中可能需要升级Homebrew,Homebrew的升级需要翻墙

安装完测试MySQL需要使用的基本命令如下:

1.使用brew安装完MySQL时,root密码默认为空,如果要设置MySQL的相关安全策略,使用命令: mysql_secure_installation

2.打开mysql,带后台线程(电脑重启后会自己开启):brew services start mysql

3.打开mysql,不带后台线程:mysql.server start

4.退出mysql:brew services stop mysql

5.登录MySQL:mysql -u root -p

6.查看MySQL的版本号:mysql> select Version();

7.查看当前登录的MySQL运行的端口号:mysql> show global variables like 'port';

8.显示可用的数据库:SHOW DATABASES;

9.选择数据库:mysql> USE crashcourse;

10.显示数据库中所有可用的表:mysql> SHOW TABLES;

11.显示表列: SHOW COLUMNS FROM customers;DESCRIBE customers;

12.显示服务器状态信息:SHOW STATUS;

13.显示用户的安全权限:SHOW GRANTS;

14.显示服务器错误信息:SHOW ERRORS;

15.显示服务器警告信息:SHOW WARNINGS;

Block底层解密

block想必做过一段iOS开发的同学都用过吧,但是大部分人都是仅仅会用,不怎么理解他是怎么实现的,今天就让我们来一步一步的分析一下底层是怎么实现的吧。

查看源码

1
2
3
void (^block)(void) =  ^(){
NSLog(@"this is a block!");
};

这样一个简单的block块大家都应该知道吧,但是这个block块是怎么实现的呢?

想要了解OC对象主要是基于C/C++的什么数据结构实现的,我们首先要做的就是将Object-C代码转化为C/C++代码,这样我们才能清楚的看清是怎么实现的

然后我们打开终端,在命令行找到cd到文件目录,然后中输入:

1
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc main.m

执行结束以后,会生成main.cpp文件,我们打开main.cpp文件,拉到最下边就是我们的main函数实现的。

我们得到c++代码的block实现

1
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

我们知道(void *)这种类型的都是类型的强制转换,为了更好的识别我们的这个Block代码,我们把类型转化去掉

1
2
void (*block)(void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA));

我们在分别查询__main_block_impl_0,__main_block_func_0,__main_block_desc_0_DATA代表什么意思

__main_block_impl_0

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 构造函数(类似于OC的init方法),返回结构体对象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

我们查看一下__block_impl里面是什么

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

__main_block_func_0

1
2
3
4
// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
}

__main_block_desc_0_DATA

1
2
3
4
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;//内存大小描述
} __main_block_desc_0_DATA

所以我们可以总结

  • 1、__main_block_impl_0__block_impl存放的是一些变量信息,其中存在isa,所以可以判断block的本质其实就是OC对象
  • 2、初始化
    1
    2
    3
    4
    5
    6
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
    我们在来查看Block方法
    1
    2
    void (*block)(void) = &__main_block_impl_0(__main_block_func_0,
    &__main_block_desc_0_DATA));
    对应上面的初始化我们可以看出第一个参数传递的是执行方法,第二个参数为描述信息

Block底层结构图

成员变量的捕获

为了保证block内部能够正常的访问外部变量,block有个变量捕获机制,这里我们先说结果,然后在进行证明

我们在main函数写下这些代码,然后在把main函数生成c++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import <Foundation/Foundation.h>
int height = 180;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int weight = 65;
void (^block)(void) = ^(){
NSLog(@"age---------%d",age);
NSLog(@"weight---------%d",weight);
NSLog(@"height---------%d",height);
};
block();
}
return 0;
}

我们直接找到c++代码里面存放变量的结构体__main_block_impl_0

1
2
3
4
5
6
7
8
9
10
11
12
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *weight;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

我们可以看到变量捕获为age,*weight,但是没有捕获到全局变量height。为了方便的理解,我们先来了解一些内存空间的分配。

  • 1、栈区(stack) 由编译器自动分配并释放,存放函数的参数值,局部变量等。栈空间分静态分配 和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。
  • 2、堆区(heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。
  • 3、全局区(静态区) (static) 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放。
  • 4、程序代码区 存放函数的二进制代码

总结:

  • 1、因为自动变量(auto)分配的内存空间在栈区(stack),编译器会自动帮我们释放,如果我们把block写在另外一个方法中调用,自动变量age就会被释放,block在使用的时候就已经被释放了,所以需要重新copy一下
  • 2、静态变量在程序结束后有系统释放,所以不需要担心被释放,block只需要知道他的内存地址就行
  • 3、对于全局变量,任何时候都可以直接访问,所以根本就不需要捕获

Block类型

block有3种类型,可以通过调用class方法或者isa指针查看具体的类型,但是最终都是继承者NSBlock类型

  • 1、__NSGlobalBlock__,没有访问auto变量
  • 2、__NSStackBlock__,访问了auto变量
  • 3、__NSMallocBlock__,__NSStackBlock__调用了copy方法

她们的内存分配:

每一种类型的Block调用copy后的结果

  • 1、__NSStackBlock__原来在栈区,copy以后从栈复制到堆
  • 2、__NSGlobalBlock__原来在程序的数据段,copy以后什么也不做
  • 3、__NSMallocBlock__原来在堆区,复制以后引用计数加1
    我们来写一小段代码证明一下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void (^block1)(void) =  ^(){
    NSLog(@"block1");
    };
    int age = 10;
    void (^block2)(void) = ^(){
    NSLog(@"block2");
    NSLog(@"age---------%d",age);
    };
    void (^block3)(void) = [ ^(){
    NSLog(@"block3");
    NSLog(@"age---------%d",age);
    } copy];

    NSLog(@"block1:%@---->block2:%@----->block3:%@",[block1 class],[block2 class],[block3 class]);
    打印结果为:

为什么block2打印类型为__NSMallocBlock__,而不是__NSStackBlock__,因为ARC环境导致了,ARC会自动帮我们copy了一下__NSStackBlock__

auto变量修饰符__weak

在开始之前,我先说一下结论,然后我们在去印证

  • 1、当Block内部访问了auto变量时,如果block是在栈上,将不会对auto变量产生强引用。
  • 2、如果block被拷贝到堆上,会根据auto变量的修饰符(__strong,__weak,__unsafe_unretained),对auto变量进行强引用或者弱引用。
  • 3、如果block从堆上移除的时候,会调用block内部的dispose函数,该函数自动释放auto变量。
  • 4、在多个block相互嵌套的时候,auto属性的释放取决于最后的那个强引用什么时候释放。

下面我们把ARC环境变成MRC环境,同时稍微修改一下代码,我们在看看dealloc什么时候打印
选择项目 Target -> Build Sttings -> All -> 搜索‘automatic’ -> 把 Objective-C Automatic Reference Counting 设置为 NO

我们写一个Person类,在MRC环境,重写dealloc方法

1
2
3
4
- (void)dealloc{
[super dealloc];
NSLog(@"person--->dealloc");
}

我们在main函数里面写下这个方法

1
2
3
4
5
{
Person *p = [[Person alloc] init];
[p release];
}
NSLog(@"--------");

我们肯定都知道打印结果吧:先打印person--->dealloc,然后打印--------

如果我们添加一个Block呢,

1
2
3
4
5
6
7
8
9
10
11
typedef void (^Block)(void);
Block block;
{
Person *p = [[Person alloc] init];
block = ^{
NSLog(@"%@",p);
};
[p release];
}
block();
NSLog(@"--------");

打印结果为:

在ARC环境下,代码稍微的改变一下

1
2
3
4
5
6
7
8
9
Block block;
{
Person *p = [[Person alloc] init];
block = ^{
NSLog(@"%@",p);
};
}
block();
NSLog(@"--------");

打印结果为:

注意打印顺序:

  • MRC环境下,是先打印dealloc,然后在打印p
  • ARC环境下,是先打印p,然后在打印dealloc

当Block内部访问了auto变量时,如果block是在栈上,将不会对auto变量产生强引用,因为当Block在栈上的时候,他自己都不能保证自己什么时候被释放,所以block也就不会对自动变量进行强引用了

在ARC环境下如果我们对自动变量进行一些修饰符,那么block对auto变量是进行怎么引用呢
我们还是老方法,把main文件转化为c++文件,我们找到__main_block_func_0执行函数,

  • 当不用修饰符修饰的时:Person *p = __cself->p; // bound by copy
  • 当使用__strong修饰时:Person *strongP = __cself->strongP; // bound by copy
  • 当使用__weak修饰的时:Person *__weak weakP = __cself->weakP; // bound by copy
    我们运行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m出错了,我们需要支持ARC,指定运行时系统版本,xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

Block会自动copy自动变量的修饰属性

__Block修饰

我们都知道想要修改Block外边的变量,我们都会用__Block来修饰自动变量,但是为什么使用__Block修饰就可以在Block内部来更改自动变量了呢。

我们先写一小段代码

1
2
3
4
5
6
7
8
__block int age = 10;
NSLog(@"block前age地址1:%p",&age);
Block block = ^{
age = 20;
NSLog(@"block内%d-->age地址2:%p",age,&age);
};
block();
NSLog(@"block后%d-->age地址3:%p",age,&age);

打印结果为:

1
根据内存地址变化可见,__block所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。

我们把main函数转化为C++代码,然后在age使用__Block前后,对Block结构体进行分析

__Block所起到的作用就是只要观察到该变量被 block 所持有之后,age其实变成了OC对象,里面含有isa指针

__Block的内存管理原则

  • 1、当Block在栈上的时候,并不会对__Block变量进行强引用
  • 2、当block被copy到堆时,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)
  • 3、当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)

我们先看到了结果,这里我们在来分析一下源码
__block int age = 10;转化为C++代码,会变成这样

1
2
3
4
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

//为了便于观察,我们可以将强制转化去掉
__Block_byref_age_0 age = {0, &age, 0, sizeof(__Block_byref_age_0), 10};

唯一我们不太清楚的就是__Block_byref_age_0了,我们查找一下发现

1
2
3
4
5
6
7
8
typedef void (*Block)(void);
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

然后我们在来查找Block实现代码:

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString*)&__NSConstantStringImpl__var_folders_nb_9qtf99yd2qlbx2m97hdjf2yr0000gn_T_main_1757f5_mi_0,(age->__forwarding->age));
}

我们来查看一下age是怎么变成20的(age->__forwarding->age) = 20;,先是找到__forwarding结构体,然后在找到结构体里面的age

总结

  • 1、在ARC环境下,Block被引用的时候,会被Copy一次,由栈区copy到了堆。
  • 2、在Block被copy的时候,Block内部被引用的变量也同样被copy一份到了堆上面。
  • 3、被__Block修饰的变量,在被Block引用的时候,会变成结构体也就是OC对象,里面的__forwarding也会由栈copy到堆上面。
  • 4、栈上__block变量结构体中__forwarding的指针指向堆上面__block变量结构体,堆上__block变量结构体中__forwarding指针指向自己。
  • 5、当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)。

解决循环引用

我们看了那么长时间的源码了,一定还记得在auto变量为OC对象的时候,在没有修饰符修饰的时候Block内部会强引用OC对象,而对象如果也持有Block的时候就会造成相互引用,也就是循环引用的问题。

我们也只能在Block持有OC对象的时候,给OC对象添加弱引用修饰符才比较合适,有两个弱引用修饰符__weak__unsafe_unretained

  • 1、 __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil
  • 2、__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变

其实还有一种解决方法,那就是使用__Block,需要在Block内部把OC对象设置为nil

1
2
3
4
5
__block id weakSelf = self;
self.block = ^{
weakSelf = nil;
}
self.block();

使用__Block解决必须调用Block

Load和Initialize实现原理

+Load实现原理

+load方法会在runtime加载分类时调用

每个类、分类的+load,在程序运行过程中只调用一次

+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用

调用顺序

  • 1、先调用类的+load
    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的+load之前会先调用父类的+load
  • 2、再调用分类的+load
    • 按照编译先后顺序调用(先编译,先调用)

objc4源码解读过程
objc-os.mm 文件

  • _objc_init
  • load_images
  • prepare_load_methods
    • schedule_class_load
    • add_class_to_loadable_list
    • add_category_to_loadable_list
  • call_load_methods
    • call_class_loads
    • call_category_loads

_objc_init方法是RunTime运行的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

小知识:images是镜像的意思

我们在_objc_init方法中找到load_imagesload_images是Load加载镜像的意思,所有我们可以猜测这个里面应该有我们load的加载方法的相关实现

我们点击进入load_images方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;

recursive_mutex_locker_t lock(loadMethodLock);

// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}

// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}

里面有两个需要我们注意的

  • 1、prepare_load_methods((const headerType *)mh)准备加载Load方法,我们也可以看到上面的官方文档解释也是这个意思
  • 2、call_load_methods() 加载load方法

我们点击进入prepare_load_methods((const headerType *)mh)准备加载Load方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;

runtimeLock.assertWriting();

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];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}

我们可以看到执行顺序

  • 1、schedule_class_load(remapClass(classlist[i]));,这个是把类中的Load方法添加到数组中
  • 2、add_category_to_loadable_list(cat);这个是把分类中的load方法添加到数组中

查看类的load方法

我们查看schedule_class_load(remapClass(classlist[i]));方法里面还有哪些实现

1
2
3
4
5
6
7
8
9
10
11
12
13
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize

if (cls->data()->flags & RW_LOADED) return;

// Ensure superclass-first ordering
schedule_class_load(cls->superclass);

add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
  • 1、schedule_class_load(cls->superclass); 把父类load先添加到数组中
  • 2、add_class_to_loadable_list(cls);把自己的load方法添加到数组中

走到这里我们大概是清楚了类中load方法的加载添加过程,就是先把父类添加到数组中,然后再把自己添加到数组中

查看分类的load方法

我们点击add_category_to_loadable_list(cat)进入查看方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void add_category_to_loadable_list(Category cat)
{
IMP method;

loadMethodLock.assertLocked();

method = _category_getLoadMethod(cat);

// Don't bother if cat has no +load method
if (!method) return;

if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}

if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}

loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}

loadable_categories_used++;分类没有什么特殊的方法,应该就是按照编译顺序添加到数组的。

实现

我们刚才看到了类分类中的添加顺序,我们在来看看加载顺序
点击call_load_methods();进入相关实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

上面直接有官方文档给我们的顺序

  • 1、call_class_loads();加载类中load方法
  • 2、more_categories = call_category_loads()加载分类中load方法

Demo
我们这里来一个测试demo

  • 父类Person
    • 1、分类Person+Run.h
    • 2、分类Person+Eat
  • 2、子类
    • 1、Student
    • 2、Teacher

编译顺序:

打印顺序:

1
2
3
4
5
6
2020-06-27 16:44:07.407021+0800 分类2[4819:145771] +[Person load]
2020-06-27 16:44:07.407358+0800 分类2[4819:145771] +[Teacher load]
2020-06-27 16:44:07.407394+0800 分类2[4819:145771] +[Student load]
2020-06-27 16:44:07.407417+0800 分类2[4819:145771] +[Person(Eat) load]
2020-06-27 16:44:07.407453+0800 分类2[4819:145771] +[Person(Run) load]
Program ended with exit code: 0
  • 1、先编译Teacher但是最先打印Person
  • 2、分类Person+Run在子类Student之前编译,但是打印确实先打印Student

所有上面总结是十分准确的

  • 1、先调用类的+load
    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的+load之前会先调用父类的+load
  • 2、再调用分类的+load
    • 按照编译先后顺序调用(先编译,先调用)

我们是否注意到了另一个问题,为什么在有分类的时候还加载类的load方法,不应该是分类覆盖类吗?

我们在查看load的源码实现的时候发现,+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用,如果使用objc_msgSend会出现分类覆盖类,但是load直接是根据指针找方法的,所以不会覆盖。

Initialize实现原理

+initialize方法会在类第一次接收到消息时调用

调用顺序

  • 先调用父类的+initialize,再调用子类的+initialize(先初始化父类,再初始化子类,每个类只会初始化1次)
  • +initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点
    • 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
    • 如果分类实现了+initialize,就覆盖类本身的+initialize调用

objc4源码解读过程

objc-runtime-new.mm

  • class_getInstanceMethod
  • lookUpImpOrNil
  • lookUpImpOrForward
  • _class_initialize
  • callInitialize
  • objc_msgSend(cls, SEL_initialize)

我们在objc-runtime-new.mm文件中找到class_getInstanceMethod,里面就有一个主要实现方法lookUpImpOrNil

1
2
3
4
5
6
7
8
9
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
#warning fixme build and search caches
lookUpImpOrNil(cls, sel, nil,
NO/*initialize*/, NO/*cache*/, YES/*resolver*/);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}

里面没有什么实现我们继续点击lookUpImpOrNil进入实现

1
2
3
4
5
6
7
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}

里面好像还是没有我们想要的具体实现,继续点击lookUpImpOrForward查看实现

1
2
3
4
5
if (initialize  &&  !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}

这个里面有一个if判断里面有一些东西,就是在没有实现isInitialized的时候,调用_class_initialize方法,我们点击进入查看相关实现

1
2
3
if (supercls  &&  !supercls->isInitialized()) {
_class_initialize(supercls);
}
1
callInitialize(cls);

里面有这两个主要的函数

  • 1、第一个是判断是否存在父类,以及父类是否实现initialize方法,如果没有实现就去实现
  • 2、去实现自己的initialize方法。

我们在点击callInitialize发现具体是通过objc_msgSend来实现的。

1
2
3
4
5
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}

Demo
测试案例1
我们创建父类Person,然后创建子类Student&Teacher,子类不实现initialize方法,父类实现该方法

1
2
[Teacher alloc];
[Student alloc];
1
2
3
4
2020-06-27 16:51:34.924111+0800 分类2[4965:150760] +[Person initialize]
2020-06-27 16:51:34.924518+0800 分类2[4965:150760] +[Person initialize]
2020-06-27 16:51:34.924562+0800 分类2[4965:150760] +[Person initialize]
Program ended with exit code: 0

结果打印三次[Person initialize]方法,打印一次我们是能够想到了,因为实现过程是先看看父类有没有已经实现,如果没有实现就先实现父类的。但是另外两次是怎么来的呢。

[Student alloc]的实现大概是这样的

1
2
objc_msgSend([Person class], @selector(initialize));
objc_msgSend([Student class], @selector(initialize));
  • 1、第一步就是实现父类的initialize方法
  • 2、第二步,Student先查找自己元类有没有initialize方法,如果自己元类没有实现,就向上查找父类元类有没有initialize方法,如果有就执行,没有继续向上查找

测试案例2

我们创建父类Person,然后创建分类Person+Eat,都是实现initialize方法

1
[Person alloc];
1
2
2020-06-27 16:53:07.870498+0800 分类2[4993:151954] +[Person(Eat) initialize]
Program ended with exit code: 0

这句代码就是证明了如果分类实现了+initialize,就覆盖类本身的+initialize调用

总结

load、initialize方法的区别什么?

  • 1.调用方式
  • 1> load是根据函数地址直接调用
  • 2> initialize是通过objc_msgSend调用

-2.调用时刻
- 1> load是runtime加载类、分类的时候调用(只会调用1次)
- 2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

load、initialize的调用顺序?

1.load

  • 1> 先调用类的load

    • a) 先编译的类,优先调用load
    • b) 调用子类的load之前,会先调用父类的load
  • 2> 再调用分类的load

    • a) 先编译的分类,优先调用load

2.initialize

  • 1> 先初始化父类
  • 2> 再初始化子类(可能最终调用的是父类的initialize方法)