ruanpapa和又吉君写字的地方


  • 首页

  • 分类

  • 归档

  • 标签
ruanpapa和又吉君写字的地方

2017年终总结

发表于 2017-12-30 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

这几天被“18岁”、“网易云音乐年度听歌报告”、“支付宝年度账单”各种刷屏,大家都玩的不亦乐乎,毕竟也是年底了,辛苦工作学习了一年了,也该是时候放松一下了。自己通常都不是太喜欢凑热闹的,偏安一隅,躲在自己的小角落,写下刚刚过去的2017年发生的大事小情,顺便展望一下明年。这也是我第一次提笔写下自己的年终总结,也希望自己以后都能坚持这件事情。

工作

过去的这一年,最先想说的还是工作上的事情,毕竟今年告别用了16年的学生身份。

前半年一直是实习生身份,上下班都是步行回学校,完全没有觉得生活有发生什么变化。公司给了大量的时间自己学习与熟悉技术,所以上班并没有感到什么压力,下了班回去宿舍,打打球,这里溜达溜达,那里吹吹水,偶尔抽出时间处理论文、入党、毕业的事宜,一切如故。

接下来顺利转了正,到现在也半年了,这半年是技术快速进步的一段时间,也开始接触比较复杂的需求。以前常常能在社区看到大家吐槽做业务的程序员是最底层的,我倒是不这么认为,我觉得凡事都需要循序渐进,不是每个人都能一入门就玩核心算法、深度学习、底层架构这些高深的东西的,相反,当前任何公司绝大部分的都是这些业务需求,做需求的能力扎实了,到哪里都不愁找不到工作。正如我在入职的职业规划上写的:希望自己在未来的一、两年内能尽量的做各种需求。

但是只停留在术而不去钻研道的话,这样职业生涯也不会太长久,自己也在空余时间接触了一下clang+llvm、逆向工程等比较高深的技术,铩羽而归算不上,反正还没有什么实际产出,还需要的前期知识、铺垫比较多,我会慢慢补上。同时自己也一直对金融这一块比较有兴趣,平时经常驻足新浪财经、虎嗅等各种财经类的网站,尤其自己也是搞技术的,互联网+金融,完全是一片广阔的天地。

生活

今年最重要的是什么呢?那当然是,我收获了一个全世界最可爱的女朋友啦~(≧▽≦)/。1月10号初识,3月7号正式在一起,不知不觉竟然携手走过了快一年了。这一年里,有过很多个阶段。从各自试探,到互生好感,再到每天甜甜腻腻,再到现在更多了一份理性。期间有因为小事吵吵闹闹,也有三观的激烈碰撞,异地总是比较辛苦的,但是能经受的住异地的考验,那得来的才更加难能可贵。

这一年也是我真正独立生活的第一年,很庆幸自己还是保持了一些良好的习惯,房间总是会打扫收拾,周末的时候会自己煮饭,平时会进行阅读学习,偶尔还会去跑步。不过跑步运动这一段时间真的是松懈了,总觉得跑起来动力不足,时常跑了没几公里就觉得够了,然后就停下了脚步,新的一年,还是希望自己能把运动的好习惯重拾起来。

上一年定下的目标

  • 写5篇高质量的技术文章(完成度60%):今年的产出只有3篇技术文章,实在是太少了,也是因为自己今年比较多私人事情,明年还是要更多的进行产出。不过今年倒是打理了自己的博客、掘金、简书等账号,更多的将自己的产出放到社交平台上互动,尤其是掘金,竟然已经有了2000个关注者了,但是自己心里清楚离大牛还有很远的距离,要多多努力了。
  • 开发并维护1个开源库(完成):呃。。。看着Github上稀稀拉拉的contributions,实在拿不出手啊。
  • 翻译5篇技术文章 3/5(完成度60%):iOS的Core Text教程:制作一个杂志应用、iPhone X 网页设计(校对)、关于Swift的编译时间优化。
  • 看15本书(完成)(《南京大屠杀》、《激荡三十年》、《史蒂夫乔布斯传》、《穷查理宝典》、《100个Swift2开发必备Tip》、《亮剑》、《数学之美》、《浪潮之巅》、《欧神文集01》、《ASO优化道与术》、《在你身边,为你设计Ⅱ》、《腾讯传》、《欧神文集02》、《iOS应用逆向工程》、《欧神文集03、04、05》):今年的阅读还是达标的,有自己感兴趣的人物传记、历史等方面的书籍,也有一些是技术书籍、投资入门方面的一些书。
  • 开始接触理财与投资产品

我想要的2018

  • 写6篇技术文章,并更多的在社区中互动。
  • 开发一个独立APP
  • 技术方面还是深耕iOS:逆向工程、图像处理,拓展方面的打算入门一下机器学习、区块链的基础知识
  • 看12本书,而且得保证看了的书都写一篇读后感,否则不能算上这本书。
  • 争取能做到Github的contributions能亮起来一半吧。。。
ruanpapa和又吉君写字的地方

(翻译)关于Swift的编译时间优化

发表于 2017-10-22 | 分类于 ruanpapa--技术贴 | | 阅读次数

原文链接:Regarding Swift build time optimizations

上周,在我读完 @nickoneill 写的一篇优秀的博文《为缓慢的Swift编译时间提速》后,我发现用一个不同的角度去审视 Swift 代码并不是很难的一件事。

可以被认为是简洁的一行代码现在引发了一个新的问题 – 是否应该把这行代码重构成对应的9行代码以让编译器更容易工作(看看接下来要讲的关于空合运算符(nil coalescing operator)的示例)?到底哪个才是更重要的,简洁的代码还是对编译器友好的代码?这取决于项目的大小和开发者的想法。

慢着。。。这里有一个 Xcode 插件

在展示具体的例子之前,我先想到就是手动查看日志是一件非常耗时的事情。有人提出了用终端命令可以让这件事情变得比较容易,但是我更进一步,把这个用 Xcode 插件 给实现出来了。

Xcode插件.png

对我来说,最初的目的就是找到并修复最耗时的地方,但我现在的想法是让它经历更多的迭代过程。这样的话我就不仅可以让代码编译更有效率,还可以防止第一次进入项目的耗时。

更加惊喜的是

我经常在多个 Git 分支间跳来跳去,等待一个缓慢的项目编译完成往往浪费了大量的时间。我想了好长一段时间为什么我的一个宠物项目会编译地这么缓慢(大概2万行 Swift 代码)。

在我学习了究竟是原因导致的这件事之后,我不得不承认我的确很吃惊,一行代码就需要几秒钟来编译。

让我们看看几个例子。

空合操作符

编译器是很不喜欢这里的第一种方式的。在展开下面两处简写的代码之后,编译时间减少了99.4%。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编译时间: 5238.3ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)
// 编译时间: 32.4ms
var padding: CGFloat = 22
if let rightView = rightView {
padding += rightView.bounds.width
}
if let leftView = leftView {
padding += leftView.bounds.width
}
return CGSizeMake(size.width + padding, bounds.height)

ArrayOfStuff + [Stuff]

这个看起来像下面这样:

1
2
3
4
return ArrayOfStuff + [Stuff]
// 而不是
ArrayOfStuff.append(stuff)
return ArrayOfStuff

我经常这样做,每次都会对所需的编译时间产生影响。下面是最差的一个,这里的编译时间减少了97.9%。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编译时间: 1250.3ms
let systemOptions = [ 7, 14, 30, -1 ]
let systemNames = (0...2).map{ String(format: localizedFormat, systemOptions[$0]) } + [NSLocalizedString("everything", comment: "")]
// 一些中间的代码
labelNames = Array(systemNames[0..<count]) + [systemNames.last!]
// 编译时间: 25.5ms
let systemOptions = [ 7, 14, 30, -1 ]
var systemNames = systemOptions.dropLast().map{ String(format: localizedFormat, $0) }
systemNames.append(NSLocalizedString("everything", comment: ""))
// 一些中间的代码
labelNames = Array(systemNames[0..<count])
labelNames.append(systemNames.last!)

三元运算符

仅仅只是把三元运算符替换成 if-else 语句,就让编译时间减少了92.9%。如果将 map 换成 for 循环,就又能减少75%(但是那样的话我的眼睛可就受不了了)。😉

1
2
3
4
5
6
7
8
9
10
// 编译时间: 239.0ms
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}
// 编译时间: 16.9ms
var labelNames: [String]
if type == 0 {
labelNames = (1...5).map{type0ToString($0)}
} else {
labelNames = (0...2).map{type1ToString($0)}
}

转换 CGFloat 到 CGFloat

没听懂我在说什么?其实下面例子中值已经是 CGFloat 了,并且有些括号是多余的。在清理完这些冗余之后,编译时间减少了99.9%。

1
2
3
4
5
// 编译时间: 3431.7 ms
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180
// 编译时间: 3.0ms
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180

Round()

下面是一个很奇怪的例子,下面的例子中变量是一个局部变量与实例变量的混合。这个问题似乎不是出在四舍五入本身,而是在于结合代码的方法。去掉四舍五入的方法大概能减少 97.6% 的构建时间。

1
2
3
4
// 编译时间: 1433.7ms
let expansion = a — b — c + round(d * 0.66) + e
// 编译时间: 34.7ms
let expansion = a — b — c + d * 0.66 + e

注意:以上所有测试都在MacBool Air(13英寸,2013年中)上进行。

尝试一下吧

不管你是否面临过编译时间太长的问题,编写对编译器友好的代码都是非常有用的。我确信你会在其中找到一些惊喜。作为参考,这里有完整的代码,我的工程中可以5秒内完成编译…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import UIKit
class CMExpandingTextField: UITextField {
func textFieldEditingChanged() {
invalidateIntrinsicContentSize()
}
override func intrinsicContentSize() -> CGSize {
if isFirstResponder(), let text = text {
let size = text.sizeWithAttributes(typingAttributes)
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)
}
return super.intrinsicContentSize()
}
}
ruanpapa和又吉君写字的地方

响应式编程与MVVM架构—理论篇

发表于 2017-10-12 | 分类于 ruanpapa--技术贴 | | 阅读次数

前段时间,在使用了一段时间的MVVM架构之后,我从实际的项目中抽离出来,对使用MVVM架构的整个过程进行了总结,对于架构、对于编程思维又有了不一样的体会。于是提笔写下自己探索MVVM架构的经验和心得,以飨读者。

本文会先对MVC架构做一个回顾,明确MVC中各层的职责;然后会提出MVVM架构的概念,本来接下来应该顺势举几个MVVM的例子进行说明的,但是考虑到响应式编程之于MVVM的重要性,所以在举例之前会先讲解一下响应式编程的概念(出于篇幅考虑,将MVVM架构实践独立成一篇文章,想直接看实例的请移驾这里);最后会对MVC和MVVM的取舍谈谈自己的看法。话不多说,现在进入正题。

MVC架构

MVC(Model-View-Controller),是一种常见的客户端软件开发框架,具体到iOS上,绝大部分人从开始接触iOS编程的时候都被告知MVC就是事实上的默认框架。系统也为我们实现好了公共的视图类:UIView 和控制器类:UIViewController。大多数时候,我们都需要继承这些类来实现我们的程序逻辑,因此,我们几乎逃避不开MVC这种设计模式。下面就对MVC各层的职责进行明确:

Model

Model层 是服务端数据在客户端的映射,是薄薄的一层,完全可以用struct表征。下面看一个实例:

Model

可以看到,Model 层通常是服务端传回的 JSON数据的映射,对应为一个一个的属性。不过现在也有很多人将网络层(Service层)归到Model中,也就是MVC(S)架构。同时,大部分时候数据的持久化操作也会放在Model层中。

总结一下,Model层的职责主要有以下几项:HTTP请求、进行字段验证、持久化等。

View层

View层是展示在屏幕上的视图的封装,在 iOS 中也就是UIView以及UIView的子类。下面是UIView的继承层级图:

View

View层的职责是展示内容和接受用户的操作与事件。

Controller层

看了Model层和View层如此简单清晰的定义,如果你以为接下来要讲的Controller层的定义也跟这两层一样,那你就要失望了。

粗略总结了一下,Controller层的职责包括但不限于:管理根视图以及其子视图的生命周期、展示内容和布局、处理用户行为(如按钮的点击和手势的触发等)、储存当前界面的状态(例如分页加载的页数、是否正在进行网络请求的布尔值等)、处理界面的跳转、作为UITableView以及其它容器视图的代理以及数据源、业务逻辑和各种动画效果等。

画风似乎不对啊,为什么Controller层的职责比其他两层加起来还多?

MVC的困境

因为MVC架构中Controller层往往代码很多,动辄2、3千行的这一特点,MVC也常常被调侃成是 Massive View Controller。造成这个问题的原因就是MVC的定义太过简单朴素,要知道支撑一个尚不算大的企业级应用都动辄几十万行代码,还不包括各种依赖的第三方库。这么多的代码如何安置?按照传统的MVC定义,分割了小部分到Model层和View层,剩下的代码都没有其他地方可以去了,于是被统统的丢到了Controll层中。

庞大的Controller层带来的问题就是难以维护、难以测试。而且其中充斥着大量的状态值,一个任务的完成依赖于好几个状态值,而一个状态值又同时参与到多个任务中,这样复杂的多对多关系带来的问题就是开发效率低下,需要花费大量的时间周旋在各个状态值之间,对以后的功能拓展、业务添加也造成了障碍。

这样的前提下,架构的改进就显得非常有必要了。

MVVM架构初探

MVVM(Model-View-ViewModel),2005年由微软的WPF和Silverlight的架构师 John Gossman 提出,是MVP模式与WPF结合发展演变过来的一种架构框架。MVVM实质上还是MVC架构范围,是一个精心优化的MVC架构,所以与MVC架构是兼容的。

MVVM首先将View层和Controller层进行了合并,统称为View层,因为View层和Controller层往往是一起出现的。然后引入了一个新的模块 — ViewModel层,ViewModel层承载的内容就是之前在Controller层中视图展现逻辑。MVVM的图示如下:

MVVM图示

什么是视图展现逻辑呢?在一款应用中,数据的来源可能是服务端返回、数据库获取和用户输入,然后存储在Model中,但是这样的数据是一种“未经格式化的”原始数据,还不能直接显示到屏幕上。比如Model中可能有姓、名、昵称等属性,在某些界面中需要显示成”姓名”的样式,某些界面中显示成”名姓”的样式,某些界面中显示”昵称”的样式。视图展现逻辑就是把这些原始数据经过业务需求处理成展现到屏幕上的数据。可以把一个应用看成是播出一个新闻节目,Model层就是一大堆繁杂的稿件,View层就是主持人实际播报的新闻,而ViewModel层就是幕后的编辑处理团队,负责从凌乱的稿件中抽出需要的信息,整理成播报时用的稿件。这样主持人拿着整理好的稿件,就能轻松的播报新闻了。

但是呢,平白无故多了一个ViewModel层。多一个层带来的直接问题就是信息的传递问题,层与层之间需要互通信息,进行交流。在MVVM架构的实现中,开发人员想出了一个与传统消息传递所不一样的方式,这就引出了响应式编程的概念。

响应式编程

响应式编程(Reactive Programming),是一种面向数据流和变化传播的范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。举个维基百科中的例子:c:=a+b表示将表达式的结果赋给c,而之后改变a或b的值不会影响c。但在响应式编程中,c的值会随着a或b的更新而更新。

响应式编程

也就是说,上图中c的值最后会是5。

同样的例子还有Excel中的单元格,单元格可以包含字面值或类似”=B1+C1”的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化 。

如何实现所谓的响应式编程?在WPF中官方提供了Data Binding技术,macOS中也有类似的Cocoa Binding框架,但是在iOS中官方没有提供这样的框架。于是GitHub上出现了ReactiveCococa(以下简称为RAC)和RxSwift等优秀的第三方框架。

在RAC的思维中,iOS上的一切都是在变化的数据流,比如输入框上用户正在不断输入的文字、被点击的按钮、旋转缩放的视图、不断改变的NSString等等,这些就像是一个”水龙头”,当有变化产生的时候,水龙头就会出水,把变化传递下去,对这个变化感兴趣的人就可以在这个水龙头上套一个”水管”,这个人就成为了一个接收者(subscriber),当有变化产生的时候,接收者就能从水管中拿到这个变化的具体信息。

RAC就提供了这样的”水管”,但是和现实中的水管有所不同,RAC有自己的一些限制:水管中传递的不是水,而是一个个的”玻璃球”,这些玻璃球的直径和水管的内径一样大,保证了玻璃球在水管中都是依次排列通过的,这就保证了不会出现多个玻璃球并列通过的情况。更加重要的是,在拿到玻璃球之前,可以对其进行一些个性化的定制。例如,可以在水龙头上加一个过滤嘴(filter),不符合的不让通过;也可以加一个改动装置,把球改变成符合自己的需求(map);还可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),经过了这些定制之后,出来的符合要求的玻璃球就能拿来直接用了。

在这么强大的框架帮助下,MVVM所引入的ViewModel层和其他层之间的通讯问题得到了解决。

MVC还是MVVM?

在考虑是否要选择MVVM架构之前,先来总结一下MVVM的优势和不足。

MVVM主要有以下几个优势:

  • Controller层瘦身:将视图展现逻辑抽出到ViewModel层带来的直接变化就是Controller层变得更加轻量级,更加容易维护。
  • 更加容易测试:Controller层代码减少了,也意味着对Controller层的测试更加容易。
  • 兼容MVC:选择MVVM并不意味着完全摒弃MVC,MVVM相当于是MVC的超集,所以和MVC是兼容的,也就是说,可以只在某一个模块中使用MVVM,不用担心迁移架构时会造成需要全局重构的问题。
  • 解决状态以及状态之间依赖过多的问题:这个优势是由RAC所带来的,响应式编程关注的数据的变化和流向,免除了一部分的状态,而是直接将变化传到显示的控件上,实例在这里。
  • 提供统一的消息传递机制:这也是由RAC所带来的,RAC对iOS编程中大部分的实物进行了抽象,提供了统一的接口,所以可以将iOS上KVO、通知(NSNotification)、委托(delegate)、Target-Action、块(Block)等消息传递方式统一,实例在这里。

MVVM存在的问题主要有:

  • 学习曲线比较陡,通常需要引入第三方库(ReactiveCocoa/RxSwift):使用MVVM通常需要引入第三方库,而且需要转换成响应式编程的思维方式,这是需要花费相当的学习适应时间的。
  • 创建更多的类:基本上每个Controller类会对应有一个ViewModel类
  • 性能上有一定影响,调用栈变深:RAC的实现底层依赖于KVO,带来的问题是性能的损耗,比如光是subscribNext就慢了1个数量级,目前的回调堆栈也比较深,最简单的[signal subscribeNext^(id x){}]就会有近40次的调用。

MVVM好处不少,缺点也一堆。那到底要不要用MVVM呢?我觉得,在项目还不算臃肿的时候,可以简单的对现有的MVC进行解耦优化,比如将网络层(Service层)、持久层(Storage层)等部分抽象出来即可。另一方面,MVVM对MVC也是兼容的,可以考虑在项目中的某个模块试水MVVM,觉得好再逐步替换其他模块;而且很重要的一点是,响应式编程这样一种范式相当的锻炼我们的编程思维,让我们可以站在数据的变化和流向的角度去思考我们的整一个项目,掌握这种思维方式也可以反哺到我们项目中别的地方。

架构没有绝对的优劣,适合自己的架构就是最好的架构,那就让我们理性分析,拥抱变化。

ruanpapa和又吉君写字的地方

iOS性能优化探讨

发表于 2017-09-20 | 分类于 ruanpapa--技术贴 | | 阅读次数

最近在公司内部做了一个分享会,探讨了iOS性能优化的话题,现在将重点的内容整理好发出来,各位大牛斧正。

本文将从原理出发,解释卡顿发生的原理,然后会讲解项目中行之有效的几个优化点,最后会展望一下接下来将要尝试的方向。下面进入正题。

屏幕显示的原理

基本原理

屏幕显示

我们知道,远古时代的CRT显示器的显示原理是用电子枪扫描荧光屏来发光。如上图所示,电子枪按照从左到右,从上到下的顺序扫描。当电子枪换到新的一行准备进行扫描时,也就是上图A4、B4、C4、D4的位置,显示器会发出一个水平同步信号;而当一帧画面绘制完成后,电子枪回复到原位准备画下一帧前,也就是上图D4的位置,显示器会发出一个垂直同步信号。垂直同步信号的作用一方面是通知显示器回到A1位置,另外一方面,也通知显卡,准备输出下一帧画面。现在已经是液晶显示器的时代了,不再使用电子枪扫描了,但是原理还是类似的,水平同步信号和垂直同步信号还是一样被使用的。

计算机工作原理

计算机系统的工作原理如上图所示:首先是CPU的工作,包括创建视图分配内存、计算布局、图片解码以及文本绘制等;接下来轮到GPU工作了,GPU负责视图变换、合成和渲染等;GPU渲染完提交到帧缓冲区中,等收到垂直同步信号后将帧缓冲区的内容显示到屏幕上。

屏幕撕裂(Screen tearing)

上述的简单的屏幕显示原理其实会产生这样一个问题:假设我们的显卡速度很快,每秒生产的帧数肯定要超过显示器刷新率。那么在实际数据处理过程中,缓冲区的数据,在被输出之前,就被显卡不断的刷新重写。但是缓冲区并不是“先清空再写入数据”,这太没有效率,而是采用“新数据覆盖老数据”的方式。

假设这样一种情况,缓冲区已经有一副完整的帧画面(A帧),然后显卡生成了下一帧画面(B帧),新一帧的数据开始写入缓冲区,写到一半的时候,垂直同步信号来了,于是缓冲区的数据被输出到显示器。但问题是,这时缓冲区的数据,是由一半A帧和一半B帧数据合成的。因此最终显示器上显示出来的画面就不是一副完整的画面,这就是“画面撕裂”现象出现的原因,如下图。

屏幕撕裂

那怎样才能解决画面撕裂的问题呢?简单来说只要让帧缓冲区里的数据始终保持一副完整的画面就可以了。从技术角度出发,其实就是利用刚刚提到的垂直同步信号。

具体说起来就是,当显卡生成了一副完整画面并写入了帧缓冲区之后,暂停!然后开始等待垂直同步信号,当得到垂直同步信号后,再继续渲染下一帧写入缓冲区。这样就可以保证在缓冲区的数据始终是一副完整的画面,不会出现前后帧混合的问题。

卡顿产生原因

但是呢,垂直同步机制带来了一个新的问题 —— 掉帧。所谓的掉帧,跟垂直同步有一定关系,因为垂直同步机制决定了如果在一个时钟周期内CPU或者GPU没有完成各自的任务的话,就会将帧缓冲区里的内容直接丢弃!掉帧并不能完全怪罪于垂直同步机制,更重要的原因是我们作为开发者没有进行足够的优化,将过重的任务派发到了CPU或者GPU上,下图(from:iOS 保持界面流畅的技巧)是掉帧的图示,表明CPU或者GPU任意一个没能在时钟周期内完成自己的任务的话都会导致卡顿掉帧。

卡顿

行之有效的优化点

  1. 提前布局

    提前布局可以说是最重要的优化点了。其实在从服务端拿到 JSON 数据的时候,关于视图的布局就已经确定了,包括每个控件的frame、cell的高度以及文本排版结果等等,在这个时候完全可以在后台线程计算并封装为对应的布局对象XXXTableViewCellLayout,每个cellLayout的内存占用并不是很多,所以直接全部缓存到内存中。当列表滚动到某个cell的时候,直接拿到对应的cellLayout配置这个cell的对应属性即可。当然,该有的计算是免不了的,只是提前算好并缓存,免去了在滚动的时候计算和重复的计算。通过这一个优化,将本来的fps50的列表优化到了55、56左右,可以说从肉眼上已经看不出有卡顿掉帧了。

    ​

    cellLayout示例图

    上图是项目中某个cellLayout的部分代码,可以看到里面存的就是所有控件的frame和文本的排版结果而已,里面没有任何的黑科技,只是将本来在滚动中才做的事情提前了而已。

    ​

  2. 按页加载缓存

    • 现状分析:90%的APP有tableview,90%的tableview里有上拉刷新和下拉加载。以我司的项目ZAKER中的热点新闻界面为例,简单流程大概是这样的:①应用启动的时候会将磁盘中所有的新闻一次性读取出来显示到屏幕上; ②在每次下拉刷新和上拉加载的时候会将内存中所有新闻缓存到磁盘中,也即全量读写。这意味着大部分的新闻数据会反复写入到磁盘中,这样的写入是冗余的,因为前面的这些新闻数据并没有发生改变。

      ​

    • 改进方案:所以优化的方法就是将这些列表数组进行分割,分割成一页一页,每次写入的数据量很小,而且避免了冗余写入的问题。现在的流程变为:①启动时只读取第一批新闻显示在屏幕中;②下拉刷新和上拉加载的时候只把当前服务器返回的一批新闻写入缓存中;③在上拉加载的时候会先查看磁盘中是否有未读的缓存,若有则读取缓存,否则才从服务器下载一批新的文章。

      ​

    • 直观图示:

      按页缓存

      可以看到,优化之前整个新闻列表以及其他配置都在一个文件里,刷新10几次之后文件大小达到2MB,并且随着不断刷新而越来越大;优化之后,其他的配置还是在刚刚的文件中,但是不断增长的新闻数组被分割成一页一页的文件,每一页里面有10多条的新闻数据,同时有一个configure文件保存这些页的信息以及页的顺序。根据测试人员的反馈,进行按页加载缓存优化能减少5%~8%的CPU占用,使用的内存也有一定的下降。还是有很明显的优化效果的。

      ​

  3. 后台线程处理图片

    圆形头像、图片裁圆角等处理可以说是非常常见的需求了,包括从iOS11的系统各处都能看到,整体的页面控件都变得更加圆润了。但是,对图片处理必然是消耗资源的,实现过图片圆角效果的应该都知道,最简单的就是 layer.cornerRadius+layer.masksToBounds 的方式,但是这种做法在tableview中往往会是滚动变得卡顿,因为这种实现方式会触发离屏渲染,屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的,所以离屏渲染往往会造成卡顿(参考:iOS 离屏渲染的研究)。

    那要怎么处理图片呢?可以使用Core Graphics,CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程。我们在项目中实现了一个后台处理图片的框架,核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    NSBlockOperation *transformOperation = [[NSBlockOperation alloc] init];
    [transformOperation addExecutionBlock:^{
    // 此处处理图片
    ...
    dispatch_async(dispatch_get_main_queue(), ^{
    // 主线程设置图片
    [self setImage:transformedImage forState:UIControlStateNormal];
    }
    });
    }];

    ​

    ​

  4. 更加高效的控件

    还可以直接从开源库中选用更加高效的控件替换项目中性能没那么好的控件。项目中将之前的TTTAttributedLabel、M80AttributedLabel全部替换为YYLabel,开启YYLabel的displaysAsynchronously、ignoreCommonProperties属性可以异步绘制文本以及忽略不需要的属性。更加追求性能的话,可以结合第1点的提前布局机制,在提前布局的阶段生成好YYLabel渲染时用到的textLayout,显示的时候直接赋值textLayout就可以了。

    ​

  5. 其他

    还有一些比较微小的优化,对性能可以说没有多大的影响,但是可以在开发阶段稍加留意,养成良好的习惯。

    • 尽量减少视图层级,合并多余的视图。同样以 ZAKER 为例,用户显示时的蓝V标签、达人标签以及楼主图片等几个视图,之前是用不同的view来展示的,优化过程将这几个view合并为一个view,一个view管理这些相似的事物,也可以减少某些相同逻辑的代码。
    • 减少频繁的addSubview、removeSubview,remove之后视图的实例对象会被释放,再add的时候会再次调用初始化函数。可以用hidden属性隐藏不显示的视图。

接下来的方向

  1. 异步绘制

    从开始接触我们就一直在被告知,UIKit的东西是绝对不能在后台线程调用的,一定得在主线程调用,所以主线程也被叫做UI线程。在后台线程调用UIKit的东西有一定几率导致崩溃,或者出现视图不显示、显示错乱等等问题。但是呢,根据刚刚所说的,Core Graphics的那一套东西是线程安全的,所以可以通过Core Graphics在后台将视图渲染到一张图片上,显示的时候在主线程将这张图片设置到相应位置上。Facebook著名的AsyncDisplayKit的核心实现应该也是基于这个原理,接下来的优化可以尝试这个方案。

    ​

  2. Metal

    根据Apple官方说法,Metal框架被设计用来实现两个目标: 3D 图形渲染和并行计算。这两者有很多共同点。它们都在数量庞大的数据上并行运行特殊的代码,并可以在GPU上执行。目前正在研究学习阶段,看项目中是否能利用Metal进行一定的优化。

    ​

  3. APM??

    Application Performance Management(APM):应用程序性能管理, 通过对应用的可靠性、稳定性等方面的监控,进而达到可以快速修复问题、提高用户体验的目的。目前比较有代表性的 APM 产品有:听云、阿里百川、腾讯bugly等,现在也在考虑自己研发一套APM系统,先从比较简单的指标入手,先对卡顿和崩溃这两个指标着手,做的顺利的话再逐步扩展别的指标的检测管理。

以上。

ruanpapa和又吉君写字的地方

(翻译)iOS的Core Text教程:制作一个杂志应用

发表于 2017-07-21 | 分类于 ruanpapa--技术贴 | | 阅读次数

原文链接:https://www.raywenderlich.com/153591/core-text-tutorial-ios-making-magazine-app

杂志,Core Text和大脑!
更新说明:本教程已经由Lyndsey Scott升级为Swift 4和Xcode 9。最初的教程由Marin Todorov所创作。

Core Text是一个底层的文本引擎,当与Core Graphics/Quartz框架配合使用的时候,可以对布局和格式进行细粒度的控制。

随着iOS 7的发布,Apple公司发布了一个名叫Text Kit的高级库,可以用来储存、布局和显示具有各种排版特征的文本。虽然Text Kit在布局文本时不仅强大而且大部分情况下已经足够用了,但是Core Text可以提供更多的控制。例如,如果你想直接使用Quartz的话,那就请用Core Text吧。如果你需要构建你自己的布局引擎的话,Core Text将会帮助你生成字形(glyphs)并且根据互相之间的关系摆放好这些字形,并具有好的排版的所有特性。

本教程将会引导你使用Core Text去创作一本非常简单的杂志应用…给僵尸看的!
呃,僵尸月刊的读者朋友们已经宽容的答应了,只要你本教程认真使用Core Text的话,就不会吃掉你的大脑了…所以呢,你还是尽快开始吧!

说明:要充分读懂本教程,你首先需要了解iOS开发的基础。如果你是iOS开发的新人的话,你应该首先查看本网站的其他教程。

开始

打开Xcode,用Single View Application模板创建一个新的Swift universal project,命名为CoreTextMagazine。

然后,将Core Text框架加到你的工程中:

  1. 单击工程导航器中的工程文件(在左边的导航条上)
  2. 在”General”按钮下,滚动到底部的”Linked Frameworks and Libraries”
  3. 单击”+”按钮然后找到”CoreText”
  4. 选中”CoreText.framework”然后点击”Add”按钮。就这么简单!

现在工程已经配置好了,是时候开始写代码了。

添加一个Core Text View

首先,你将要创建一个自定义的UIView,在这个UIView的draw(_:)方法中将会用到Core Text。

创建一个新的继承于UIView的Cocoa Touch Class file,命名为CTView。打开CTView.swift,然后在

UIKit ``` 语句下面加上下面的代码:
1
2
```Objective-C
import CoreText

然后,将这个自定义的view设置为应用的主视图。打开Main.storyboard,在右边打开Utilities菜单,然后在顶部工具条单击Identity Inspector图标。在Interface Builder的左侧菜单中,选中View。现在在Utilities菜单的Class字段中应该写着UIView。在Class字段输入CTView以子类化主视图控制器的视图,然后点击回车键。

接下来,打开CTView.swift并将被注释掉的draw(_:)方法全都替换成下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1
override func draw(_ rect: CGRect) {
// 2
guard let context = UIGraphicsGetCurrentContext() else { return }
// 3
let path = CGMutablePath()
path.addRect(bounds)
// 4
let attrString = NSAttributedString(string: "Hello World")
// 5
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
// 6
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
// 7
CTFrameDraw(frame, context)
}

让我们一步一步地分析一下代码:

  1. 在视图创建的时候,draw(_:)会自动运行,渲染这个视图的背景图层。
  2. 打开用于绘制的当前图形上下文。
  3. 创建一条用来限定绘图区域的路径,在这个例子中就是整个视图的bounds。
  4. 在Core Text,使用NSAttributedString而不是String或者NSString,保存文本和属性(attributes)。初始化一个”Hello World”的属性字符串。
  5. CTFramesetterCreateWithAttributedString使用提供的属性字符串创建一个CTFramesetter。CTFramesetter会管理你引用的字体和绘图区域。
  6. 通过使CTFramesetterCreateFrame在路径内渲染整个字符串,可以创建一个CTFrame。
  7. CTFrameDraw在给定的上下文中绘制CTFrame。
    这就是你绘制简单文本所需要做的全部了!Build,运行然后查看结果。

噢不…似乎看起来不太对。就像很多的底层API一样,Core Text使用的是Y-flipped坐标系统。更糟糕的是,内容在竖直方向上也翻转了!

添加以下代码到

let context ```语句以修正内容的方向:
1
2
3
4
5
```Objective-C
// Flip the coordinate system
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)

这段代码通过应用变换(transformation)到视图的上下文来将内容翻转。

Build然后运行app。别担心状态栏重叠的问题,你接下来会学到怎样通过约束解决这个问题。

祝贺你的第一个Core Text软件!僵尸们很高兴看到你的进步。

Core Text对象模型

如果你对CTFramesetter和CTFrame感到有点疑惑也是正常的,也是时候说明一下它们了。:]
Core Text对象模型如下所示:

当你提供一个NSAttributedString创建一个CTFramesetter对象实例的时候,一个CTTypesetter的实例对象会自动为你创建用以管理你的字体。接下来你会在渲染文本的时候用到这个CTFramesetter去创建一个或者多个frame。

当你创建了一个frame,你可以为这个frame提供文本的一个subrange去渲染这段文本。Core Text会自动为文本的每一行创建一个CTLine,并为每个具有相同格式的字符创建一个CTRun。举个例子,Core Text只会创建一个CTRun用于同一行中的几个红色的单词,创建一个CTRun用于接下来的纯文本,创建一个CTRun用于粗体段落等等。Core Text创建会根据你提供的NSAttributedString中的属性创建CTRun。此外,上面说到的每一个CTRun对象都可以采用不同的属性,也就是说,你可以很好地控制字距、连字、宽度、高度等。

深入杂志App!

下载并解压the zombie magazine materials。拖拽解压出来的文件夹到你的Xcode工程中。当弹出对话框时,确保Copy items if needed和Create groups选中。

为了创建这个app,你需要对文本应用各种属性。你将要创建一个用标签设置杂志格式的简单文本标记解析器。

创建一个新的Cocoa Touch Class file,命名为MarkupParser,继承于NSObject。

首先,我们快速看一下zombies.txt。看看它是如何在整个文本中包含括号内的格式化标签的。”img src”标签指向杂志的图片,而”font color/face”标签则确定了文本的颜色和字体。

打开MarkupParser.swift然后将它的内容替换为以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import UIKit
import CoreText
class MarkupParser: NSObject {
// MARK: - Properties
var color: UIColor = .black
var fontName: String = "Arial"
var attrString: NSMutableAttributedString!
var images: [[String: Any]] = []
// MARK: - Initializers
override init() {
super.init()
}
// MARK: - Internal
func parseMarkup(_ markup: String) {
}
}

你在这段代码中添加了属性持有字体和文本颜色,设置了它们的初始值。创建了一个变量去持有parseMarkup(_:)生成的属性字符串。还创建了一个数组用来持有定义了尺寸、位置以及从文本中解析出来的图片文件名等信息的键值对。

通常来说,写一个解析器并不是一个轻松的工作,但是本教程所实现的解析器将会非常简易,只提供开放标签的支持,也就意味着一个标签会决定紧随着这个标签的文本的样式,直到找到一个新的标签。这段文本的标记如下所示:

1
2
These are <font color="red">red<font color="black"> and
<font color="blue">blue <font color="black">words.

输出如下所示:

将以下代码加到

``` 方法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
```Objective-C
//1
attrString = NSMutableAttributedString(string: "")
//2
do {
let regex = try NSRegularExpression(pattern: "(.*?)(<[^>]+>|\\Z)",
options: [.caseInsensitive,
.dotMatchesLineSeparators])
//3
let chunks = regex.matches(in: markup,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSRange(location: 0,
length: markup.characters.count))
} catch _ {
}

  1. attrString初始为空,但是最后会包含解析出来的标记。
  2. 这个正则表达式,匹配了紧跟着这些标签的文本块。它就好像在说:“去查看字符串直到你找到一个开头的括号,然后查看字符串直到你找到一个结束的括号(或者文档的末尾)”。
  3. 搜索regex匹配到的整个标记范围,然后生成一个NSTextCheckingResult的数组。

想要学习更多有关正则表达式的内容,访问NSRegularExpression Tutorial吧。

现在你已经解析了所有的文本并将所有格式化的标签都放进了chunks中,你要做的就是遍历chunks数组去生成对应的属性字符串。

但在那之前,你是否留意到matches(in:options:range:)方法是如何接受一个NSRange作为参数的吗?在你应用NSRegularExpression到你的标记String的时候会有大量NSRange到Range的转化。Swift已经成为了我们所有人的好帮手,所以它值得帮助。

还是在MarkupParser.swift中,将下面的extension加到文件的最后面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MARK: - String
extension String {
func range(from range: NSRange) -> Range<String.Index>? {
guard let from16 = utf16.index(utf16.startIndex,
offsetBy: range.location,
limitedBy: utf16.endIndex),
let to16 = utf16.index(from16, offsetBy: range.length, limitedBy: utf16.endIndex),
let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self) else {
return nil
}
return from ..< to
}
}

上面这个函数将以NSRange表示的字符串的起止索引转换为了String.UTF16View.Index格式,即UTF-16字符串中的位置(position)集合,然后将每个String.UTF16View.Index格式转换为String.Index格式。String.Index格式在组合时,会生成Swift的范围格式:Range。只要索引是有效的,这个函数就会返回原始NSRange格式对应的Range格式。

现在是时候回头处理文本和标签数组了。

在parseMarkup(_:)函数中添加一下代码到

chunks ``` 到下面(在do循环语句块中):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
```Objective-C
let defaultFont: UIFont = .systemFont(ofSize: UIScreen.main.bounds.size.height / 40)
//1
for chunk in chunks {
//2
guard let markupRange = markup.range(from: chunk.range) else { continue }
//3
let parts = markup.substring(with: markupRange).components(separatedBy: "<")
//4
let font = UIFont(name: fontName, size: UIScreen.main.bounds.size.height / 40) ?? defaultFont
//5
let attrs = [NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font] as [NSAttributedStringKey : Any]
let text = NSMutableAttributedString(string: parts[0], attributes: attrs)
attrString.append(text)
}

  1. 循环chunks数组。
  2. 获取当前NSTextCheckingResult的range,展开Range并且只要range存在就继续执行以下的语句块。
  3. 将chunk用”<”分割成几部分。第一部分包含了杂志的文本而第二部分包含了对应的标签(如果标签存在的话)。
  4. 用fontName生成字体,现在的默认字体是”Arial”,并且根据设备屏幕创建了字体的大小。假如fontName不能产生有效的UIFont的话,将默认字体设为当前字体。
  5. 创建字体格式的字典,将其应用于parts[0]以创建属性字符串,然后将该字符串添加到结果字符串后面。

将下面用来处理”font”标签的代码插到attrString.append(text)下面:

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
// 1
if parts.count <= 1 {
continue
}
let tag = parts[1]
//2
if tag.hasPrefix("font") {
let colorRegex = try NSRegularExpression(pattern: "(?<=color=\")\\w+",
options: NSRegularExpression.Options(rawValue: 0))
colorRegex.enumerateMatches(in: tag,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in
//3
if let match = match,
let range = tag.range(from: match.range) {
let colorSel = NSSelectorFromString(tag.substring(with:range) + "Color")
color = UIColor.perform(colorSel).takeRetainedValue() as? UIColor ?? .black
}
}
//5
let faceRegex = try NSRegularExpression(pattern: "(?<=face=\")[^\"]+",
options: NSRegularExpression.Options(rawValue: 0))
faceRegex.enumerateMatches(in: tag,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in
if let match = match,
let range = tag.range(from: match.range) {
fontName = tag.substring(with: range)
}
}
} //end of font parsing

  1. 如果parts数组元素少于2个,则跳过这个循环语句块。否则的话,将parts的第二部分存为tag。
  2. 如果tag以”font”开始则创建一个正则表达式去匹配字体的”color”值,然后用这个正则去枚举匹配到的tag中的”color”值。在这种情况下,只应该有一个匹配到的颜色值。
  3. 如果enumerateMatches(in:options:range:using:)函数返回标签的一个有效的match和一个有效的range的话,就去搜索出指示值,接着用这个颜色值生成一个UIColor的selector。执行这个selector所返回得到的color(如果存在的话)会赋值到你的类的color属性上,如果返回的color不存在的话,color属性会被赋值为black。
  4. 同样的,创建一个正则表达式去处理文本中字体的”face”值。如果匹配到一个”face”值,则将类的fontName属性设置为匹配的”face”值。

干得漂亮!现在parseMarkup(_:)函数已经可以获取文本中的标记并生成一个对应的NSAttributedString了。

现在也是时候把你的app喂给一些僵尸了!我的意思是,喂一些僵尸给你的app… 也就是说,(开始处理)zombies.txt。

事实上,显示出被赋予的内容才是UIView的职责所在,而不是去加载内容。打开CTView.swift然后将下面代码添加到draw(_:)方法之前:

1
2
3
4
5
6
7
// MARK: - Properties
var attrString: NSAttributedString!
// MARK: - Internal
func importAttrString(_ attrString: NSAttributedString) {
self.attrString = attrString
}

接下来,将

attrString = NSAttributedString(string: "Hello World") ``` 从draw(_:)函数中删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这段代码中你创建了一个实例变量持有属性字符串和一个函数以便app的其他地方可以设置这个属性字符串。
然后,打开ViewController.swift并将以下代码添加到viewDidLoad()中:
```Objective-C
// 1
guard let file = Bundle.main.path(forResource: "zombies", ofType: "txt") else { return }
do {
let text = try String(contentsOfFile: file, encoding: .utf8)
// 2
let parser = MarkupParser()
parser.parseMarkup(text)
(view as? CTView)?.importAttrString(parser.attrString)
} catch _ {
}

然后一步步的过一下这段代码:

  1. 从zombie.txt文件中加载文本。
  2. 创建一个新的解析器,传入文本作为参数,然后将返回的属性字符串赋给ViewController的CTView。

Build并且运行这个app!

简直太棒了!归功于这50多行解析代码你可以轻松的用文本文件持有你杂志app的内容了。

基本的杂志布局

如果你认为僵尸新闻的每月杂志只能全塞在一个可怜的页面中,那么你就错了!幸运的是,Core Text在文本列布局时相当有用,因为CTFrameGetVisibleStringRange可以告诉给定frame的情况下显示多少文本才是合适的。也就是说,你可以创建一列文本,当这一列塞满文本之后,你可以知道并开始新的一列。

就本app而言,你需要先打印出列,然后集列成页,再集页成文。未免冒犯这些亡灵,所以。。。你还是尽快把把你的CTView改成继承于UIScrollView。
打开CTView.swift然后将

CTView ``` 一行改成以下代码:
1
2
```Objective-C
class CTView: UIScrollView {

看到了吗,僵尸老爷?现在这个app已经支持永恒不死了!对的,行、滚动以及翻页现在都是可用的了。

到现在为止,你已经在draw(_:)方法里创建了framesetter和frame了,不过由于你有很多不同格式的文本列,所以最好还是创建一个独立的实例表征所述的文本列。

创建一个新的名为CTColumnView的Cocoa Touch Class file,继承于UIView。
打开CTColumnView.swift并添加下列初始代码:

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
import UIKit
import CoreText
class CTColumnView: UIView {
// MARK: - Properties
var ctFrame: CTFrame!
// MARK: - Initializers
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
required init(frame: CGRect, ctframe: CTFrame) {
super.init(frame: frame)
self.ctFrame = ctframe
backgroundColor = .white
}
// MARK: - Life Cycle
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
CTFrameDraw(ctFrame, context)
}
}

跟开始在CTView里做的工作一样,这段代码生成了一个CTFrame。自定义的初始化函数init(frame:ctframe:)设置了:

  1. 这个视图的frame。
  2. 在当前上下文中绘制的CTFrame。
  3. 以及将这个视图的背景颜色设置为白色。

接下来,创建一个新的swift文件命名为CTSettings.swift,用来持有你的文本列的设置。
将CTSettings.swift的内容替换为以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import UIKit
import Foundation
class CTSettings {
//1
// MARK: - Properties
let margin: CGFloat = 20
var columnsPerPage: CGFloat!
var pageRect: CGRect!
var columnRect: CGRect!
// MARK: - Initializers
init() {
//2
columnsPerPage = UIDevice.current.userInterfaceIdiom == .phone ? 1 : 2
//3
pageRect = UIScreen.main.bounds.insetBy(dx: margin, dy: margin)
//4
columnRect = CGRect(x: 0,
y: 0,
width: pageRect.width / columnsPerPage,
height: pageRect.height).insetBy(dx: margin, dy: margin)
}
}

  1. 这些属性将用来确定page的约束(在这个教程中约束默认是20)、在每一个页中文本列的数量、包含文本列的每一页的frame以及每一页中的每一个文本列的frame。
  2. 由于这本杂志的服务对象是拿着iPhone和iPad的僵尸们,iPad上显示两列,iPhone上显示一列,这样的列数对于任意的屏幕尺寸来说都是适宜的。
  3. 用约束的大小计算出来的pageRect去布局页面的边界。
  4. 用每一页的文本列数量划分pageRect的宽度并配合约束去计算出columnRect。

打开CTView.swift,将文件中的内容整个替换成下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import UIKit
import CoreText
class CTView: UIScrollView {
//1
func buildFrames(withAttrString attrString: NSAttributedString,
andImages images: [[String: Any]]) {
//2
isPagingEnabled = true
//3
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
//4
var pageView = UIView()
var textPos = 0
var columnIndex: CGFloat = 0
var pageIndex: CGFloat = 0
let settings = CTSettings()
//5
while textPos < attrString.length {
}
}
}

  1. buildFrames(withAttrString:andImages:)函数会创建并添加CTColumnView到滚动视图。
  2. 运行滚动视图的翻页行为;也就是说,用户不管什么时候停止滚动,滚动视图都能卡到位置使得同一时间只有一个完整的页面在显示。
  3. CTFramesetter framesetter会为创建的每一列CTFrame提供属性字符串。
  4. UIView pageView会作为每个页面文本列对应的子视图的容器;textPos会持续跟踪接下来的文字;columnIndex会持续跟踪当前列;pageIndex会持续跟踪当前页;同时settings能让你访问app的约束尺寸、每页的列、页的frame和列frame的设置等。
  5. 你将要遍历attrString然后逐列布局文本直到当前文本的位置到了最末尾。

是时候开始遍历attrString了。把下列代码加进

textPos < attrString.length { ``` 里面:
1
2
3
4
5
6
7
8
9
10
11
12
13
```Objective-C
//1
if columnIndex.truncatingRemainder(dividingBy: settings.columnsPerPage) == 0 {
columnIndex = 0
pageView = UIView(frame: settings.pageRect.offsetBy(dx: pageIndex * bounds.width, dy: 0))
addSubview(pageView)
//2
pageIndex += 1
}
//3
let columnXOrigin = pageView.frame.size.width / settings.columnsPerPage
let columnOffset = columnIndex * columnXOrigin
let columnFrame = settings.columnRect.offsetBy(dx: columnOffset, dy: 0)

  1. 如果用每页列数划分的列索引等于0,也就能说明这是所在页的第一列,那么就创建新页视图持有这些列。为了设置这些列的frame,需要获取算好约束的setting. pageRect按照当前页面索引乘以屏幕宽度来对其原点计算偏移量。这样才能在翻页滚动视图内部保证杂志每一页都在前一页的右边。
  2. 自增pageIndex。
  3. 通过settings.columnsPerPage将pageView的宽度除以第一列的x原点,将该列乘以列索引以获得列偏移量;然后通过采用标准columnRect并通过columnOffset将其x原点偏移来创建当前列的frame。

接下来把下列代码加到columnFrame初始化方法的下面:

1
2
3
4
5
6
7
8
9
10
11
12
//1
let path = CGMutablePath()
path.addRect(CGRect(origin: .zero, size: columnFrame.size))
let ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, nil)
//2
let column = CTColumnView(frame: columnFrame, ctframe: ctframe)
pageView.addSubview(column)
//3
let frameRange = CTFrameGetVisibleStringRange(ctframe)
textPos += frameRange.length
//4
columnIndex += 1

  1. 创建一个CGMutablePath大小的列,然后从textPos开始在合适的范围内渲染足够多的文本到CTFrame中。
  2. 用CGRect类型的columnFrame和CTFrame类型的ctframe创建一个CTColumnView并将这一列加到pageView上。
  3. 用CTFrameGetVisibleStringRange(_:)函数计算用列限制的文本的范围,然后用这个计算出来的范围自增textPos的值。
  4. 在遍历到下一列之前将column的索引值加1。

最后在遍历完成之后设置好滚动视图的size:

1
2
contentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width,
height: bounds.size.height)

通过将内容大小设置为屏幕宽度乘以页数,僵尸先生现在可以滚动杂志到最后了。

打开ViewController.swift,将原先的下列代码:

1
(view as? CTView)?.importAttrString(parser.attrString)

替换为下列代码:

1
(view as? CTView)?.buildFrames(withAttrString: parser.attrString, andImages: parser.images)

Build并在iPad上运行应用。检查一下双列布局!在页面间左右拖动试试。看起来棒极了!

你已经有了按列排列且格式化好的文本了,但是你还忘了图片呢。用Core Text绘制图片并不是那么简单,Core Text毕竟是一个文本处理框架,但是呢在你刚刚创建的标记解析器的帮助之下,添加图片也没有那么糟糕。

用Core Text绘制图片

虽然Core Text不能直接绘制图片,但是作为一个布局引擎,它可以为图片留开空间。通过设置为CTRun的delegate,你可以确定CTRun的ascent空间、decent空间和宽度。就像下面这样:

当Core Text遇到一个设置了CTRunDelegate的CTRun,它就会询问delegate:“我需要留多少空间给这块的数据”。通过在CTRunDelegate中设置这些属性,您可以在文本中给图片留开空位。

首先让(解析器)支持”img”标签。打开MarkupParser.swift然后找到”} //end of font parsing”语句。并将下面代码加到后面:

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
//1
else if tag.hasPrefix("img") {
var filename:String = ""
let imageRegex = try NSRegularExpression(pattern: "(?<=src=\")[^\"]+",
options: NSRegularExpression.Options(rawValue: 0))
imageRegex.enumerateMatches(in: tag,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in
if let match = match,
let range = tag.range(from: match.range) {
filename = tag.substring(with: range)
}
}
//2
let settings = CTSettings()
var width: CGFloat = settings.columnRect.width
var height: CGFloat = 0
if let image = UIImage(named: filename) {
height = width * (image.size.height / image.size.width)
// 3
if height > settings.columnRect.height - font.lineHeight {
height = settings.columnRect.height - font.lineHeight
width = height * (image.size.width / image.size.height)
}
}
}

  1. 如果tag是以”img”开头的话就用正则去查找图片的”src”值,比如图片的filename。
  2. 设置图片宽度为列的宽度同时在保证图片宽高比的情况下设置图片高度。
  3. 如果图片的高度高过了列的高度就将列的高度设置为图片高度并减少图片的宽度以维持图片的宽高比。

接下来,将下面代码加到紧随

let image ``` 语句块后面的地方:
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
```Objective-C
//1
images += [["width": NSNumber(value: Float(width)),
"height": NSNumber(value: Float(height)),
"filename": filename,
"location": NSNumber(value: attrString.length)]]
//2
struct RunStruct {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
}
let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
extentBuffer.initialize(to: RunStruct(ascent: height, descent: 0, width: width))
//3
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in
}, getAscent: { (pointer) -> CGFloat in
let d = pointer.assumingMemoryBound(to: RunStruct.self)
return d.pointee.ascent
}, getDescent: { (pointer) -> CGFloat in
let d = pointer.assumingMemoryBound(to: RunStruct.self)
return d.pointee.descent
}, getWidth: { (pointer) -> CGFloat in
let d = pointer.assumingMemoryBound(to: RunStruct.self)
return d.pointee.width
})
//4
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
//5
let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedStringKey): (delegate as Any)]
attrString.append(NSAttributedString(string: " ", attributes: attrDictionaryDelegate))

  1. 添加包含图片尺寸、文件名和文本位置的字典到images数组中。
  2. 定义RunStruct结构图去持有用来描述空格的属性。然后初始化一个包含RunStruct的指针,这个结构体的ascent等于图片的高度,宽度等于图片的宽度。
  3. 创建一个CTRunDelegateCallbacks返回ascent、decent和宽度。
  4. 用CTRunDelegateCreate生成一个绑定了callbacks和数据的委托实例。
  5. 创建一个包含委托实例的属性字典,然后添加单个空格到attrString末尾,属性字典其实就是用来持有这些占位空格的位置和大小信息的。

现在MarkupParser可以处理”img”标签了,你需要调整CTColumnView和CTView去渲染图片。

打开CTColumnView.swift。把下列用来持有列的图片和图片的frame的代码加到

ctFrame:CTFrame! ``` 语句后面:
1
2
```Objective-C
var images: [(image: UIImage, frame: CGRect)] = []

再然后就是把下列代码加到draw(_:)函数的后面:

1
2
3
4
5
6
for imageData in images {
if let image = imageData.image.cgImage {
let imgBounds = imageData.frame
context.draw(image, in: imgBounds)
}
}

在这段代码中你遍历了每一张图片然后把图片绘制到上下文它正确的frame内。

然后打开CTView.swift并添加下面这个属性到类的顶部:

1
2
// MARK: - Properties
var imageIndex: Int!

imageIndex会持续追踪当前的图片索引在你绘制CTColumnView时。

接下来,把下面一行代码添加到buildFrames(withAttrString:andImages:)函数上方:

1
imageIndex = 0

这标志着images数组的第一个元素。

然后添加下述attachImagesWithFrame(_:ctframe:margin:columnView)函数到buildFrames(withAttrString:andImages:)函数后面:

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
func attachImagesWithFrame(_ images: [[String: Any]],
ctframe: CTFrame,
margin: CGFloat,
columnView: CTColumnView) {
//1
let lines = CTFrameGetLines(ctframe) as NSArray
//2
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), &origins)
//3
var nextImage = images[imageIndex]
guard var imgLocation = nextImage["location"] as? Int else {
return
}
//4
for lineIndex in 0..<lines.count {
let line = lines[lineIndex] as! CTLine
//5
if let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun],
let imageFilename = nextImage["filename"] as? String,
let img = UIImage(named: imageFilename) {
for run in glyphRuns {
}
}
}
}

  1. 获取一个ctframe的CTLine对象的数组。
  2. 用CTFrameGetOrigins去复制ctframe的行初始点坐标到origins数组中。通过设置长度为0的range,CTFrameGetOrigins将知道要遍历整个CTFrame。
  3. 设置nextImage以包含当前图像的属性数据。如果nextImage包含图像的位置,请将其展开并继续;否则,早点返回函数。
  4. 遍历下一行文本。
  5. 如果这一行的字形、文件名和图片文件名都存在的话,则遍历这一行的字形。

然后呢,添加下列代码到字形的for-loop语句块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1
let runRange = CTRunGetStringRange(run)
if runRange.location > imgLocation || runRange.location + runRange.length <= imgLocation {
continue
}
//2
var imgBounds: CGRect = .zero
var ascent: CGFloat = 0
imgBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, nil, nil))
imgBounds.size.height = ascent
//3
let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil)
imgBounds.origin.x = origins[lineIndex].x + xOffset
imgBounds.origin.y = origins[lineIndex].y
//4
columnView.images += [(image: img, frame: imgBounds)]
//5
imageIndex! += 1
if imageIndex < images.count {
nextImage = images[imageIndex]
imgLocation = (nextImage["location"] as AnyObject).intValue
}

  1. 如果当前字形的范围不包含下一个图像,则跳过循环的其余部分。否则,在此渲染图像。
  2. 使用CTRunGetTypographicBounds计算图像宽度,并将高度设置为ascent。
  3. 用CTLineGetOffsetForStringIndex获取线的x偏移,然后将其添加到imgBounds的起点坐标。
  4. 将图像及其frame添加到当前的CTColumnView。
  5. 增加图像索引。如果images[imageIndex]是一个图片,则更新nextImage和imgLocation,以便它们引用下一个图像。

好了!干得漂亮!基本完成,还剩下最后一步。

在buildFrames(withAttrString:andImages:)内部的pageView.addSubview(column)上面添加以下代码用以附加图像(如果图像存在的话):

1
2
3
if images.count > imageIndex {
attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
}

Build并且运行到iPhone和iPad上:

恭喜你!因为感激你所做的这些辛苦的工作,僵尸先生决定不吃你的大脑了!

何去何从

在这里查看完整的项目。

正如介绍中所述,Text Kit通常可以替代Core Text;所以尝试用Text Kit编写同一个教程吧,比较一下两者有什么不同。也就是说,这堂Core Text课程不会白学!
Text Kit提供免费桥接(toll free bridging)到Core Text,因此你可以根据你的需要轻松地在框架之间进行转换。

有任何问题,意见或建议?加入论坛讨论吧!

ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之六

发表于 2017-05-25 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

从苏州回来之后,迎来了第一个比较重要的日子。

papa突然跟我说!!!

“我星期六请你吃大餐啊!!!”

完全反应不过来的又吉君

完全反应不过来的又吉君


就是!!!5.20!!!

然而我还是一脸糊涂:

一直都没在意过的节日….突然开始还真像那么回事!主要是:

有!得!吃!

嘿嘿嘿嘿

嘿嘿嘿嘿


于是红包雨一下!!papa和我都有得吃了!!!

但是 !!

papa竟然和舍舍过二人世界去了!午饭还是舍舍请他吃的!!!!!

嗯!我的papa可能一直是个 🤞

sad!(┐「ε:)

悲伤的我脑洞一开,决定实施秀恩爱计划

  • 异地恋一定要秀一秀的!(可惜papa不爱秀)
  • 于是…….
  • 吃的东西经过我的手都变成这样了!!!

​

徐州的虾+广州的麻辣香锅

徐州的虾+广州的麻辣香锅


酸奶酪+奶茶

酸奶酪+奶茶



绿健奶业+蜜雪冰城

绿健奶业+蜜雪冰城


papa呀!!超级谢谢你噢!!你要一直这样!!!

不跟我一起吃至少也要让我吃!!!

吃!!

吃!

  • 身体和灵魂必须有一个在路上!!

    。

    。

    。

    。

    。

    。

    。

    。

    。

    。

    。

    。

    要么旅行!要么边旅行边吃!!




by:又吉君

ruanpapa和又吉君写字的地方

短暂而快乐--苏州行

发表于 2017-05-15 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

倒计时45天…..似乎还是遥不可及的事情,日常挑逗我的又吉宝宝(づ ̄3 ̄)づ╭❤~
倒计时30天……还一个月呐O__O “…该写代码写代码,该吃饭吃饭
倒计时14天……完全没办法压抑自己的激动了,每天和又吉宝宝憧憬着到了苏州要玩什么吃什么,各种叮嘱对方带这个带那个的,连到时候一起看《虫师》都决定好了
倒计时7天……反而冷静了下来,两个人都少了提及苏州旅游的话题,约定好这星期要一起认真学习认真工作,内心却为即将到来的见面而翻腾
倒计时1天……还写个毛的代码啊,一天都没写够10行代码,可惜又吉宝宝整整一天的课,我这满腔的兴奋没地方诉说/(ㄒoㄒ)/~~

终于熬到了下班了!!!三步并作两步冲回宿舍背上我的小书包,奔着机场就去了。一路上,无视因为放假堵塞的地铁站,无视闷热的天气,无视自己的第一次坐飞机。苏州,我来啦!!!

行程一览:天河–>白云机场–>硕放机场–>无锡火车站–>苏州火车站

超级欢脱

超级欢脱


无锡火车站外左等右等,终于等来了载着我朝思暮想的又吉宝宝的火车。踏上第4节车厢一眼就看到座位上探头探脑寻找着我的小又吉,赶快戴上帽子压低头附身跟着前面的人走向那个朝思暮想了几个月的那个家伙。一屁股坐下又吉宝宝旁边,这小傻瓜竟然第一句话是问我要不要吃她带的枣糕(喵喵喵???汪汪汪???你就这开场白吗???) 这货竟然敢对我那么冷淡,一把握住她的手,不由分说牵的死死的:这才像话嘛,劳资山长水远的飞奔而来,可不得好好牵牵小手啊吼!
但是,还没坐几分钟,位置的主人就来了,只好起身站在了走道上,她呢,身子一侧又睡了!!!我这个暴脾气啊:你就这么冷淡吗,看不到我有位置都不回去坐,站在这儿不就是为了看你吗?你还这么心大?待会儿下车你就知道错了!口亨!!!

气死我啦

气死我啦


28分钟的火车我就跟站了28小时一样,明明两步外坐着自己心爱的女人(虽然睡得🐖一样),自己却没办法触碰到她。/(ㄒoㄒ)/~总算捱到了下车,发现我出省连网都没得上了,于是我全程像个考拉一样贴在了宝宝身上,又吉宝宝找肯德基准备待到天亮,而我的关注点就全在牵着又吉宝宝的手上~摸摸手~搭搭肩膀~掐掐脸~管她去哪儿呢,爱去哪去哪!

清晨的苏州真的是很舒适宜居,爱人在旁,清风拂面,绿树相伴,天空蓝蓝的,空气凉凉的,好奇的盯着这江南每一处建筑,每一个过去的人,通宵没睡的倦意也一扫而光,贪婪的享受这闲适的一切。步行来到早已计划好的平江路,果真名不虚传,俩人都赞不绝口,背阴像隔开了旭日初升的热气,两旁的店面都满满的江南水乡风格。俩人在沿河的石阶上上坐了好久好久,完全没有第一次见的陌生感,聊聊平江路,聊聊待会儿的行程,聊聊喝茶,聊聊坐飞机坐车的事情,一点都不想走了。接着去了闻名遐迩的拙政园,真的是人山人海啊,游完一遍俩人一致认为这里言过其实了…..

来到酒店顺利办了入住,进入房间的时候终于能松一口气了,顾不上放下行李,紧紧抱住了眼前这个人,情不自禁的亲吻她。简单的洗漱过后终于能躺下了,瞬间倦意袭来,抱着软软的又吉宝宝睡了过去,就像抱住了全世界,俩人相拥而眠,然后……就睡死过去啦:-D

宇宙第一可爱的又吉宝宝

醒来的第一眼就能看到心爱的人,看着熟睡的她,轻轻的吐着气呼吸,长长的眼睫毛一动一动的,像只乖巧的小猫咪,我情不自禁心上像开了一朵花,轻轻的亲了她糖果味的嘴唇仿佛拥有了全世界, 凑过去抱住了她又睡了过去……订了喜欢吃的海鲜外卖,总是忍不住看着她吃东西,心都要萌化了好吗?怎么有这么可爱的人!!

心灵手巧的又吉宝宝(插叙)

吉:“papapapapa~你看你看,这个日本的羊毛毡狗系列好萌啊啊啊啊啊!!!!”
pa:“买买买!喜欢就买!!!”
俩人一通找,发现正版一个竟然要几百块钱。囧,买个鬼啊买,根本买不起好吗?(话说不就一个手工羊毛毡制品吗,卖那么贵干嘛哦!)
吉:“可我真的喜欢嘛……诶不如我自己来做吧”?
pa:“呃……你行吗”?(手残星人表示不可思议)
然后我就华丽丽的忘了这一茬了(`・ω・´)
没想到她竟然真的买了材料,花了一个下午加晚上给我扎了一个羊毛毡出来,还带到苏州来送给我(感动cry)
完全不输给正版的好吗!!!看这胖乎乎的身体,看这蠢萌的脸,看这傲娇的……屁股,哈哈哈哈啊哈哈
羊毛毡小狗狗

羊毛毡小狗狗


小拖延的又吉宝宝

三催四请让她去洗澡都不听话,我走过去一把熊抱起来,扔到床上:“立刻马上找你换洗的衣服”!然后一把抱起丢到浴室:“你!立刻洗澡!! 别废话!!!” 她悻悻地关上门洗澡,一脸不甘愿的表情我就知道她肯定还想再玩会儿。没门!小样儿,还治不了你了!(╬ ̄皿 ̄)=

毫不客气的又吉宝宝

吉:“papa,我这脚好痛噢(ಥ_ಥ)你帮我揉一下~” 我还在想开口问为什么脚痛呢。啪!一只大白腿就架在了我肚皮上,脚趾都快碰到我嘴了。好气又好笑٩(๑❛ᴗ❛๑)۶ 揉揉揉,这三天尽给她揉脚了(๑¯∀¯๑)



欢乐的时光总是特别短暂,转眼到了该分别的时候(;´༎ຶД༎ຶ`) 浑浑噩噩的回到了广州,满满的不舍得……不过呢,下一次的行程也安排好了,我又可以开始倒数啦~(≧≦)/~啦啦啦




by:又吉君的ruanpapa

ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之五

发表于 2017-04-26 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

ruanpapa和又吉君的日常

感觉好久没有更新了,主要是零零碎碎一些日常不太好写。 想着,既然如此零碎!那就写一篇零零碎碎的日常好了,反正就我和papa两人看。( ̄^ ̄)

先讲述一些ruanpapa的强迫症

我家papa呢,直男到不能再直了。as is known to all,所有直男都有那么点“硬邦邦”的原则,比如:小洁癖,鞋子一定要放鞋架上,不允许整整齐齐的东西有一丝错位等等等等等,要不然就要叽叽咕咕一大堆。

“papa的强迫症超级好笑噢!”

  • 一次,他问我要课表,然后我就坦然的给他看我的课有多满!!!本以为他要安慰一下我,然后再哄哄我没事没事课多好好学就好了……我都准备好撒个娇,结果,他就来了这么一句!“你能不能把左上角的小红点去掉,再给我发一份”
    (╯‵□′)╯︵┻━┻
    ┻━┻︵╰(‵□′)╯︵┻━┻
    老子日了你的狗!
    课程表不起眼的小点

课程表不起眼的小点

  • 他某次很烦恼的跟我讲:
    -“宝宝,我找了三四个壁纸软件都找不到我想要的壁纸。”
    -“你想要什么样的”
    -“就是那种能不被图标遮住的….被图标遮住的壁纸我看着难受”
    -“…………”
    “我干脆给你做一个吧..”
    这就是他不爽的理由

    这就是他不爽的理由

  • 于是我就给他做了一张。

    _(┐「ε:)_

    (┐「ε:)

    ​

    我tm有什么办法!!

    我tm有什么办法!!

    ​

    老子有什么办法!!!!鼠标手绘!!你让我怎么样!!!ಥ_ಥ

  • 然后嘛!我就一直给他做壁纸了….不能挡着图标的壁纸…… 有谁也有图标强迫症也可以拿去用好了……
    小绿

    小绿

    ​

    小蓝

    小蓝

“淘宝卖家要气死了”

  • 我把最喜欢的一支笔弄丢了,于是在淘宝找了半天,就想要一毛一样的,然后papa就很负责任的帮我一本正经的找,由于随机发颜色,所以很苦恼,就一家一家的问可不可以指定颜色….于是,找到一家!!!!
    嘿嘿嘿

    嘿嘿嘿

    ​

    ​

  • 哈哈哈哈哈哈!说完那句之后,这个淘宝卖家就没再回复一句…..快递也没告诉我发啥….但是我还是买了哈哈哈哈哈哈哈哈哈哈哈

“去你妈的六碗饭!”

  • 前段时间和其他两个小伙伴去吃午饭,吃了个鸡公煲,三人一共吃了八碗饭……鸡公煲小哥哥上饭的时候都卧槽了好大一声….那天是早上三个人都起晚了赶去上课没吃早餐,结果饿得一塌糊涂……

  • 觉得很好玩,每个人吃了两碗多点,就和papa讲了一下觉得好笑,结果这小讨厌“污蔑我”吃了六碗!!!!!!

    去你妈的六碗饭

    去你妈的六碗饭

    ​

  • 不过养我的预算是应该 提 高 了

  • 应该 提 高 了

  • 提 高 了

  • 提 高

  • 高

  • 耶!✌️

  • 我是又吉君! 一个需要提高预算的孩子!!

汪~

汪~




by:又吉君

ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之四

发表于 2017-04-18 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

万恶之源

又吉君:papa,今晚我不能陪你,我得做个Flash课件……

我:纳尼?????Flash这么out的东西咱别用了嘛,又耗电又难看的

又吉君:没办法啊,我也没打算参加的,老师非要我们每一个人交作品,算期末分数的……

我:那我不打扰你了,你要乖乖的做完噢(满怀着对又吉君的想念我一个人睡了)

万万没想到,第二天我起来发现又吉君竟然熬夜到了2点钟,而且还有好几个地方都有问题:按钮没响应、场景跳转不正常…..桥豆麻袋,这些听起来怎么像是代码的东西。于是在拯救又吉君于水深火热中的坚定信念以及程序猿的强烈自尊的驱使下,我果断接下了这个光荣而艰巨的任务!

假装标题

这任务是接下来了,海口也夸下了。可是一打开Adobe Flash Professional CS6我就懵逼了:这他喵的都是什么鬼啊

一脸懵逼

一脸懵逼

我这自己的Flash作业还是网上抄的呢,上课的时候都在玩手机,而且这上古时代的ActionScript语法简直逆天的难用,难怪乔老爷子10几年前就断定Flash命不久矣了。我的天老爷,难道这次要翻车了?

又吉君:papa,你看一下main场景的那个按钮为什么没有响应啊,明明跟其他按钮一样的代码,就是没有反应,你知道怎么弄的吧

我:那当然知道啦,也不看看我是谁!(内心OS:Excuse me?场景是什么鬼Σ(☉▽☉”a 代码?这个还能用代码控制?这么神奇的吗 !!!∑(゚Д゚ノ)ノ

又吉君:噢再帮我插一个视频到flash里,谢谢papa~

我:谢什么谢,这么简单我三下五除二就搞定了,我做出来你亲我一下就行了(内心OS:视频?那不是还要播放暂停音量控制一大堆的咯…..)

没办法,自己吹的牛皮,跪着也要吹完。于是Google+百度、中文+英文搜索:Flash场景快捷键是什么、insert video in flash、flash如何内嵌视频、1+1=?……等等一系列的词条,外加各种硬编码、magic code(希望部门老大永远不会看到这些代码,不然我肯定要被开除了),总算是把又吉君的所有要求都完成了✺◟(∗❛ัᴗ❛ั∗)◞✺

皆大欢喜

事情发展到这里并不能算完,这个逼装的只能算及格水平,算不得一个高质量的逼,于是:

我:拿去吧,以后这么简单的东西早点给我嘛,省的你自己在那里弄那么久,心疼死我了

又吉君:哎呀!你都做好了吖!好棒啊你,mua~


迷妹又吉君

迷妹又吉君


我:唉,没挑战啊这任务,配不上我的身价啊

又吉君:么么么,爱死你啦~

总算装了一个满意的逼,我人生装过无数的逼,但是最满意的,无疑是这一个(逃~




by:又吉君的ruanpapa

ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之三

发表于 2017-04-11 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

ruanpapa和又吉君的日常之三

前几天周末,我闲着没事画了一下水彩画。去年说要学一下水彩画,到今年才练了两次手˚✧₊⁎❝᷀ົཽ≀ˍ̮ ❝᷀ົཽ⁎⁺˳✧༚

  • 骚逼 的秀一下:

又吉君的第二次水彩

​

又吉君的第二次水彩


(没有纸…拿的素描纸画的….)

​

于是,ruanpapa叫我教他画画

哈哈哈哈哈哈哈哈哈

  • 我就让他自己先瞎画,随意涂鸦一下
    好奇的ruanpapa

    好奇的ruanpapa


  • 于是,我就给他做了个示范,在相册里随意选了几张图片,在微信里编辑起来了。
    如下图:
    又吉君有剧情的示范

    又吉君有剧情的示范


  • 然后……哈哈哈哈哈哈哈哈哈哈哈哈,他竟然画成这个样子!!!!!!!!太可爱了!!(〃ω〃)
    ruanpapa 的涂鸦

    ruanpapa的涂鸦


  • 哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈!!!!!!

  • 虽然现在画风这么可爱,但是想想以后可能还会在博客里看到papa的进步噢!!!感觉棒棒的呢!!!(・ω・)ノ

  • papa你要努力成为我这样的灵魂画师噢!!哈哈哈哈哈啊哈哈哈哈哈哈

  • 我是又吉君,一个集ruanpapa和灵魂画手于一身的大可爱!

    喵~

喵~




by:又吉君

123
ruanpapa & 又吉君

ruanpapa & 又吉君

ruanpapa和又吉君写字的地方

25 日志
3 分类
12 标签
GitHub
© 2018 ruanpapa & 又吉君
由 Hexo 强力驱动
主题 - NexT.Muse