15行Python代码,帮你理解令牌桶算法

在网络中传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送,令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。

什么是令牌

从名字上看令牌桶,大概就是一个装有令牌的桶吧,那么什么是令牌呢?

紫薇格格拿的令箭,可以发号施令,令行禁止。在计算机的世界中,令牌也有令行禁止的意思,有令牌,则相当于得到了进行操作的授权,没有令牌,就什么都不能做。

用令牌实现限速器

我们用1块令牌来代表发送1字节数据的资格,假设我们源源不断的发放令牌给程序,程序就有资格源源不断的发送数据,当我们不发放令牌给程序,程序就相当于被限流,无法发送数据了。接下来我们说说限速器,所谓限速器,就是让程序在单位时间内,最多只能发送一定大小的数据。假设在1秒发放10块令牌,那么程序发送数据的速度就会被限制在10bytes/s。如果1秒内有大于10bytes的数据需要发送,就会因为没有令牌而被丢弃。

改进限速器——加个桶

我们实现的限速器,速度是恒定的,但是在实际的应用中,往往会有突发的传输需求(需要更快速的发送,但是不会持续太久,也不会引起网络拥塞),这种数据碰上我们的限速器,就因为拿不到令牌而无法发送。

对限速器进行一下改动,依然1秒产生10块令牌,但是我们把产生出来的令牌先放到一个桶里,当程序需要发送的时候,从桶里取令牌,不需要的时候,令牌就会在桶里沉淀下来,假设桶里沉淀了10块令牌,程序最多就可以在1秒内发送20bytes的数据,满足了突发数据传输的要求,并且由于桶里的令牌被用完,下一秒最多依然只能发10bytes的数据,不会因为持续发送大量数据,对网络造成压力。

15行Python代码实践令牌桶

令牌桶需要以一定的速度生成令牌放入桶中,当程序要发送数据时,再从桶中取出令牌。这里似乎有点问题,如果我们使用一个死循环,来不停地发放令牌,程序就被阻塞住了,有没有更好的办法?

我们可以在取令牌的时候,用现在的时间减去上次取令牌的时间,乘以令牌的发放速度,计算出桶里可以取的令牌数量(当然不能超过桶的大小),从而避免循环发放的逻辑。

接下来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time


class TokenBucket(object):

# rate是令牌发放速度,capacity是桶的大小
def __init__(self, rate, capacity):
self._rate = rate
self._capacity = capacity
self._current_amount = 0
self._last_consume_time = int(time.time())

# token_amount是发送数据需要的令牌数
def consume(self, token_amount):
increment = (int(time.time()) - self._last_consume_time) * self._rate # 计算从上次发送到这次发送,新发放的令牌数量
self._current_amount = min(
increment + self._current_amount, self._capacity) # 令牌数量不能超过桶的容量
if token_amount > self._current_amount: # 如果没有足够的令牌,则不能发送数据
return False
self._last_consume_time = int(time.time())
self._current_amount -= token_amount
return True

扫码关注Python私房菜

最爱你的人,会让你不费脑细胞的理解区块链原理

区块链是一个近期非常火的概念,随便走进一个写字楼的电梯,都会听到有人谈论区块链,或者炒币: ) 希望通过这篇文章,能让你对区块链的概念有一个整体的认识,在理解概念后,下一篇文章将用大约300行Python代码,实现一个区块链网络。

区块 和 链

所谓区块,就是一个块咯(要不然还是什么?),把这些块一个一个连在一起,像链条一样,就称为区块链(别急着打我,先往下看)。

区块和区块链

这是一些链条,链条的价值更多的体现在锁楼下小电动车的时候,不过我们再仔细看一下这个链条是不是有点像那什么?⛓

脱氧核糖核酸

很聪明,高中生物老师在冲你微笑!这就是传说中的脱氧核糖核酸(DNA),DNA也是一种链式结构,携带了遗传信息。区块链中的“区块”,就好比DNA分子中的脱氧核糖核苷酸(我也不知道自己在说什么),而区块链中的“链”,就好比DNA分子的链式结构。

DNA和链条的价值差异,关键在于DNA携带了大量遗传信息,而链条什么都没有携带。对于区块链来说,携带信息也是它的一个重要特点(不携带信息连锁电动车的作用都没有)。

当区块链携带的是账务信息的时候,它就变成了一个特别厉害的东西——账本(我们给他起了个00后的名字——比特币)。

Peer-to-Peer

为了显得专业一些,我们祭出比特币的创造者,中本聪的论文《Bitcoin: A Peer-to-Peer Electronic Cash System》,从这篇论文的题目我们可以看到,区块链是基于Peer-to-Peer的,下面就来了解一下什么是Peer-to-Peer。

Peer-to-Peer,简称P2P(不是互联网借贷的那个P2P),它是一种点对点网络,看图(我也不知道该怎么讲)。

P2P网络

图中每一个方脑袋就是一个Peer(节点),注意一下,这些方脑袋有一个共同点,他们都一毛一样(不仅长得一样,每一个脑袋都跟其它脑袋相连)。这也是P2P网络的最大特点——去中心化,P2P网络中不存在中心节点,所有节点都是平等的,任何一个节点,想跟谁说话就跟谁说话,并且谁(节点)都不能说了算。

共识机制

你和好基友老王,还有你们共同喜欢的一个姑娘阿圆(对,就是圆滚滚的圆),组成了一个三节点的P2P网络,根据P2P网络定义你们三个人谁都不能说了算。这一天,阿圆生日,你和老王同时给阿圆送了一个大蛋糕,那么问题来了,阿圆先吃哪个呢?

既然没有人能说了算,也不能让你和老王决斗(计算机这么做恐怕人类会毁灭),那么就需要采用一个文明的办法决定——商量。既然商量,就要有规矩,这个规矩叫共识机制

Proof-of-work

区块链共识机制有很多种,Proof-of-work(POW,工作量证明)是其中一种,所谓工作量证明,有点像是比武招亲。你和老王对阿圆都很好,阿圆也很难抉择到底先吃谁的蛋糕,于是她请了两位武力相当的武林高手,让你和老王分别与高手过招,谁赢了高手,就先吃谁的蛋糕,如果你们都赢了高手,那么谁先赢算数。

把这些捏在一起

了解了POW、共识机制、P2P、区块、链的概念,我们就可以把他们拼在一起,看看会发生什么了。

区块链诞生了!

我们把P2P网络中的每一个节点,赋予一条链,这样网络中所有的链都是平等的了,接下来在其中一条链上增加一个带有信息的区块,P2P网络会将这个区块同步到所有的链上,也就是这条信息会被存储在所有节点。

把上面的一段话缩成一句(会显得比较厉害):

区块链是用分布式数据库识别、传播和记载信息的智能化对等网络, 也称为价值互联网。

看到这里是不是对区块链略知一二了?

关注Python私房菜

下一篇文章会用300行Python代码实现一个区块链,不要错过哟

面试不再怕,20行Python代码帮你搞懂LRU算法

LRU算法在后端工程师面试中,是一个比较常出现的题目,这篇文章带大家一起,理解LRU算法,并最终用Python轻松实现一个基于LRU算法的缓存。

缓存是什么

先看一张图,当我们访问网页,浏览器会给服务器发请求,服务器会经过一系列的运算,把页面返回给浏览器。

当有多个浏览器同时访问的时候,就会在短时间内发起多个请求,而服务器对每一个请求都要进行一系列相同的操作。重复工作不仅浪费资源,还可能导致响应速度变慢。

而缓存则可以把服务器返回的页面保存下来,当有其他的浏览器再访问时候,就不必劳服务器大驾,直接由缓存返回页面。为了保证响应速度,缓存通常是基于比较昂贵的硬件,比如RAM,这就决定了我们很难用大量的缓存把所有的页面都存下来,当恰好没有缓存浏览器请求的页面时,依然需要请求服务器。由于缓存容量有限,而数据量无限(互联网每天新产生的页面数无法估计),就需要把好刚用在刀刃上,缓存那些最有用的信息。

LRU是什么

LRU是一种缓存淘汰算法(在OS中也叫内存换页算法),由于缓存空间是有限的,所以要淘汰缓存中不常用的数据,留下常用的数据,达到缓存效率的最大化。LRU就是这样一种决定“淘汰谁留下谁”的算法,LRU是Least recently used的缩写,从字面意思“最近最少使用”,我们就可以理解LRU的淘汰规则。

LRU的淘汰逻辑

我们用一张图来描述LRU的淘汰逻辑,图中的缓存是一个列表结构,上面是头结点下面是尾节点,缓存容量为8(8个小格子):

  • 有新数据(意味着数据之前没有被缓存过)时,加入到列表头
  • 缓存到达最大容量时,需要淘汰数据多出来的数据,此时淘汰列表尾部的数据
  • 当缓存中有数据被命中,则将数据移动到列表头部(相当于新加入缓存)

按上面的逻辑我们可以看到,一个数据如果经常被访问就会不断地被移动到列表头部,不会被淘汰出缓存,而越不经常访问的数据,越容易被挤出缓存。

20行Python代码实践LRU

接下来我们用Python来实现一个采用LRU算法的缓存。

从前面的文章中我们可以知道,缓存简化下来就两个功能,一个是往里装数据(缓存数据),一个是往外吐数据(命中缓存),所以我们的缓存对外只需要put和get两个接口就可以了。

按照前面的示意图,缓存内部我们只需要有一个列表(list)就可以实现LRU逻辑,不过用列表虽然能实现逻辑,但是在判断是否命中缓存时,速度可能非常慢(列表需要遍历才能知道数据有没有在里面)。在Python中,我们可以用基于hash的结构,比如字典(dict)或集合(set),来快速判断数据是否存在,解决列表实现的性能问题。但是字典和集合又是没有顺序的,如果能有一种既能排序,又是基于hash存储的数据结构,就好了。

在Python的collections包中,已经内置了这种实用的结构OrderedDict,OrderedDict是dict的子类,但是存储在内部的元素是有序的(列表的特点)。

解决了数据结构的问题,我们可以直接上手写逻辑了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LRUCache:

def __init__(self, capacity):
self.capacity = capacity
self.queue = collections.OrderedDict()

def get(self, key):
if key not in self.queue:
return -1 // 要找的数据不在缓存中返回-1
value = self.queue.pop(key) // 将命中缓存的数据移除
self.queue[key] = value // 将命中缓存的数据重新添加到头部
return self.queue[key]


def put(self, key, value):
if key in self.queue: // 如果已经在缓存中,则先移除老的数据
self.queue.pop(key)
elif len(self.queue.items()) == self.capacity:
self.queue.popitem(last=False) // 如果不在缓存中并且到达最大容量,则把最后的数据淘汰
self.queue[key] = value // 将新数据添加到头部

下次面试在遇到LRU的题目,是不是就胸有成竹了?

扫码关注Python私房菜

60行Python代码,实现多线程PDF转Word

工作中经常会遇到需要提取PDF文件中文字的情况,一个PDF还好,复制粘贴一下也花不了太多时间,如果需要把大量PDF转为Word,怎么办呢?

今天教大家用60行代码实现,多线程批量PDF转Word。没兴趣看具体过程可以直接拉到最后,有代码。

分解任务

把PDF转为Word,分几步?两步,第一步读取PDF文件,第二步写入Word文件。

是的,就是这么简单,借助Python第三方包,可以轻松实现上面两个过程,我们要用到pdfminer3k和python-docx这两个包。

读取PDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams


resource_manager = PDFResourceManager()
return_str = StringIO()
lap_params = LAParams()

device = TextConverter(resource_manager, return_str, laparams=lap_params)
process_pdf(resource_manager, device, file) // file是使用open方法打开的PDF文件句柄
device.close()

// 此处content就是转换为文字的PDF内容
content = return_str.getvalue()

content变量存储的就是我们从PDF文件中读取出的文字内容,可以看到,使用pdfminer3k可以轻松完成这个任务。接下来我们需要把文字内容写入成一个word文件。

写入Word

1
2
3
4
5
6
7
8
9
10
11
12
from docx import Document


doc = Document()
for line in content.split('\n'):
paragraph = doc.add_paragraph()
paragraph.add_run(remove_control_characters(line))
doc.save(file_path)
content是我们前面读取出的文字内容,由于是讲整个PDF读成一个字符串,所以需要使用split方法将每一行分隔开,然后按行写入word,否则所有的文字会在同一行。同时这段代码使用了一个remove_control_characters函数,这个函数是需要自己实现的,目的是移除控制字符(换行符、制表符、转义符等),因为python-docx是不支持控制字符写入的。
def remove_control_characters(content):
mpa = dict.fromkeys(range(32))
return content.translate(mpa)

控制字符就是ASCII码在32以下的,所以我们使用str的translate方法,把32以下的字符移除就可以。

用是能用,但是太慢了!

如果我们用上面代码去转换100个PDF文件,就会发现速度慢到难以接受,每个PDF都需要花很长时间才能转换好,怎么办?别急,接下来我们引入多线程,同时转换多个PDF,可以有效加快转换速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
from concurrent.futures import ProcessPoolExecutor


with ProcessPoolExecutor(max_workers=int(config['max_worker'])) as executor:
for file in os.listdir(config['pdf_folder']):
extension_name = os.path.splitext(file)[1]
if extension_name != '.pdf':
continue
file_name = os.path.splitext(file)[0]
pdf_file = config['pdf_folder'] + '/' + file
word_file = config['word_folder'] + '/' + file_name + '.docx'
print('正在处理: ', file)
result = executor.submit(pdf_to_word, pdf_file, word_file)
tasks.append(result)
while True:
exit_flag = True
for task in tasks:
if not task.done():
exit_flag = False
if exit_flag:
print('完成')
exit(0)

代码中config是包含存储PDF文件夹地址和word文件夹地址的字典,使用Python标准库中的concurrent包,实现多进程,pdf_to_word方法是对上面读取PDF和写入word逻辑的封装。后面的while循环是查询任务是否进行完成。

效果

到这里,我们已经实现了多线程批量转换PDF为word文档。拿谋篇著名文章来试验一下,效果如图(左侧是转换后的word,右侧是PDF):

不想写代码?

本文介绍的所有代码,已经打包成了一个独立可运行的项目,存放在github,如果不想自己写代码,可以直接clone或下载github项目运行。项目地址如下(记得点star):

https://github.com/simpleapples/pdf2word

欢迎关注Python私房菜

「贪吃蛇大作战」的刷分尝试

昨天同学推荐了一个最近比较火的游戏「贪吃蛇大作战」,今天已经到了AppStore总榜第二的位置。这两天一有空就会掏出手机玩几盘,不过无尽模式玩到3000分左右就遇到了瓶颈,于是怀着单纯的目的想研究下这个游戏的接口…

图1

使用Charles抓取请求

既然是研究接口,第一步就是看看接口的URL和参数返回都是什么了,请出神器Charles。

图2

通过Charles可以清楚地看到,游戏提交分数的接口和参数,至于返回,貌似没什么用。接下来仔细研究下请求,有几个参数能比较容易的判断他们的含义和作用。

1
2
3
4
5
6
7
8
9
10
11
12
device_id: 设备ID
game_mode: 1是无尽模式 2是限时模式
kill: 击杀次数
length: 蛇的长度
market: 我抓的是iOS设备发的请求,所以这里是apple
platform: 抓到的请求里是1,还不太明白具体的意思
push_channel: 也是1,不明白意思
push_id: 固定值111111111222222223333333344444444,后面逆向apk也证明了这一点
sid: 每次登录都会更新
snake_sign: 签名
uid: 用户ID
version: 我使用的版本是固定值2.1

通过逆向APK获取签名算法

明确了参数的意思,接下来就可以伪造请求了,不过在伪造请求之前,还有一个参数比较麻烦,那就是snake_sign,请求签名,从图中snake_sign的内容看,似乎有点像是一个base64过的东西,不过base64decode发现,根本不可读。与其猜签名算法,不如考虑一下其他办法。

「贪吃蛇大作战」还有安卓版本,并且引导下载的页面提供了apk,于是下载apk,使用jadx-gui进行逆向,使用jadx-gui打开apk后,代码一览无余。

通过查看代码,可以清楚的看到snake_sign字段的签名逻辑,首先将请求中所有参数按ASCII码顺序排列拼接成字符串,然后在字符串前添加POST& + [URL Path],添加后使用Key进行SHA1加密,key也可以在代码中获取到。对加密生成的结果,需要做一次base64,才是最终snake_sign字段的结果。

1
POST&top_list_v2/update_score&device_id=XXX&game_mode=1&kill=1&length=35&market=apple&platform=1&push_channel=1&push_id=111111111222222223333333344444444&sid=XXX&uid=XXX&version=2.1

图3

关于「贪吃蛇大作战」的一些猜测

签名方法已经获取到,本以为可以安心刷分了,但是只成功刷了一次4000分160击杀。猜测可能进入排行版需要人工审核,一个理由是刷完后过了几分钟才显示到排行版,另一个理由是,刷完4000分169击杀后,很快刷了一个高的离谱的分,但是并没有显示在排行榜,后面无论刷多少分都再也没有显示出来过,怀疑可能被人工屏蔽。

另外整个游戏和API的交互只有登录、获取用户信息、获取排行版最高分、提交分数等几种,所以游戏也不是真正的实时。

图4

在阿里云CentOS7中配置基于Nginx+Supervisor+Gunicorn的Flask项目

需要在阿里云的CentOS7中搭建Flask应用的生产环境,记录一下。

配置Centos7

root登录后,首先新建一个普通用户并设置密码

1
2
adduser user
passwd user

接下来将用户的公钥复制到~/.ssh中,命名为authorised_keys,修改/etc/ssh/sshd_config禁用ssh中的root登录,修改默认ssh端口,并使用证书登陆,修改如下内容

1
2
3
Port 65535
PasswordAuthentication no
PermitRootLogin no

配置完成后重启ssh服务

1
systemctl restart sshd.service

CentOS7中用firewalld替换了iptables,需要手动将80端口和修改后的ssh端口添加到firewalld中

1
2
3
firewalld --add-port 80/tcp --permanent
firewalld --add-port 65535/tcp --permanent
firewalld --reload

配置Nginx

yum中可以直接安装nginx

1
yum install nginx

安装好后在/etc/nginx/default.d中添加location的配置,并指向8001端口,以后Flask会监听8001端口

1
2
3
location / {
proxy_pass http://127.0.0.1:8001
}

配置好后重新载入nginx配置

1
systemctl reload nginx.service

安装Python

CentOS自带Python2.7,如果使用Python3,需要单独安装。

1
2
3
4
5
6
wget https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz
tar xf Python-3.4.3.tgz
cd Python-3.4.3
./configure --prefix=/usr/local --enable-shared
make
make altinstall

接下来在项目中搭建虚拟环境,Python2虚拟环境使用virtualenv安装(使用pip install virtualenv命令安装),Python3环境使用pyvenv安装(Python3自带pyvenv),以Python3环境为例,在Web项目中,建立虚拟环境文件夹venv

1
pyvenv venv

接下来在项目路径下启用虚拟环境

1
source venv/bin/active

退出虚拟环境使用Ctrl+C或deactive命令

安装配置Gunicorn

Gunicorn使用pip install gunicorn安装,注意需要在虚拟环境中使用pip安装,这样才对应虚拟环境中的Python版本。安装好后,新建一个Gunicorn的配置文件,比如deploy_config.py,加入内容如下

1
2
3
4
5
6
7
8
import os
bind='127.0.0.1:8001' #绑定的端口
workers=4 #worker数量
backlog=2048
debug=True
proc_name='gunicorn.pid'
pidfile='/var/log/gunicorn/debug.log'
loglevel='debug'

保存文件后在虚拟环境中使用Gunicorn尝试启动

1
gunicorn -c deploy_config.py myapp:app

myapp是入口Python文件名,app是函数名。如果输出worker相关信息,表明启动成功。

安装配置Supervisor

yum可以直接安装Supervisor,需要注意的是Supervisor只支持Python2,所以不要在虚拟环境中使用pip安装supervisor。

1
yum install supervisor

安装后,在/etc/supervisord.d中建立配置文件xxx.ini,内容如下

1
2
3
4
5
6
7
[program:xxx]
command=/var/proj/xxx/venv/bin/python /usr/bin/gunicorn -c /var/proj/xxx/deploy_config.py myapp:app
autorstart=true
directory=/var/proj/xxx
autorestart=true
startsecs=10
startretries=20

xxx是项目名称,注意command中最好都写全路径,以区别系统环境和项目虚拟环境。完成后启动supervisord,使配置生效

1
supervisord -c /etc/supervisord.conf

在viewDidAppear中PushViewController失败的问题

需要在ViewController(FirstViewController)的viewDidAppear中Push另一个ViewController()SecondViewController),于是使用如下代码:

- (void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];

    UIViewController *secondViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"secondViewController"];
    [self.navigationController pushViewController:secondViewController animated:YES];
}

在iOS8中,这段代码工作良好,当FirstViewController出现时,由于立刻Push了SecondViewController,即使animated参数是YES,Push的动画都没有显示出来。但是在iOS7中却出现了不一样的情况,pushViewController方法似乎没有执行,SecondViewController也没有被推出。打断点可以发现pushViewController方法是被执行了的,但是界面上没有任何效果。

由于pushViewController方法是在viewDidAppear中被调用的,会不会是因为viewDidAppear时FirstViewController还有什么UI上的动作没有处理完,导致立即调用pushViewController失败?那么将pushViewController放入dispatch_async中应该就能解决这个问题。使用如下代码实现:

- (void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];

    UIViewController *secondViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"secondViewController"];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.navigationController pushViewController:secondViewController animated:YES];
       });
}

使用GCD后,SecondViewController被成功的Push了出来。虽然不能确定,但是也在一定程度上印证了上面的猜测。

使用TestFlight进行应用的Beta测试

TestFlight已经被Apple集成到iTunes Connect中,现在使用TestFlight可以很方便的进行应用的Beta测试。下面就来介绍一下如何使用TestFlight进行测试,已经其中的一些小问题。

首先需要在iTunes Connect中启用TestFlight,可以针对每个App的某一版本,决定是否启用TestFlight。进入iTunes Connect -> My Apps -> 某个App -> Prerelease,在上传的Build右上角,打开TestFlight的开关,接下来就针对这个版本启动了TestFlight。

图1

有了可以测试的应用,接下来还要有测试用户,TestFlight测试用户有两种,一种是内测用户InternalTester,最多25个,一种是公测用户ExternalTester,最多1000个,内测用户需要首先成为iTunes Connect User,而公测用户只需要知道他的Apple Id就可。要进行公测需要先经过Apple Review团队的审核。我们以添加一个内测用户为例,进入iTunes Connect -> Users and Roles -> iTunes Connect Users,首先添加一个iTunes Connect User,之所以要添加iTunes Connect User,是因为TestFlight的测试用户必须是一个iTunes Connect User,并且这个用户的角色必须是Admin或者Technical。添加后,用户的邮箱里会收到一封邀请邮件,点击邮件中的链接可以激活成为iTunes Connect User。进入iTunes Connect -> Users and Roles -> TestFlight Beta Testers,激活的iTunes Connect User会出现在这里,选中点击右上角的保存,这个用户就成为一个内测用户了。

图2

有了内测用户和测试App,下一步就要将两者关联了,进入iTunes Connect -> My Apps -> 某个App -> Prerelease -> Internal Testers中,这里会显示可用的内测用户,勾选用户并点击右上角的Invite,Apple就会给这个邮箱发送一封邀请邮件。接下来需要在要测试的手机上安装TestFlight,安装好后,TestFlight会自动绑定当前手机的Apple Id登录,这里你会发现TestFlight里并没有出现测试App,经过测试,必须要在iOS的自带邮件客户端上点击邀请邮件中的链接,才会跳转到TestFlight中安装应用。这是TestFlight让人非常不爽的一点。你可以将收到的邀请邮件转发到自带的邮件客户端所绑定的邮箱上,让后在自带邮件客户端中点击邀请链接打开TestFlight。

图3

点击链接后会自动跳转到TestFlight中,点击Install安装应用,再次进入TestFlight中就可以看到已安装的测试应用了。

iOS8中LaunchImage和LaunchScreen的完美结合

Apple在iOS8中推出了LaunchScreen.xib来代替之前的LaunchImage作为程序的启动界面,相比与LaunchImage,在iOS设备屏幕尺寸越来越多样的情况下,LaunchScreen.xib依托AutoLayout无疑更方便,否则,对于一个兼容iPhone5-iPhone6Plus的应用,就需要有4长不同尺寸的LaunchImage。

但是LaunchScreen只有在iOS8中才能被支持,所以一些开发者还是选择用传统的LaunchImage方式。不过,还有一种方式是将LaunchImage和LaunchScreen结合,在大尺寸iPhone中使用LaunchScreen(iPhone6和iPhone6Plus都是iOS8系统),在iOS7中使用LaunchImage(使用iOS7的手机只有4寸和5.5寸的iPhone,所以只需要两张图)。

首先进入Target配置,找到App Icons and Launch Images,Xcode6中默认使用了LaunchScreen.xib,而LaunchImage则没有使用。

图1

接下来点击Use Assets Catelog,这时Xcode会在Images.xcassets中生成LaunchImage,这里我们只需要给4寸Retina屏和3.5寸2x屏放两张LaunchImage就好了。

在iOS8中系统会优先调用LaunchScreen作为启动界面,而iOS7不支持LaunchScreen则会使用LaunchImage中的图片作为启动界面。

图2

项目在iOS7模拟器中运行效果(使用LaunchImage)

图3

项目在iOS8模拟器中运行效果(使用默认的LaunchScreen.xib)

创建一个简单的Safari扩展

之前做过一个把网址转为二维码的Chrome扩展,想在Safari中也使用这样的扩展,搜索了一下貌似没有同类型的,所以自己打造了一个Safari扩展,并且把过程记录下来。

获得开发者证书

要建立一个Safari扩展,首先需要生成一个开发者证书。访问Apple开发者中心,加入Safari Developer Program,加入开发者计划是免费的。加入开发者计划后就可以生成证书了,访问Certificates, Identifiers & Profiles,点击右上角的加号,生成一个开发者证书。成功后将证书下载到本地并导入Keychain Access中。

在Safari中创建扩展

打开Safari,在菜单中选择Safari—Preferences-Advanced,勾选最下方的Show Develop menu in menu bar,如图。

图1

这时在菜单栏中会出现Develop菜单,选择Develope-Show Extension Builder,打开扩展编辑器,点击左下角的+,选择New Extension,保存到一个位置(例如Desktop)。

图2

这时,Desktop文件夹中会出现一个demo.safariextension文件夹,这个文件夹里的内容就是我们生成的Safari扩展的根目录。如果前面的开发者证书已经正确导入,扩展的介绍里会出现Safari开发者的Id。

图3

编辑扩展基本信息

扩展建立后,下面会有一票东西需要填写,首先填写扩展的基本信息。

  • Display Name: 扩展的显示名称
  • Author: 作者名字
  • Description: 插件介绍
  • Website: 插件网站
  • Bundle Identifier: 这里需要填写唯一id
  • Update Manifest: 这里需要填写一个plist格式文件的地址,Apple会定期检查这个地址中的Version,如果有升级,就会访问插件的下载地址更新插件,当然,如果插件只是自用而不提交Safari Extensions Gallery的话,这一栏可以不填,plist文件格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Extension Updates</key>
<array>
<dict>
<key>CFBundleIdentifier</key>
<string>com.zangzhiya.url2qrcode</string>
<key>Developer Identifier</key>
<string>开发者ID</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>URL</key>
<string>http://simpleapples.com/upload/url2qrcode-safari/url2qrcode.safariextz</string>
</dict>
</array>
</dict>
</plist>
  • Access Level: 这里需要选择插件对页面访问的权限,比如我们我们要做的URL转二维码插件,需要获取当前Tab的URL,那么这里需要选择ALL

图4

添加图标

我们的扩展到现在还没有图标,Safari会分配一个默认的指南针图标,添加图标的方式很简单,在扩展的根目录下放置一个16px * 16px的png格式图片,Safari就会自动将其置为图标。

添加Toolbar Item和Popovers

首先看一下这个扩展的完成态,如图。

图5

可以看到这个插件有两部分组成,一个是工具栏的按钮,在Safari中被称为Toolbar Item,一个是点击按钮后弹出的层,是一个Popover,和Chrome中的Popup类似,这个Popover也是一个html页面。

接下来,需要在Safari Extension Builder中继续编辑,添加一个Toolbar Item和一个Popover,如图。

图6

Toobar Item红的Image必须是一个8bit的16px * 16px透明背景的黑白png图像(繁琐的要求),而Popover需要是一个html文件。这里的路径都是相对于扩展文件夹的。到这里插件的配置工作就完成了。

编程

可以在我的Github上查看这个扩展的代码,里面用到的Safari Api(获取当前页URL),可以在Safari Developer Libray中找到,里面的内容非常详细。

打包

当扩展开发完成后,就可以点击Safari Extension Builder中的Build Package打包了,打包出来的会是一个safariextz格式的文件,双击就可以安装。如果不想提交Safari Extension Gallery,可以直接把这个文件拷贝给他人安装。

图7