超简单!使用PicGo+Gitee搭建免费图床

很早就发现以前用的新浪图床,和七牛云图床都相继出现图片图片无法显示的问题,好在提早把图片存在本地,近期抽出点时间解决一下图床的问题。
因为我对图床的需求目前仅用于存博客的用图,所以我的唯一需求就是“不要钱”即可!最后发现可以用Gitee来存图,再通过PicGo工具简化传图和复制链接的流程。下面就来介绍一下整体搭建流程:

一、下载安装PicGo,安装Gitee插件

下载Gitee插件之前,需要确定已安装npm环境。

二、在Gitee创建仓库和私人令牌

  1. 新建repo时,需要设置为公开的仓库,并勾选初始化仓库

  2. 创建好私人令牌,需要及时保存好生成的一串字符,因为它只出现一次。

三、在PicGo中配置Gitee图床参数

如图配置参数。path表示仓库中具体的文件夹名称,考虑到未来可以将图床用到别的项目上,所以提前先为博客创建专用的文件夹。

四、使用流程

  1. 使用时,直接将图片拖动到状态栏的PicGo图标上;PicGo也会读取电脑剪切板上的截图,点开状态栏手动上传即可。

  2. 上传成功的图片会出现在面板的相册中,点击复制小图标,可以快捷获取Markdown图片代码。

参考资料:

PicGo+Gitee搭建图床

职业生涯规划——读《远见》

引言

今年而立之年将至,工作也竟有7年之久。这些年,换了几家公司涨了几次薪水,也别无其他成就。作为程序员,到了三十岁,难免都会有中年危机,或遇到职业瓶颈,我也不例外。2020年的一整年,我的状态比较低迷,对未来产生了怀疑和迷茫,但是好在及时调整,制定计划,培养习惯,算是暂时度过了那段时间。但是,对于未来的职业生涯,还未有做过仔细的思考和规划——《远见:如何规划职业生涯3大阶段》这边书在我的书单里吃灰了很长时间,不得不说此时正是阅读这本书的最佳时刻了。

三大职业生涯阶段

书中列出的3个阶段分别是:

第一阶段:加添燃料,强势开局
第二阶段:锚定甜蜜区,聚焦长板
第三阶段:优化长尾,发挥持续影响力

目前,我国的退休年龄是女性55周岁、男性60周岁,而完成大学本科学业的时间,大概是在22周岁左右。所以,我们的职业生涯总长度平均至少有36年以上的时间(后续计算和陈述),而每个阶段大约为12年。

第一阶段:加添燃料,强势开局

在第一阶段里,职业生涯对初出茅庐的我们而言充满着未知和挑战。此时正是【蓄力】的好时候,我们需要想方设法去积累未来我们一定会用到的:职业技能、工作经验和人脉关系,这也是作者所描述的职场燃料的3种最基本的形式。

针对职业技能

“1万小时定律”告诉我们如果想要成为卓越非凡的人,就必须付出坚持不断地努力,这也是任何人从平凡变成世界级大师的必要条件。

不断提升和加强自身的职业技能是最基本的要求,而职业技能不仅指的是解决问题的能力,还需要涉及到:

  • 与人沟通的能力
  • 建立自己良好的职业形象的能力
  • 帮助和求助他人的能力
  • 理解和连接他人情绪状态的能力,也就是所谓的EQ

这些能力能够帮我们完成大部分的工作,即使从一家公司换到了另一家公司,这些能力依然通用,这也就是作者所描述的“可迁移的技能”。

针对工作经验

有人会说,工作时间长了,不就有了“丰富”的工作经验了嘛。实际上,大多数人在大多数时间里,都安稳地使用着相同的经验,也就是说,如果我们一直都没有去做一些有挑战性的事情时,我们的工作经验都是和别人重复的没有意义的,对于我们自身而言在职场中是没有竞争力的。

书中提到,如果你非常喜欢当前的工作和环境,但是又不想局限于当前的工作状态或者职位,不凡在完成“本职工作”的情况下,了解更多公司的情况,钻研某个工作中遇到的具体问题,并需要想方设法解决它们,为公司提升效率,带来价值。一个人在公司的价值,恰恰决定了他的职位和薪资。

所以,我们需要“预先”去掌握与职位还不是很匹配的工作经验,才有可能获得我们所渴望的职位。

针对人脉关系

“人脉关系可能是最有效、最耐用的一种职场燃料了”

人脉的重要性对所有人来说,都是不言而喻的。在职场中,我们会遇到许多不同的人:上司、客户、商业伙伴,还有同事,大家相互之间组成的职业生态系统,需要我们持续地去维护。

第二阶段:锚定甜蜜区,聚焦长板

根据前面的分析,职业生涯的第二阶段大约从踏入职场的12年后开始,此时的我们或许已经小有成就——不错的薪资、身居管理岗位,而如何让自己的职业生涯更近一步呢?作者在这里抛出的观点是:锚定自己喜欢和擅长的工作,这项工作还必须是有市场的,然后只需要将自己的热情和核心长板不断地提升和发扬即可!

在第二阶段中,有的人身居管理或独自创业,身兼管理职能,促使我们迫切地需要提升一些特定的能力,因为我们需要关注更多工作的细节之处,作者在这里提到了“技能冲刺法”,即以90天为一个循环,为提高某一项特定能力而进行周期性密集训练。不断地学习不仅能够完善自身的长板技能,还能增加多角度对工作的认识和思考,综合能力提现差异。

同时,我们需要管理好自己的时间和精力,毕竟在第二阶段,人到中年,良好的睡眠、饮食和锻炼才是能够更好工作和生活的保障。

第三阶段:优化长尾,发挥持续影响力

职业生涯后期并不一定就得在压抑中被人遗忘,或在退休的那天遭遇突然打击。合理规划后的第三阶段持续的时间超乎想象,而且回报也相当丰厚,关键就在于主动对这一阶段进行永不懈怠的塑造。

职业生涯的第三个阶段,不应该是只有等待退休这一条路,在过往的职业生涯所积蓄的职场燃料是我们一生的财富,此时的我们还可以作为工作顾问的形式存在。

总结

《远见》这本书将职业生涯划分为3个阶段,刷新了我之前对职业生涯的认知,有些人到了30岁的时候,都会觉得自己一事无成,或遇到了职业瓶颈,我觉得可以读读此书,先沉下心分析一下自身情况,并进行职业规划,重新树立目标(甚至野心)。我觉得做事一定要有目标,不然就像一只无头苍蝇,飞到哪儿是哪儿,所做的努力之间毫无关联,劲儿都没往一处使,最后毫无成就也是在所难免的。
我每年年初都会为自己定好整年的目标,涵盖了工作、学习和家庭三个方面,前往不要忘记将家庭规划进来,因为要知道,其实我们努力工作的初衷之一不就是为了家庭吗。这里推荐阅读《平衡的智慧》这本书,学习因特尔CTO帕特·基辛格是如何平衡家庭和工作的优先次序的。
光定好计划还不够,所以每个月还必须制定OKR,将目标和工作内容细化,并在每个月月底进行总结,已经改进和加强做的不够好的部分。
善于使用工具能够帮助我们更好地规划,目前我使用的是OffScreen记录手机的使用情况,以及清单类软件记录工作事项备忘,结合这两个软件我可以知道我每天都做做了什么,玩儿了多久手机,又有多少时间是用手机在工作的。掌握时间分布这一点非常重要,我们必须搞清楚我们每天都在做些什么,有多少时间是花在了没有意义的娱乐上面,当然如果工作时间超出了正常范围也不是一件健康的事情,所以我们规划好自己的职业生涯,掌控时间,好好睡觉,坚持锻炼,劳逸结合,迎接未来的挑战!

如何封装一个支持LaTex的Markdown解析器

起因

最近在仿写一款优秀的写作软件,优秀的写作软件支持markdown的解析自然是一个必不可少的功能,在思考如何实现解析器的同时,恰好阅读了钟颖大佬出品的关于“代码编辑器”的技术文章,发现可以使用 WebView封装 markdown-ithighlightjs.org 一类的web项目,低成本地实现一个有着不错效果的CommonMark解析器。

实现过程

1. HTML文件创建

创建本地文件 index.html,并输入代码。页面中用到的js文件和资源需要后续创建和生成。<body>标签中唯一的<div>将用于后续插入通过 markdown-it渲染的代码,页面的样式布局通过css文件实现和优化。

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="./main.css" />
<script src="./main.js"></script>
</head>
<body>
<div class="container" id="contents"></div>
</body>
</html>

2. JS文件创建

创建本地文件 index.js,在文件中编写需要注入网页的js代码。

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
import hljs from 'highlight.js'
import MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji'
import './../css/bootstrap.css'
import './../css/gist.css'
import './../css/github.css'
import './../css/index.css'

window.showMarkdown = (percentEncodedMarkdown, enableImage = true) => {

if (!percentEncodedMarkdown) {
return
}

const markdownText = decodeURIComponent(percentEncodedMarkdown)

let markdown = new MarkdownIt({
html: true,
breaks: true,
linkify: true,
highlight: function(code){
return hljs.highlightAuto(code).value;
}
})
if (!enableImage) {
markdown = markdown.disable('image')
}
markdown.use(emoji)
markdown.use(require('markdown-it-latex2img'))
let html = markdown.render(markdownText)

document.getElementById('contents').innerHTML = html
let tables = document.querySelectorAll('table')
tables.forEach((table) => {
table.classList.add('table')
})
let codes = document.querySelectorAll('pre code')
codes.forEach((code) => {
hljs.highlightBlock(code)
})

}

主要用到的js库有:

markdown-it:100% CommonMark 语法解析
highlight.js:代码高亮
markdown-it-emoji:emoji语法支持
markdown-it-latex2img:基于服务器端的MathJax解析器

代码的基本逻辑主要是:
1.由于markdown格式的内容被传入时事先进行了编码操作,所以这里需要调用decodeURIComponent()函数对内容先进行解码。
2.使用 markdown-it别结合所需插件对内容进行渲染,更多关于 markdown-it的使用和插件支持,可以参考官方的API文档:《markdown-it 中文文档》
3.将渲染后的代码插入 index.html页面的对应位置,并支持表格(内嵌功能)和代码高亮的显示。

3. 控件封装

业务逻辑:创建 WKWebView,并注入js代码(showMarkdown()函数调用)
关键代码

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
@objc public func load(markdown: String?, enableImage: Bool = true) {
guard let markdown = markdown else { return }

if let url = htmlURL {
let templateRequest = URLRequest(url: url)

let escapedMarkdown = self.escape(markdown: markdown) ?? ""
let imageOption = enableImage ? "true" : "false"
let script = "window.showMarkdown('\(escapedMarkdown)', \(imageOption));"
let userScript = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

let controller = WKUserContentController()
controller.addUserScript(userScript)

let configuration = WKWebViewConfiguration()
configuration.userContentController = controller

let wv = WKWebView(frame: self.bounds, configuration: configuration)
wv.scrollView.isScrollEnabled = self.isScrollEnabled
wv.translatesAutoresizingMaskIntoConstraints = false
wv.navigationDelegate = self
addSubview(wv)

// 代码省略:布局约束、空间样式
// ...

wv.load(templateRequest)
} else {
// TODO: raise error
}
}

private func escape(markdown: String) -> String? {
return markdown.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)
}

4. 资源文件打包

到此,代码层面的工作就可以结束了。
最后我们需要使用 Webpack工具将js应用程序打包成方便使用的静态资源,输出名为 main.js的文件,将其放置在 index.html文件相同的目录下。这里提供一个可供参考的配置文件:webpack.config.js

写在最后

本文所描述的实现过程,主要参考了三方库 MarkdownView,因为需要支持LaTeX的解析,我稍微重写了showMarkdown函数,加入了 markdown-it-latex2img库的使用,能够将数学公式以图片的形式进行展示。过程中,还学习了 Webpack工具的使用和js静态资源的打包。Webview项目的封装能够为原生提供更丰富的功能,能够为复杂的业务实现提供更多的思路和扩展,希望本文能够对需要的人有所帮助。

参考资料:

Taio开发笔记
Webpack教程
markdown-it API文档
keitaoouchi/MarkdownView

2020年度总结

2020是个多事之秋,从年初开始的新冠疫情,将大家封锁在家中,而我也在为接下去的一年该如何前行而迷茫着。我试着整理自己的资源,并为2020进行了大方向的规划,准确的说,更像是为2020年写了思维导图式的备忘录。恍然间,时间如白驹过隙,2020终究还是过去了,我希望能够通过总结过去一年的成绩,鼓励做更好的自己,并筹划更好的未来。

工作

2020加入了一家从事IOT业务的公司,公司的工作节奏是我认为比较舒服的那种,会给够开发人员足够的时间,其中6成的时间用于功能的开发,剩下的时间则是用于测试和修改,这让我们能够有时间对产品进行深入调试和优化,尽可能地保证产品的质量。
2020是工作维稳的一年,我离开上一家公司的原因,是因为它的工作内容与我的技术栈和职业规划相悖,过于安稳的状态甚至让技术有所下降。所以,我需要对自己进行调整,让职业规划回归正轨。
“物联网+智能家居”是未来势不可挡的趋势,也是我喜欢的行业之一,非常幸运在2020能够找到适合自己,以及自己能够喜欢的工作。
同时,作为开发人员,一定要有能够独当一面的作品,所以2020的下半年,“偷偷地”进行着一款写作软件的开发,希望能够在不远的未来,发布这个软件。(给自己挖个坑)

学习

2020年的年初,虽然对未来有些迷茫,但还是定制了这一年的学习目标(算是一个学习内容备忘录吧)。主要学习内容包括一下几点。

iOS新技术

在新技术方面,其实有过到底是学习flutter还是swift的彷徨,最终的决定还是继续深入走iOS的技术方向。
重新翻出“吃灰”了的《戴铭的iOS高手课程》,个人感觉这本书最大的有点就是成列了iOS开发的知识体系,让像我这种遇到瓶颈的开发者能够有清晰的方向去提升。对于文章的内容,其实需要花大量的时间去研究,因为这门课程的内容主要还是对知识体系的介绍,更深入的内容需要开发者从实践、底层等方向去学习的。
开发语言方面,着重学习的了Swift语言,除了公司开发必须使用OC之外,不论是自己的独立项目还是LeetCode都使用Swift进行编写。真的。Swift相比OC是用了回不去的语言。

LeetCode算法题

使用语言:Swift
解决问题:

  • 简单:95 / 541
  • 中等:18 / 989
  • 困难:01 / 395
    刷了“大量”的简单问题,进行一个算法的“入门”。太久没有接触算法题了,有些生疏。但是解题还是很有意思的一个事情,每日一题,不多不少,算是每天打卡吧。坚持了两个多月,后期应为工作有些忙碌,稍微有些懈怠了。今年和同事创建了一个LeetCode打卡群,希望大家能够在相互监督的环境下共同成长。同时Swift也在慢慢刷题的过程中,逐渐学习起来了。(哈哈~)

源码阅读+博客

开发人员如何快速提成自己的技术水平呢?其中一个最好的方法就是学习优秀的开发者所设计开发的代码,从中吸取经验并为己所用。从9月份开始,在工作的空闲时间,我会阅读常用的第三方库的源码,并将自己的理解写成博客发布在自己的网站上,所谓“不动笔墨,不读书”吗,有输入也必须有输出,通过写博客的方式,是能够检验自己对内容的理解和巩固的。

以下是源码阅读的链接:

阅读

我们这一代人的时间都是碎片化的,社会节奏太快,就算是在下班时间,也会时不时地接到老板的一通电话,要求就地修改需求。
这一年最大的成就可能就是养成了阅读的习惯吧,我给自己设定了每天一小时的阅读打卡任务,要求自己必须完成这项任务。
上下班的通勤时间,其实是最好的阅读时间,所以我大部分的阅读时光都是在地铁上度过的,这一年特别幸运的是“遇见”了不少好书,而且也想白岩松老师说的,读书是一件特别神奇的事情,你越去读书,好书就会源源不断地“主动”找到你,然后你就会读到许多优秀的作品。所以读书真的是一件特别幸福的事情。
这一年,我的阅读时间总共是122小时,读完了17本书,并在不同的地方留下了自己的足迹。这里推荐微信读书App,非常方便,可以算是一款社交类的阅读软件,而且几乎都是在白嫖🤣不过遇到了好书,我还会购买实体书,以方便后续反复阅读。

以下是读书笔记的链接:

生活

对于我自己的生活没有太多可以分享的地方。最重要的是,真的要感谢洪太太,我的老婆大人。为了照顾我们的小宝,你放弃了个人的时间,在一定意义上成为了一名“全职妈妈”。从原来在家人人宠爱的小公主,摇身一变变成了家里的女超人,不过你依旧是,且永远是我心中的小公主。小宝能有现在的成长,也离不开你不停的学习,以及对小宝的认真和耐心的教育。
成长真的是一件很神奇的事情,从孩子第一次拿着食物放进自己的嘴里,第一次叫妈妈,第一爬行,第一次独自站立,第一次独自走路,时间真的很快,一晃眼小宝都已经一周岁了,学会了很多技能,也经常会可爱地惹得大家哈哈大笑,当然也有“不乖”的时候,但除了放下已经捏紧的拳头,还能怎么办呢,谁让他那么可爱的。当然,作为父母的职责,是陪伴和帮助孩子,教他明辨是非善恶,未来成为有独立思想的人!我们一直努力着。

展望

2020有太多可以优化的地方了,在各个方面。
工作上,希望能够找到心仪的远程岗位,让“只工作,不上班”的状态早日实现。
学习上,继续实行ORK工作法,脚踏实地完成任务,达成目标。
阅读上,读书是一个非常好的习惯,我要改变有时候沉迷社交网络的习惯,花费更多的时间投入书籍的怀抱,并影响身边的人,加入阅读的行列。
生活上,承担更多照顾和教育孩子义务。相比带孩子,上班真的是太幸福。所以,我想要让我的太太有时间去做自己的事情,或是心中的事业,去实现自己的梦想。

果然啊,又记成了流水账。

总计,2021,继续加油!

iOS源码阅读——MJRefresh

MJRefresh几乎是我们开发工作中必用的一款三方库,它提供一套非常简单实用的拖拽执行回调事件的解决方案。下面是官方提供的框架图。

其中最常用的几个默认视图类分别是:

1
2
3
下拉刷新控件:MJRefreshNormalHeader
上拉加载控件:MJRefreshAutoNormalFooter、MJRefreshBackNormalFooter
左滑加载控件:MJRefreshNormalTrailer

下面将对这些类,自上而下地进行分析。

公共基类控件

MJRefreshComponent

通过框架图可以看出所有视图都源于同一个基类——MJRefreshComponent,它为子类提供了公用的属性和事件,主要有:

  • 回调对象和回调方法
  • 拖拽状态定义和控制
  • 通过KVO,对事件(控件偏移、内容尺寸、手势状态)添加监听(回调响应交给子类实现)
  • 其他:
    • 拖拽百分比
    • 根据拖拽比例自动切换透明度

MJRefreshComponent还为子类搭建了基本的逻辑框架:

视图创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.初始化
- (instancetype)initWithFrame:(CGRect)frame{;}

// 2.准备工作
- (void)prepare{;}

// 3.视图即将被父视图加入
- (void)willMoveToSuperview:(UIView *)newSuperview{
// 滚动视图初始值的记录
// 一些值的更新
// 监听事件的更新
}

// 4.布局
- (void)layoutSubviews{
[self placeSubviews];
}

滚动视图状态回调

1
2
3
4
5
6
// 当偏移值发生变化
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 当内容大小发生变化
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
// 当点击手势状态发生变化
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

状态设置

1
2
// 状态设置
- (void)setState:(MJRefreshState)state{;}

常用方法

1
2
3
4
// 进入刷新状态
- (void)beginRefreshing{;}
// 结束刷新状态
- (void)endRefreshing{;}

其他

1
2
3
4
5
6
7
// 自动切换透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha{;}
- (BOOL)isAutoChangeAlpha{;}
- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha{;}

// 根据拖拽进度实时设置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent{;}

下拉刷新控件(Header)

下拉刷新控件包含四个类:

  • MJRefreshHeader
    • MJRefreshStateHeader
      • MJRefreshNormalHeader
    • MJRefreshGifHeader

MJRefreshHeader

MJRefreshHeader类是一个包含了完整的下拉刷新功能逻辑的空白视图,子类MJRefreshStateHeaderMJRefreshGifHeader只需要再添加一些额外的图片和文字,就能提升使用体验和保持代码的简洁易读性。

实现过程

1.初始化

创建视图,设置高度和位置。

1
2
3
4
5
6
7
8
9
10
- (void)prepare {
[super prepare];
// 设置存储key
// 设置Header的高度
}

- (void)placeSubviews {
[super placeSubviews];
// 设置Header的位置(y坐标)
}
2.偏移变化:- scrollViewContentSizeDidChange

当用户拖拽滚动控件,是其偏移值发生改变时,会回调- scrollViewContentOffsetDidChange:(NSDictionary *)change方法,在不同的状态下执行对应的逻辑。如果滚动视图已经将Header滚动至屏幕外,则不处理后续逻辑。

- scrollViewContentOffsetDidChange:(NSDictionary *)change方法中,有一些关键的变量值,分别是:

  • 当前滚动的偏移值:offsetY
  • 头部控件刚好出现的偏移值:happenOffsetY
  • 即将刷新的临界点:normal2pullingOffsetY

通过对这些变量值的比较,可以计算出拖拽动作应该被设置为何种状态。

  • 控件正在被拖拽
    • 当拖拽时的偏移量大于临界值,且原状态为闲置时,将状态置为即将刷新
    • 当拖拽时的偏移量小于临界值,且原状态为即将刷新时,将状态重置会闲置
  • 控件未被拖拽,且当前状态为松手进行刷新
    • 执行开始刷新的方法
  • 控件未被拖拽,且为达到执行刷新回调的临界点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (self.scrollView.isDragging) { // 如果正在拖拽
self.pullingPercent = pullingPercent;

// 当拖拽时的偏移量大于临界值,且原状态为闲置时,将状态置为即将刷新
if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
// 转为即将刷新状态
self.state = MJRefreshStatePulling;
}
// 当拖拽时的偏移量小于临界值,且原状态为即将刷新时,将状态重置会闲置
else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 转为普通状态
self.state = MJRefreshStateIdle;
}
}
// 原状态为即将刷新,且手已松开
else if (self.state == MJRefreshStatePulling) {
// 开始刷新
[self beginRefreshing];
}
// 未达到刷新的偏移量,且手已松开
else if (pullingPercent < 1) {
// 记录header露出的百分比
self.pullingPercent = pullingPercent;
}

这里需要注意的是,当Header的状态处于MJRefreshStateRefreshing正在刷新,且控件还在滚动时,会执行- resetInset方法,目的是记录刷新结束后需要调整的上边距值insetTDelta,同时避免 CollectionView 在使用根据 Autolayout 和 内容自动伸缩 Cell, 刷新时导致的 Layout 异常渲染问题。

3.状态设置
1
2
3
4
5
6
- (void)setState:(MJRefreshState)state{
_state = state;

// 加入主队列的目的是等setState:方法调用完毕、设置完文字后再去布局子控件
MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}

视图刷新被加入了异步队列的主线程中,是为了尽量等空间的属性设置完毕后再进行布局的刷新。

4.开始刷新

执行- beginRefreshing方法,设置状态为MJRefreshStateRefreshing刷新中。
方法调用流程如下:

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
1.开始刷新方法调用
- (void)beginRefreshing{
// ...
self.state = MJRefreshStateRefreshing;
// ...
}

2.设置状态为正在刷新中
- (void)setState:(MJRefreshState)state{
MJRefreshCheckState

// 根据状态做事情
if (state == MJRefreshStateIdle) {
//...
} else if (state == MJRefreshStateRefreshing) {
[self headerRefreshingAction];
}
}
3.执行刷新动作
- (void)headerRefreshingAction {
// 主要代码
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滚动区域top
self.scrollView.mj_insetT = top;
// 设置滚动位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
}
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
}

- headerRefreshingAction方法为滚动视图设置了新的insetoffset,使得Header能在滚动视图的顶部停留,用于展示刷新文字动画之类的。

5.结束刷新

结束刷新需要使用者在耗时操作结束后,主动调用- endRefreshing方法。
方法调用流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.结束刷新方法调用
- (void)endRefreshing{
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}
2.设置状态为闲置
- (void)setState:(MJRefreshState)state{
MJRefreshCheckState

// 根据状态做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;

[self headerEndingAction];
} else if (state == MJRefreshStateRefreshing) {
// ...
}
}
3.执行结束动作
- (void)headerEndingAction {;}

- headerEndingAction方法将滚动视图的inset重置为刷新状态前的值,将header又隐藏了起来

上拉加载控件(Footer)

下拉刷新控件包含七个类:

  • MJRefreshFooter
    • MJRefreshBackFooter
      • MJRefreshBackNormalFooter
      • MJRefreshBackGifFooter
    • MJRefreshAutoFooter
      • MJRefreshAutoNormalFooter
      • MJRefreshAutoGifFooter

MJRefreshFooter

MJRefreshFooter类不能直接被使用,它仅定义了少量的基础属性和方法,例如构造方法、初始化控件高度,以及无数据加载情况下的处理。

能够直接使用的上拉加载控件是,由MJRefreshFooter衍生出的两个子类,MJRefreshBackFooterMJRefreshAutoFooter,这两个控件的不同之处在于:

  • MJRefreshBackFooter:隐藏在滚动视图的底部边界之外,当拖动至Footer的刷新临界点并放开手,才会执行加载操作。
  • MJRefreshAutoFooter:会紧贴在滚动视图contentSize的边界,如果contentSize的尺寸小于滚动视图的尺寸,用户在不需要滚动的情况下也能看到Footer控件的。它的刷新时机是,用户在拖拽中且达到了Footer刷新临界点。

MJRefreshBackFooter

实现过程

1.初始化

当MJRefreshBackFooter即将被加入父视图时,会走 - willMoveToSuperview: 方法,并在方法体内调用 - scrollViewContentSizeDidChange: 方法。该方法获取了父视图高度和父视图内容的高度,取二者中较大的数,作为Footer的纵坐标值,确保Footer的位置正好隐藏在视图或内容的最底部。

1
2
3
4
5
6
7
8
9
10
11
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
{
[super scrollViewContentSizeDidChange:change];

// 内容的高度
CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
// 表格的高度
CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom;
// 设置位置和尺寸
self.mj_y = MAX(contentHeight, scrollHeight);
}
2.偏移变化:- scrollViewContentSizeDidChange

当用户在滑动控件使offset发生变化时,会触发 MJRefreshKeyPathContentOffset 的监听事件 —— - scrollViewContentOffsetDidChange
MJRefreshBackFooter- scrollViewContentOffsetDidChange方法里的代码逻辑与MJRefreshHeader是几乎相同的,这里不赘述。需要提一点的是,MJRefreshBackFooter视图的临界值计算需要考虑内容的高度与滚动视图之间的高度差问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma mark 获得scrollView的内容 超出 view 的高度
- (CGFloat)heightForContentBreakView
{
CGFloat h = self.scrollView.frame.size.height - self.scrollViewOriginalInset.bottom - self.scrollViewOriginalInset.top;
return self.scrollView.contentSize.height - h;
}

#pragma mark 刚好看到上拉刷新控件时的contentOffset.y
- (CGFloat)happenOffsetY
{
CGFloat deltaH = [self heightForContentBreakView];
// 内容和视图的高度差
if (deltaH > 0) {
// 内容高度 > 视图高度
return deltaH - self.scrollViewOriginalInset.top;
} else {
// 内容高度 < 视图高度
return - self.scrollViewOriginalInset.top;
}
}
3.状态设置

MJRefreshBackFooter类的- setState方法的主要工作就是在开始刷新和结束刷新的时候,为滚动视图更新对应的offsetinset值。

MJRefreshAutoFooter

实现过程

1.初始化

当MJRefreshAutoFooter即将被加入父视图时,会调用- willMoveToSuperview: 方法,该方法获取了父视图的内容,作为Footer的纵坐标y的值,确保Footer的位置正好紧贴内容的底部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];

if (newSuperview) { // 新的父控件
if (self.hidden == NO) {
self.scrollView.mj_insetB += self.mj_h;
}

// 设置位置
self.mj_y = _scrollView.mj_contentH;
} else { // 被移除了
if (self.hidden == NO) {
self.scrollView.mj_insetB -= self.mj_h;
}
}
}
2.刷新逻辑

MJRefreshAutoFooter控件的位置是紧贴内容的,所以会存在两种情况:
1.当内容高度 < 控件高度,可以直接看到紧贴内容底部的Footer。这种情况下,加载的时机是在用户松手后调用的。

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
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
[super scrollViewPanStateDidChange:change];

if (self.state != MJRefreshStateIdle) return;

UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;

switch (panState) {
// 手松开
case UIGestureRecognizerStateEnded: {
if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {
// 内容 < 控件高度
if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
self.triggerByDrag = YES;
[self beginRefreshing];
}
} else {
// 内容 > 控件高度
if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
self.triggerByDrag = YES;
[self beginRefreshing];
}
}
}
break;

case UIGestureRecognizerStateBegan: {
[self resetTriggerTimes];
}
break;

default:
break;
}
}

2.当内容高度 ≥ 控件高度,需要拖动视图到Footer的加载临界值,但此时不需要松开手,只要滚动视图的偏移量突破了临界值,就会触发加载方法。

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)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];

if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;

// 当autoTriggerTimes被设置成-1(滚动时无限加载)
// 该方法保证拖动放手后,视图还在滚动的情况下,一直保持加载状态

// 内容超出控件高度
if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) {

// 内容高度 - 控件高度 + 控件底部边距 + footer高度 * 百分比 - footer高度
// 内容高度 - 控件高度 + 控件底部边距
if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
// 防止手松开时连续调用
CGPoint old = [change[@"old"] CGPointValue];
CGPoint new = [change[@"new"] CGPointValue];
if (new.y <= old.y) return;

if (_scrollView.isDragging) {
self.triggerByDrag = YES;
}
// 当底部刷新控件完全出现时,才刷新
[self beginRefreshing];
}
}
}

从代码中可以看出,满足刷新的条件是:
拖动偏移量 ≥ 内容高度 - 控件高度 + 控件底部边距 + footer高度 * 刷新控件露出百分比 - footer高度
当刷新控件露出百分比为默认值1.时,不等式可以简化为:
拖动偏移量 ≥ 内容高度 - 控件高度 + 控件底部边距

3.无限触发

MJRefreshAutoFooter的另一特点就是无限触发,开发者可以设置属性自定义自动刷新的次数。

1
2
3
4
5
/** 自动触发次数, 默认为 1, 仅在拖拽 ScrollView 时才生效,

如果为 -1, 则为无限触发
*/
@property (nonatomic) NSInteger autoTriggerTimes;

当滚动视图在持续地滚动时(内容高度≥控件高度),会不停地调用-scrollViewContentOffsetDidChange:方法,满足加载条件时从而不停的调用-beginRefreshing方法。

1
2
3
4
5
6
7
8
- (void)beginRefreshing{
// 新的拖拽动作 && 剩余触发次数 && 是否无限触发
if (self.triggerByDrag && self.leftTriggerTimes <= 0 && !self.unlimitedTrigger) {
return;
}

[super beginRefreshing];
}

当前如果支持无限触发autoTriggerTimes == -1,那么在滚动视图停止滚动前,视图到达加载临界点时都会触发加载任务。

4.状态设置

-beginRefreshing在触发的时候,会将state设置成MJRefreshStateRefreshing,并执行加载数据的回调。
通常我们会在加载数据结束的回调方法中去调用-endRefreshing-endRefreshingWithNoMoreData方法,此时state会被设置成MJRefreshStateIdleMJRefreshStateNoMoreData,对应-setState:方法中的代码,我们可以看到无限触发次数是在此处进行了控制。

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
- (void)setState:(MJRefreshState)state{
MJRefreshCheckState

if (state == MJRefreshStateRefreshing) {
// 执行加载数据回调
[self executeRefreshingCallback];
} else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
if (self.triggerByDrag) {
if (!self.unlimitedTrigger) {
self.leftTriggerTimes -= 1;
}
self.triggerByDrag = NO;
}

/** 结束刷新 */
if (MJRefreshStateRefreshing == oldState) {

// 当视图开启了分页显示,设置动画和回调
if (self.scrollView.pagingEnabled) {
CGPoint offset = self.scrollView.contentOffset;
offset.y -= self.scrollView.mj_insetB;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.contentOffset = offset;

if (self.endRefreshingAnimationBeginAction) {
self.endRefreshingAnimationBeginAction();
}
} completion:^(BOOL finished) {
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}];

return;
}

// 结束刷新回调
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}
}
}

左滑加载控件(Trailer)

MJRefreshTrailer 在实现逻辑上与 MJRefreshBackFooter 是完全一样的,只不过是将部分参数从垂直方向换成了水平方向。

State、Normal 子类控件

State类型的控件 的主要特点是添加了不同状态的提示文字和刷新时间的显示。
Normal类型的控件 在 State类型控件 的基础上,添加了箭头图标和刷新的动画。

Gif 子类控件

Gif类型的控件可以在拖拽和刷新时展示精美的动画来提升用户体验。
主要的两个方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state
{
if (images == nil) return;

self.stateImages[@(state)] = images;
self.stateDurations[@(state)] = @(duration);

/* 根据图片设置控件的高度 */
UIImage *image = [images firstObject];
if (image.size.height > self.mj_h) {
self.mj_h = image.size.height;
}
}

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state {
[self setImages:images duration:images.count * 0.1 forState:state];
}

刷新控件会根据图片的高度调整自身高度,同时会在没有自定义动画时长的情况下,根据动画的帧数自动设置完整播放一遍动画的时间。

拖拽动画

开发者可以通过拖拽百分比设置用户在拖拽时的动画,具体实现方式是通过计算当前拖拽的百分比在整体动画中对应的某个帧的图片来获取大致的下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过拖拽百分比设置 Idle~Pulling状态之间的 对应的动画帧
- (void)setPullingPercent:(CGFloat)pullingPercent
{
[super setPullingPercent:pullingPercent];
NSArray *images = self.stateImages[@(MJRefreshStateIdle)];
if (self.state != MJRefreshStateIdle || images.count == 0) return;
// 停止动画
[self.gifView stopAnimating];


// 设置当前需要显示的图片
NSUInteger index = images.count * pullingPercent;
if (index >= images.count) index = images.count - 1;
self.gifView.image = images[index];
}

刷新动画

刷新动画会在视图状态处于“即将开始刷新”和“刷新中”进行,使用UIImageView的startAnimating对提前设置好的图片组逐帧播放,默认情况下是无限循环播放的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState

// 根据状态做事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
// 即将刷新 和 刷新中 状态的动画
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;

[self.gifView stopAnimating];
if (images.count == 1) { // 单张图片
self.gifView.image = [images lastObject];
} else { // 多张图片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
}
} else if (state == MJRefreshStateIdle) {
// 限制状态停止动画
[self.gifView stopAnimating];
}
}

总结

MJRefresh清晰整齐的架构为开发者提供了及其丰富的扩展性,而在通常没有定制需求的情况下,默认的控件已经十分够用了。

2020年10月读书心得

这本书主要描述了马斯克的几段创业史,即早期的从事企业信息上网的Zip2和支付系统PayPal,还有历尽重重困难现在终于成功的太空运输公司SpaceX和纯电动车公司Tesla。书中最有意思的部分就是马斯克在创业时遇到的痛苦和磨难,和他如何去解决这些问题的过程。他从不吝啬将挣到的钱又再次地投入到他梦想的事业中去,同时为此疯狂地工作,并要求其他人像自己一样努力工作,甚至放弃和亲友的相聚时光。成功是需要代价的,但我不认为牺牲和家人的时间是必须的。但是,马斯克那种工作劲头和面对任何困难都毫不放弃的决心,只为实现一个疯狂的梦想是值得人学习的。
SpaceX公司创立于2002年,2008年获得NASA的正式合同,直到2012年花了10年十年终于成功地为国际空间站输送货物。这十年内,SpaceX无数次濒临破产,航空业的每一次实验成本都是一个天文数字,更何况是在SpaceX完成火箭回收技术之前,并且它还是一家私营的公司。2020年5月31日,SpaceX将两名宇航员成功送入国际空间站并回收火箭,标志着其成为了第一家实现载人航天的商业公司!
而几乎是相同的时间,马斯克还管理运营着Tesla电动汽车公司,而不管造火箭还是造车,马斯克都毫不妥协,他不相信供应商的工作效率和积极性,所以核心零件都由公司自主研发制造,掌握关键技术,并有效控制成本。Tesla还“颠覆”了传统汽车销售模式,采用直销的方式,除了希望带给消费者实惠以外,还有实实在在的服务和便利。这些都是Tesla的企业愿景,具体如何,Tesla车主们应该更有发言权。但从目前的市场情况来讲,将Tesla比作类似于苹果的消费电子类产品,已经一点也不为过了。
站在相对于本书的未来角度,看着SpaceX和Tesla历经磨难一步步地走到今天,确实还是挺心潮澎湃的。


中国历史上第一个实现大一统的秦国,仅仅建立14年,就被推翻,后由汉高祖刘邦称帝建立新朝——西汉。
西汉的发展可以说是在对的时间出现了对的人,文、景、武、宣四个皇帝,都是在最合适的时机登场,并取得了影响后世的历史成就。西汉极盛时,疆域东并朝鲜、南据交趾、西逾葱岭、北抵大漠,国富民强,在疆域、政治、军事、经济、文化、民族和外交上皆有建树。只可惜汉宣帝之后,从汉元帝开始的皇帝,一蟹不如一蟹,最终使汉朝一步步走向衰退。
《盛世:西汉》这本是引用了详实的史料(多出于《史记》)对西汉盛世进行描写,且不说作者自身观点如何,但从对历史的描述来说,阅读此书给我的感觉用两个字描述足以——过瘾,非常值得一读。


《能力陷阱》这本书主要讲的是如何提升自己的领导力,而我在阅读次书的时候,将其视为如何提升自己的“外在”能力。所谓的外在能力,我的理解是那些,我们并不擅长的,亦或是我们还不知道的一些能力。与其对应的,当然就是所谓的“内在能力”,内在能力指的是,我们已经掌握的、擅长做的事情的能力。能力陷阱的体现之处就在于,我们都喜欢做那些我们擅长的事,它会让我们非常有成就感,越有这种感觉,我们就越乐意做这件事,使得我们就越擅长做这件事,注意到了吗,这是一个“可怕”的循环。如果没有及时意识到这点,就会被这个“陷阱”套住,而限制了其他能力的发展。有时候,我们总是在做我们“想做”的事情,而忽略了“应该”做的事情。而“应该”做的事情,可能是我们能力外的,这样我们便损失了可以锻炼提升“外在”能力的机会。

有些人可能会说,做自己“喜欢”的事情,是忠于真实的自己。但大家都忽略了一个非常重要的问题:每个人都有多面性,都有多个“自己”。而你要忠于的是哪一个自己?

我认为如此地“忠于自己”,是一种安于舒适圈的借口。

人的一生都在不停地改变

心理学家丹尼尔·赖文森(Daniel Levinson)提出了著名的“七年之痒”(Seven-year Itch)的概念。他的研究发现改变可能会循环发生,“稳定”与“改变”两个时期总是不断地在生活里交替发生。

“稳定”期通常能维持的时间是7年,当然这并不是说7年内不会有任何改变(我们总是在不断地变化),这里的稳定是相对的。这段时期内,我们的任务就是完成各项“计划”。

但一段时间以后,我们会发现有些事并不像我们所计划的那样发展。或许是因为环境变了,而我们的能力却还没有跟上环境的变化,此时便会萌发出一种情绪——焦虑。

“焦虑”的情绪会驱使着我们去“改变”,“改变”期通常持续的时间是三年,在这个时期内,我们需要重新思考自己所做的事,还要思考做这些是的目的。成年人的世界,充满了各种选择和诱惑,我们需要分辨出做什么事情,对自己更有“意义”。

我个人的心得是,面对不断变化的环境,先不要考虑自己的能力问题(因为能力陷阱),然后去选择自己认为有发展前景的事情,去做实现它“应该”做的事,如果这件事有困难,那么恭喜自己,首先不管结果如何,我们都将收货些什么,以此来提升我们的”外在“能力。

”外在“能力的提升,需要我们去实践和验证,而不是一个人坐在安静的地方连续思考3个小时!

2020年9月读书心得

一直以来觉自己读书没有什么章法,每次阅读都是毫无目的的一行一行浏览,这样的阅读方法会让我非常容易走神,并且吃不进书的内容。早些时候,因为听过《樊登读书会》的一些音频,感觉樊登对一本书的内容理解和讲述地还不错,即表达清楚了书的内容,又有自己的见解。以《清单革命》这本书为例,我是先停了樊登读书的音频版本,再去读的这本书,结果是阅读这本书让我感觉非常地枯燥和范围,但是音频版本确实不错,值得一听,也是从那时候起,我自己开始使用清单App对日常的生活工作,进行管理备忘和安排。
好像扯远了…
读完这本书,让我了解到,樊登在读书的时候,会将自己安置在一个安静的地方,这样的环境可以保证在看书的时候能够全身心地投入。接着在看书之前,设置一个阅读的框架:

  • 这本书的使命是什么?
  • 为了解决什么问题?
  • 为了达成使命,或解决问题,提出哪些假设?
  • 提出解决方案。
  • 如何论证这些假设和方案的有效性?
  • 最后得出了什么结论?
  • 这些结论对我们的意义是什么?

然后,带着这些问题到书里去寻找答案。在读罢一本书之后,樊登的做法是静下心来回忆一遍书里的内容,将核心点梳理和复盘成思维导图,再通过语言组织,在尽肯能保证原意的基础上,用自己的方式进行表述。

能够总结出这样的框架,并拥有极强的专注力,记忆力和表达能力,可能要得益于樊登的辩论经历和刻意练习。

总结

我一直都希望能够有一个系统的读书方法,而本书对我的帮助在于两点。
第一是在读书之前应建立大局观。我会在读完一本我认为颇有收获的书之后,写上一篇读书笔记(心得)。这第二就是再创作,读书笔记就是再创作。在陈述一本书的内容时,通过自己的方式,既要表达出自己的观点和理解,又不能脱离其核心观点和原貌。



起因

《认知天性》这本书,是在阅读《读懂一本书:樊登读书法》的内容中,偶然发现的,我带着能够获取一些学习方法的心态和目的阅读了这本书,结果是效果还不错,至少不会让我觉得枯燥。

学习是挑战天性的必修课

本事开篇就抛出了一个人们在学习这件事情上的认知错误,那就是人们总是觉得对知识点的反复阅读能够提升它的记忆,并错误的觉得自己已经掌握了知识点。但是,通过对比实验可以证明,反复阅读记忆对掌握知识点其实没有太大的帮助。
而需要通过“考试”(或“测试”)这种检索式的学习方法,从反馈中了解对知识的掌握情况,再进行有针对性的刻意练习,方为行之有效的学习方法。

学习的本质 & “后刻意练习”时代

练习从记忆中检索新知识或新技能是有效的学习工具。

大脑的检索与反复阅读相比,它是有一定意义的思考方式。反复阅读可以理解成是对知识的输入,它带来的流畅感,会给人掌握的错觉。而检索记忆则是通过不同题目、不同场景去考察对知识的掌握度,是一种输出,是一种对掌握能力的反馈,同时还“强迫性”地带动大脑的运转。当知识检索成为了习惯,就可以成为一种条件反射,大大提高解决问题的效率和正确率。

有间隔、有内容穿插出现,以及内容多样化,其实就是我们生活的本来面貌。

但是知识检索这种练习,也很容易进入和反复阅读一样的认知误区,那就是集中练习相同的知识或能力,人们总是觉得熟能生巧,但其实可能只是产生了短期记忆,并且如果遇到稍有不同的问题,可能就没法解决了。所以,作者在文中提倡进行有间隔的、有穿插的、具有多样性的练习。这样练习的目的是因为,我们生活中的问题往往是不经意的、没有顺序的出现,问题它不会像我们学习知识一样,反复地有序地按照我们学习的顺序出现。而穿插练习和多样化练习便是根据这个特点应运而生的学习方法,它能帮助我们更好地学习评估问题的背景,以及辨别问题间的差异,再从我们的知识储备中检索最合适的解决方案。最后通过对结果的反思,找出不足,重点学习,做到对知识掌握的巩固和提升。

知识的“滚雪球”效应

学习的的三个步骤:心理学家将学习/记忆过程中的三个阶段分为:编码(获得信息)、存储(将信息维持一段时间)、检索(以后使用信息)。

学习是一个编码(获取),巩固(存储),检索(运用)的过程。已经掌握的知识会形成一条检索路径,当遇到合适场景的时候,能通过一些关键点,检索到对应的知识点。通过有间隔地,穿插式的学习方法,可以避免反复练习造成的麻痹和错觉,更好地对知识点进行区分,建立心智模型。而通过多样化的方式,对知识进行测试,则是通过不同的现实场景,结合关联关键知识点,锻炼和构建检索路径,使得对知识的掌握和运用更加地灵活。在学习的过程中,我们应该趋向于有难度的练习和自主的学习方式,这有助于强化自己的辨析和归纳能力,并且我们应该不惧犯错,通过有效合理的纠正性反馈,能收获更好的学习效果。

心智模型

掌握了生活中方方面面的知识,我们会倾向于把做事的步骤集合在一起来解决各种问题。

由于直觉式的元认知,人们总是会对自身给出高于实际的评估分数,这种认知甚至衍生到了对周遭的理解上,人们会想方设法用自己熟悉的自认为理性的方式来诠释身边发生的事情。这在很大程度上给我们提供了错误的认知和影响了我们的判断能力。而我们需要做的,应该是有意识地分析和推理,并进行自我控制,建立自己的心智模型(将知识集合成解决问题的步骤),通过实践和测验,去发现学习的漏洞,并把它填补完整。

学习风格

分析型智力是我们解决问题的能力,典型的例子就是解答测验中的问题;
创新型智力是我们综合并应用现有的知识与技能,应对那些新的特殊情况的能力;
实践型智力是我们适应日常生活的能力;

构建适合自己的学习风格,首先要求我们能掌控学习的主动性,利用动态的测验方法,找到自己对于某种知识和技能的缺失,从而进行改善。同时,我们还需要根据不同的文化和学习场景锻炼和运用不同的智力类型(分析型,实践型,创新型),并通过实践去提取其中的原则,构建心智模型的结构,最终成为可以使自己在实际问题中可以捷足先登的诀窍。

终生学习

智商测验一直被用来衡量个人的逻辑与语言潜能。这类测验会规定一个智力商数,代表心理年龄与实际年龄的比率,然后再乘以100得出智商值。

智力水平(也就是我们常说的“智商”)是一种可以通过努力提高的能力,而我们努力的目标和方向应该是为了学到新的知识或技能。在成长的过程中,优秀的执行力甚至比学习技巧更加重要,我们需要通过刻意练习和反复应用,让知识扎根于我们的潜意识之中。

总结

学习和提升一定是一个艰辛、痛苦,且见效慢的过程,永远不要让自己长时间处于舒适的状态中,居安思危,不断挑战自己,才能获得成长。

iOS源码阅读 —— YYModel vs MJExtension

YYModelMJExtension作为JSON模型转换工具,应该算是国内使用者比较多的第三方框架。相信两款都用过的开发者大有人在,我也是其中之一。既然如此,笔者便相继阅读了这两个库的主要源码,并参考YYModel作者ibireme《iOS JSON 模型转换库评测》一文进行了的评测和展开。本文仅代表个人观点,如有异议,欢迎交流指导。

评测对象

1
2
3
pod 'YYModel', '~> 1.0.4'

pod 'MJExtension', '~> 3.2.2'

评测用例:GithubUserWeiboStatus
评测代码:https://github.com/a334713698/JSONModelTransformReview
运行环境:iOS 13.5 | iPhone XS Max

性能

性能评测的方法是两个库执行相同次数的JSON模型转换,对比二者的耗时情况。

用例1:GithubUser

GithubUser的数据主要类型是string,和少量的number,主要测试转换库的基本功能。
我们分三遍执行两个转换库的相关方法,每遍执行50000次,统计耗时毫秒数。
结果如下:

Json 2 Model 第一次 第二次 第三次 遍历次数
MJExtension 1481.25 ms 1468.86 ms 1452.77 ms 1,550,000
YYModel 257.29 ms 250.48 ms 250.39 ms 1,400,000
Model 2 Json 第一次 第二次 第三次 遍历次数
MJExtension 1182.23 ms 1162.47 ms 1173.69 ms 1,550,000
YYModel 382.40 ms 373.91 ms 379.84 ms 1,300,000

用例2:WeiboStatus

微博数据WeiboStatus包含大量的复杂类型,主要测试转换库在复杂数据类型情况下的性能。
我们分三遍执行这个方法,每遍执行5000次,统计耗时毫秒数。
结果如下:

Json 2 Model 第一遍 第二遍 第三遍 遍历次数
MJExtension 4061.77 ms 4054.89 ms 4057.63 ms 2,290,000
YYModel 813.44 ms 803.64 ms 806.97 ms 2,285,000
Model 2 Json 第一遍 第二遍 第三遍 遍历次数
MJExtension 596.46 ms 592.42 ms 589.69 ms 475,000
YYModel 660.04 ms 626.69 ms 615.30 ms 1,215,000

性能评测结果

  • 每个用例的第一遍评测,都会比后两遍有稍多的用时,是因为第一遍运行,会首次创建和缓存类的类元信息和属性元信息,后两遍再运行的时候,可以直接使用缓存,减少重复生成类元和属性元造成的开销。
  • 系统方法和容器使用方面:MJExtension主要使用的是Foundation框架的NSArray、NSDictionary,以及KVC的方法进行取值和赋值。YYModel主要使用了CoreFoundation框架的容器和遍历方法,通过objc_msgSend消息发送的方式,调用属性的_setter_getter进行取值和赋值,部分地方还使用inline和纯C函数。

容错

容错性主要是测试,当JSON和Model之间的数据格式不完全相同时,转换库是如何处理的,是否会产生错误或造成Crash。

用例 1 JSON属性是:数值,Model属性是:NSString
MJExtension 100 -> @”100”
YYModel 100 -> @”100”
用例 2 JSON属性是:数值字符串,Model属性是数值
MJExtension @”100” -> 100
YYModel @”100” -> 100
用例 3 JSON属性是时间字符串,Model属性是NSDate
MJExtension nil,属性类型与值类型不匹配
YYModel 支持ISO标准时间格式的时间字符串自动转成NSDate
用例 4 JSON属性是字符串,Model属性是NSValue
MJExtension nil,属性类型与值类型不匹配
YYModel nil,属性类型与值类型不匹配

YYModelMJExtension 都会都属性类型与值类型进行类型检测,避免属性被赋予了错误的类型值,以避免潜在的风险。

这里需要提一下,ibireme发布的转换库评测代码发布于2015-09-18。当时MJExtension的最高版本应该是2.5.7版本,此时的代码中还未添加类型检测的代码。

对于NSDate类型的JSON数据,如果时间格式满足ISO标准,YYModel支持将ISO标准时间格式的字符串,转换成NSDate类型的值;而MJExtension会因为类型检测不匹配,为模型赋空值(nil)。

功能

功能 MJExtension YYModel
属性名转换
自定义属性值转换
黑白名单
Coding
Copying -
hash/equal -
CoreData -

侵入性

YYModelMJExtension都采用Category来实现,无侵入,并在方法名之前添加了前缀,与原生方法进行区分。

结论

YYModelMJExtension从逻辑上来说是相似的,都通过Category无侵入性地实现功能,且都是使用runtime动态地获取、创建和缓存类元、属性元信息,再通过对数据的遍历进行转换。具体功能上都支持自定义属性名映射和自定义属性值转换,以及方便的归档接档的方法。少部分功能,略有差异,可根据需求选取。
从方法/函数使用上来说,MJExtension使用的是Foundation框架的方法,而YYModel使用的是相比之下更底层的CoreFoundation框架的函数,再配合使用内联和纯C函数,能够做到比MJExtension更少的资源开销,从而在性能上有显著的优势。

iOS源码阅读 —— MJExtension

MJExtension是一款开源的,简单易用的字典与模型转换框架。
常用的方法,主要是以下几个:

1
2
3
4
5
6
7
8
9
10
11
// JSON|字典 转 模型
+ (instancetype)mj_objectWithKeyValues:(id)keyValues;

// 通过 JSON|字典 为 模型赋值
- (instancetype)mj_setKeyValues:(id)keyValues;

// 模型转JSON
- (NSMutableDictionary *)mj_keyValues;

// JSON数组转模型数组
+ (NSMutableArray *)mj_objectArrayWithKeyValuesArray:(id)keyValuesArray;

功能

JSON|字典 转 模型

+ mj_objectWithKeyValues:

+ mj_objectWithKeyValues: 是框架中最简单的JSON转模型的方法,通过直接调用类方法并传入JSON数据即可快速实现转换。而在+ mj_objectWithKeyValues:方法中,实际是调用了+ mj_objectWithKeyValues: context:方法,参数中如果传了contenxt,最终会返回CoreData模型;如果不传,返回已赋值的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (instancetype)mj_objectWithKeyValues:(id)keyValues context:(NSManagedObjectContext *)context
{
// 获得JSON对象
keyValues = [keyValues mj_JSONObject];
MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], nil, [self class], @"keyValues参数不是一个字典");

// 判断是否传入 "contenxt" 参数
if ([self isSubclassOfClass:[NSManagedObject class]] && context) {
NSString *entityName = [NSStringFromClass(self) componentsSeparatedByString:@"."].lastObject;
return [[NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context] mj_setKeyValues:keyValues context:context];
}
return [[[self alloc] init] mj_setKeyValues:keyValues];
}

在为实例赋值的方法中,由于- mj_setKeyValues:实际的实现是调用- mj_setKeyValues: context:。所以我们直接进入- mj_setKeyValues: context:进行分析。

首先,需要将传入的keyValues处理成可用的JSON对象,并获取当前类的类型,以及黑白名单属性。

1
2
3
4
5
6
7
// 获得JSON对象
keyValues = [keyValues mj_JSONObject];
MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");

Class clazz = [self class];
NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

紧接着,调用类的扩展方法+ mj_enumerateProperties:,获取和遍历类的属性列表,通过block参数进行回调,在回调的代码块中,对每个属性进行注意赋值。

核心代码:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。
[clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
@try {
// 0.检测是否被忽略
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
if ([ignoredPropertyNames containsObject:property.name]) return;

// 1.取出属性值
id value;
NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
for (NSArray *propertyKeys in propertyKeyses) {
value = keyValues;
for (MJPropertyKey *propertyKey in propertyKeys) {
value = [propertyKey valueInObject:value];
}
if (value) break;
}

// 值的过滤
id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
if (newValue != value) { // 有过滤后的新值
[property setValue:newValue forObject:self];
return;
}

// 如果没有值,就直接返回
if (!value || value == [NSNull null]) return;

// 2.复杂处理
MJPropertyType *type = property.type; // 数据类型类
Class propertyClass = type.typeClass; // 对象类型
Class objectClass = [property objectClassInArrayForClass:[self class]]; // 数组中的模型类型

// 不可变 -> 可变处理
if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
value = [NSMutableArray arrayWithArray:value];
} else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
value = [NSMutableDictionary dictionaryWithDictionary:value];
} else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
value = [NSMutableString stringWithString:value];
} else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
value = [NSMutableData dataWithData:value];
}

if (!type.isFromFoundation && propertyClass) { // 模型属性
// 既不是基础类型,也不是NS类型。即:基本数据类型
value = [propertyClass mj_objectWithKeyValues:value context:context];
} else if (objectClass) {
if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
// string array -> url array
NSMutableArray *urlArray = [NSMutableArray array];
for (NSString *string in value) {
if (![string isKindOfClass:[NSString class]]) continue;
[urlArray addObject:string.mj_url];
}
value = urlArray;
} else { // 字典数组-->模型数组
value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
}
} else if (propertyClass == [NSString class]) {
if ([value isKindOfClass:[NSNumber class]]) {
// NSNumber -> NSString
value = [value description];
} else if ([value isKindOfClass:[NSURL class]]) {
// NSURL -> NSString
value = [value absoluteString];
}
} else if ([value isKindOfClass:[NSString class]]) {
if (propertyClass == [NSURL class]) {
// NSString -> NSURL
// 字符串转码
value = [value mj_url];
} else if (type.isNumberType) {
NSString *oldValue = value;

// NSString -> NSDecimalNumber, 使用 DecimalNumber 来转换数字, 避免丢失精度以及溢出
NSDecimalNumber *decimalValue = [NSDecimalNumber decimalNumberWithString:oldValue
locale:numberLocale];

// 检查特殊情况
if (decimalValue == NSDecimalNumber.notANumber) {
value = @(0);
}else if (propertyClass != [NSDecimalNumber class]) {
value = [decimalValue mj_standardValueWithTypeCode:type.code];
} else {
value = decimalValue;
}

// 如果是BOOL
if (type.isBoolType) {
// 字符串转BOOL(字符串没有charValue方法)
// 系统会调用字符串的charValue转为BOOL类型
NSString *lower = [oldValue lowercaseString];
if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) {
value = @YES;
} else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
value = @NO;
}
}
}
} else if ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){
// 过滤 NSDecimalNumber类型
if (![value isKindOfClass:[NSDecimalNumber class]]) {
value = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]];
}
}

// 经过转换后, 最终检查 value 与 property 是否匹配
if (propertyClass && ![value isKindOfClass:propertyClass]) {
value = nil;
}

// 3.赋值(KVC)
[property setValue:value forObject:self];
} @catch (NSException *exception) {
MJExtensionBuildError([self class], exception.reason);
MJExtensionLog(@"%@", exception);
}
}];

从代码中,可以直观看出,赋值操作主要分为步骤4个步骤。

0.检测是否被忽略

1
2
3
4
// 白名单
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
// 黑名单
if ([ignoredPropertyNames containsObject:property.name]) return;

判断黑白名单中是否包含相应的属性名称。

1.取出属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id value;
NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
for (NSArray *propertyKeys in propertyKeyses) {
value = keyValues;
for (MJPropertyKey *propertyKey in propertyKeys) {
value = [propertyKey valueInObject:value];
}
if (value) break;
}

// 值的过滤
id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
if (newValue != value) { // 有过滤后的新值
[property setValue:newValue forObject:self];
return;
}

// 如果没有值,就直接返回
if (!value || value == [NSNull null]) return;

因为同一个成员属性,父类和子类的行为可能不一致(originKey、propertyKeys、objectClassInArray),所以其键值可能是一个数组,通过循环这个数组尝试获取值。
对值得过滤,指的是使用者通过实现- (id)mj_newValueFromOldValue: property:方法,对结果进行进一步的处理(比如字符串日期处理为NSDate、字符串nil处理为@””)。

2.复杂处理

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
MJPropertyType *type = property.type; // 数据类型的信息
Class propertyClass = type.typeClass; // 属性的类型
Class objectClass = [property objectClassInArrayForClass:[self class]]; // 数组中模型的类型

// 不可变 -> 可变处理
if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
value = [NSMutableArray arrayWithArray:value];
} else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
value = [NSMutableDictionary dictionaryWithDictionary:value];
} else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
value = [NSMutableString stringWithString:value];
} else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
value = [NSMutableData dataWithData:value];
}

if (!type.isFromFoundation && propertyClass) { // 模型属性
// 既不是基础类型,也不是NS类型。即:基本数据类型
value = [propertyClass mj_objectWithKeyValues:value context:context];
} else if (objectClass) {
if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
// string array -> url array
NSMutableArray *urlArray = [NSMutableArray array];
for (NSString *string in value) {
if (![string isKindOfClass:[NSString class]]) continue;
[urlArray addObject:string.mj_url];
}
value = urlArray;
} else { // 字典数组-->模型数组
value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
}
} else if (propertyClass == [NSString class]) {


} else if ([value isKindOfClass:[NSString class]]) {

} else if ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){

}

复杂处理,主要是对属性值的类型进行判断,属性值的类型只要分为:模型属性(自定义类)、数组属性和其他属性(NS类型)。
模型属性的value,需要通过继续调用- mj_objectWithKeyValues:value context:方法,将字典转换成模型。
数组属性的值,则需要根据数组中模型的类型,进行循环转换。
其他情况的值,可以通过简单的转化或者直接使用。

3.赋值

至此,属性信息和值都有了。

1
2
3
4
5
// 经过转换后, 最终检查 value 与 property 是否匹配
if (propertyClass && ![value isKindOfClass:propertyClass]) {
value = nil;
}
[property setValue:value forObject:self];

在确定 value的值 与 property得类型 确实匹配后,通过KVC进行赋值。

1
2
3
4
5
6
7
8
/**
* 设置成员变量的值
*/
- (void)setValue:(id)value forObject:(id)object
{
if (self.type.KVCDisabled || value == nil) return;
[object setValue:value forKey:self.name];
}

到此,JSON转模型的工作就完成了。

模型 转 JSON|字典

- mj_keyValues

模型转JSON的方法主要有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 转换并返回模型中所有属性的键值对
- (NSMutableDictionary *)mj_keyValues;

/**
@para keys 需要返回的特定键的数组
@return 特定关键词的键值对
*/
- (NSMutableDictionary *)mj_keyValuesWithKeys:(NSArray *)keys;

/**
@para ignoredKeys 需要忽略的特定键的数组
@return 除特定关键词的其他有效键值对
*/
- (NSMutableDictionary *)mj_keyValuesWithIgnoredKeys:(NSArray *)ignoredKeys;

以上方法统一调用了- mj_keyValuesWithKeys:ignoredKeys:,让我们直接进入这个方法一探究竟。
- mj_keyValuesWithKeys:ignoredKeys:方法与JSON转模型的核心逻辑是极其相似的,即通过遍历类的所有属性,进行相关操作,这里我们直接进入代码块,进行分析。

0.检测是否被忽略

1
2
3
4
5
6
7
8
// 白名单
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
// 黑名单
if ([ignoredPropertyNames containsObject:property.name]) return;
// 只需要返回的特定键
if (keys.count && ![keys containsObject:property.name]) return;
// 需要被忽略的特定键
if ([ignoredKeys containsObject:property.name]) return;

返回的结果,不仅可以对黑白名单中的属性进行筛选,还可以根据具体场景设置需要返回和忽略的特定键值。

1.取出属性值

使用KVC取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id value = [property valueForObject:self];

/**
* 获得成员变量的值
*/
- (id)valueForObject:(id)object
{
if (self.type.KVCDisabled) return [NSNull null];

id value = [object valueForKey:self.name];

// 32位BOOL类型转换json后成Int类型
/** https://github.com/CoderMJLee/MJExtension/issues/545 */
// 32 bit device OR 32 bit Simulator
#if defined(__arm__) || (TARGET_OS_SIMULATOR && !__LP64__)
if (self.type.isBoolType) {
value = @([(NSNumber *)value boolValue]);
}
#endif

return value;
}

2.模型属性和数组的处理

如果当前的属性属于模型类型或数组,则需要对 value 进行递归调用 - mj_keyValues 方法,直至最终得到非模型和非数组的数据类型。

1
2
3
4
5
6
7
8
9
10
MJPropertyType *type = property.type;
Class propertyClass = type.typeClass;
if (!type.isFromFoundation && propertyClass) {
value = [value mj_keyValues];
} else if ([value isKindOfClass:[NSArray class]]) {
// 3.处理数组里面有模型的情况
value = [NSObject mj_keyValuesArrayWithObjectArray:value];
} else if (propertyClass == [NSURL class]) {
value = [value absoluteString];
}

3.赋值

在对结果keyValues进行赋值之前,需要先判断创建键值时,是否引用了替换键 —— 也就是在+ mj_replacedKeyFromPropertyName方法中返回的自定义映射表。
对于没有引用替换键的值,可以直接赋值。

1
keyValues[property.name] = value;

对于引用了替换键的值,需要获取原始的key,最终结果也将返回最原始的JSON或字典。

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
// 获取原始key
NSArray *propertyKeys = [[property propertyKeysForClass:clazz] firstObject];
NSUInteger keyCount = propertyKeys.count;
// 创建字典
__block id innerContainer = keyValues;
[propertyKeys enumerateObjectsUsingBlock:^(MJPropertyKey *propertyKey, NSUInteger idx, BOOL *stop) {
// 下一个属性
MJPropertyKey *nextPropertyKey = nil;
if (idx != keyCount - 1) {
nextPropertyKey = propertyKeys[idx + 1];
}

if (nextPropertyKey) { // 不是最后一个key
// 当前propertyKey对应的字典或者数组
id tempInnerContainer = [propertyKey valueInObject:innerContainer];
if (tempInnerContainer == nil || [tempInnerContainer isKindOfClass:[NSNull class]]) {
if (nextPropertyKey.type == MJPropertyKeyTypeDictionary) {
tempInnerContainer = [NSMutableDictionary dictionary];
} else {
tempInnerContainer = [NSMutableArray array];
}
if (propertyKey.type == MJPropertyKeyTypeDictionary) {
innerContainer[propertyKey.name] = tempInnerContainer;
} else {
innerContainer[propertyKey.name.intValue] = tempInnerContainer;
}
}

if ([tempInnerContainer isKindOfClass:[NSMutableArray class]]) {
NSMutableArray *tempInnerContainerArray = tempInnerContainer;
int index = nextPropertyKey.name.intValue;
while (tempInnerContainerArray.count < index + 1) {
[tempInnerContainerArray addObject:[NSNull null]];
}
}

innerContainer = tempInnerContainer;
} else { // 最后一个key
if (propertyKey.type == MJPropertyKeyTypeDictionary) {
innerContainer[propertyKey.name] = value;
} else {
innerContainer[propertyKey.name.intValue] = value;
}
}
}];

总结

核心代码:

  • JSON|字典转模型的各类方法,最终都会调用- mj_setKeyValues:(id)keyValues context:
  • 模型转JSON|字典的各类方法,最终都会调用- (NSMutableDictionary *)mj_keyValuesWithKeys: ignoredKeys:

性能方面:

  • 使用runtime动态生成类的属性信息,并通过缓存机制进行性能提优。

容错方面

  • 在JSON|字典转模型最后赋值之前,会对值和属性的类型进行一致性的判断。如果不匹配,value会被置为nil,避免潜在的Crash风险。

iOS源码阅读——YYModel

YYModel作为一个 iOS/OSX 模型转换框架,为JSON与数据模型之间的转换,提供了高性能的解决方案。

在我个人的日常开发中,主要使用的方法有以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
// JSON|字典 转 模型
+ (nullable instancetype)yy_modelWithJSON:(id)json;
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;

// 通过 JSON|字典 为 模型赋值
- (BOOL)yy_modelSetWithJSON:(id)json;
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic;

// 模型转JSON
- (NSString *)yy_modelToJSONString;

// JSON数组转模型数组
+ (nullable NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json;

由于多个功能,最终调用的方法是相同的,所以这里仅列出主要方法的代码解析。

功能

JSON转模型

+ yy_modelWithDictionary:

由于调用+ yy_modelWithJSON:方法时,方法内部先将JSON序列化为可用的字典,然后调用+ yy_modelWithDictionary:方法。所以我们直接进入+ yy_modelWithDictionary:进行分析。

代码:

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
/**
通过一组 键-值对(NSDictionary),创建和返回一个新的实例
此方法是线程安全的。

@参数: dictionary 一组能够映射实例属性的 键-值对(dictionary)
无效的键值对将会被忽略。

@返回: 一个通过 键-值对(dictionary) 创建的新实例,出错的情况下返回nil。

@说明: 字典中的 key 和 value 将分别映射在模型的属性名,和属性值上。
如果值得类型不发与属性相匹配,此方法将尝试根据如下规则,进行转化:

`NSString` or `NSNumber` -> c number, such as BOOL, int, long, float, NSUInteger...
`NSString` -> NSDate, parsed with format "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss" or "yyyy-MM-dd".
`NSString` -> NSURL.
`NSValue` -> struct or union, such as CGRect, CGSize, ...
`NSString` -> SEL, Class.
*/

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
if (!dictionary || dictionary == (id)kCFNull) return nil;
if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;

// 创建当前类的类对象实例
Class cls = [self class];
// 创建和获取 模型的元类(包含类的详细信息)
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];

// 判断使用者是否自定义 类的(子类)类型
if (modelMeta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}

// 创建实例实例
NSObject *one = [cls new];

// 为属性赋值
if ([one yy_modelSetWithDictionary:dictionary]) return one;
return nil;
}

+ yy_modelWithDictionary:方法中,主要做了三件事:1.确定类型;2.创建实例;3.为实例赋值。

1. 确定类型

在类方法中使用[self class]可以轻松获取当前类的类对象,在这里作者通过类对象创建了该类的类元_YYModelMeta *model,类元中包含了丰富的关于该类的信息。

_YYModelMeta 类元的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// 模型对象的类元信息
@interface _YYModelMeta : NSObject {
@package
YYClassInfo *_classInfo;
/// Key:mapped key and key path, Value:_YYModelPropertyMeta. 数据结构:{"pic": [_YYModelPropertyMeta new]}
NSDictionary *_mapper;
/// Array<_YYModelPropertyMeta>, 所有有效属性元的数组
NSArray *_allPropertyMetas;
/// Array<_YYModelPropertyMeta>, 映射到键值路径的属性元
NSArray *_keyPathPropertyMetas;
/// Array<_YYModelPropertyMeta>, 映射到多个键的属性元
NSArray *_multiKeysPropertyMetas;
/// 有效的键值对数量,所谓有效即包含 _getter、_setter、成员变量。 值与 _mapper.count 相同
NSUInteger _keyMappedCount;
/// 数据类型
YYEncodingNSType _nsType;

BOOL _hasCustomWillTransformFromDictionary;
BOOL _hasCustomTransformFromDictionary;
BOOL _hasCustomTransformToDictionary;
BOOL _hasCustomClassFromDictionary;
}
@end

在确定类型之前,需要先判断使用者是否根据不同情况自定义了返回类的(子类)类型,即是否实现了+ modelCustomClassForDictionary:(NSDictionary *)dictionary;方法返回自定义类型。

官方示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@class YYCircle, YYRectangle, YYLine;

@implementation YYShape

+ (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary {
if (dictionary[@"radius"] != nil) {
return [YYCircle class];
} else if (dictionary[@"width"] != nil) {
return [YYRectangle class];
} else if (dictionary[@"y2"] != nil) {
return [YYLine class];
} else {
return [self class];
}
}
@end

2. 创建实例

确定数据类型后,通过类对象快速创建实例。

1
NSObject *one = [cls new];

3. 为实例赋值

调用 -yy_modelSetWithDictionary: 方法为实例赋值。

代码:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
if (!dic || dic == (id)kCFNull) return NO;
if (![dic isKindOfClass:[NSDictionary class]]) return NO;

// 创建和获取 模型的元类(包含类的详细信息)
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];

// 判断当前类的有效属性数量
if (modelMeta->_keyMappedCount == 0) return NO;

// 判断使用者是否自定义了转换映射
if (modelMeta->_hasCustomWillTransformFromDictionary) {
dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
if (![dic isKindOfClass:[NSDictionary class]]) return NO;
}

// 创建 模型设置上下文
ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic); //dic or json

// 比较 元模型的键值数量 & 传入字典的键值数量
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
/**
@function CFDictionaryApplyFunction
对字典中的每个键值对调用函数一次。

@param theDict
要查的字典。

@param applier
要对字典中的每个值调用一次的回调函数。

@param context
一个指针大小的用户定义值,作为第三个参数传递给applier函数,但此函数不使用它。

*/
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);

if (modelMeta->_keyPathPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
if (modelMeta->_multiKeysPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
} else {
/**
@function CFArrayApplyFunction
对数组中的每个元素调用函数一次。

@param theArray
要操作的数组。

@param range
要将函数应用于的数组中的值范围。

@param applier
对数组中给定范围内的每个值调用一次的回调函数。如果此参数不是指向正确原型的函数的指针,则行为未定义。如果在应用程序函数期望的范围内存在或不能正确应用的值,则该行为是未定义的。

@param context
一个指针大小的用户定义值,它作为第二个参数传递给applier函数,但此函数不使用它。如果上下文不是applier函数所期望的内容,则行为是未定义的。

*/
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}

if (modelMeta->_hasCustomTransformFromDictionary) {
return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
}
return YES;
}

这里会先判断使用者是否对数据字典做了额外的处理,即是否实现了 -modelCustomWillTransformFromDictionary: 方法。如果有,则返回和使用自定义的字典。

一切准备就绪,创建模型设置上下文ModelSetContext context,准备赋值。

1
2
3
4
5
typedef struct {
void *modelMeta; ///< _YYModelMeta 类元
void *model; ///< id (self) 实例本身
void *dictionary; ///< NSDictionary (json) 数据字典(json)
} ModelSetContext;

比较 类元的有效键值数量传入字典的键值数量,以较小的代价进行属性的遍历赋值(减少不必要的循环次数)。这里分别使用CFDictionaryApplyFunction( )CFArrayApplyFunction( ) 对应 ModelSetWithDictionaryFunction( )ModelSetWithPropertyMetaArrayFunction( ),进行遍历调用。二者最终都是通过 ModelSetValueForProperty( ) 函数进行赋值的。

1
2
3
4
static void ModelSetValueForProperty(__unsafe_unretained id model,// 实例对象
__unsafe_unretained id value,// 值
__unsafe_unretained _YYModelPropertyMeta *meta //属性元
)

ModelSetValueForProperty( ) 函数中对属性的数据进行了详细的类型判断,主要分为三大类(C的基础数据类型、Foundation的NS数据类型、自定义数据类型)。除了C的基本数据类型,后者都通过消息发送 objc_msgSend 的方式,调用属性的 meta->_setter 方法进行赋值。

由于实现代码较长,这里就不展示了,有兴趣的可以自行查看源码:《YYModel/NSObject+YYModel.m》第784~1098行

到此,JSON转模型的工作就完成了。

模型转JSON

+ yy_modelToJSONString:

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
- (id)yy_modelToJSONObject {
/*
Apple said:
The top level object is an NSArray or NSDictionary.
All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
All dictionary keys are instances of NSString.
Numbers are not NaN or infinity.
*/
id jsonObject = ModelToJSONObjectRecursive(self);
if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;
return nil;
}

- (NSData *)yy_modelToJSONData {
id jsonObject = [self yy_modelToJSONObject];
if (!jsonObject) return nil;
return [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:NULL];
}

- (NSString *)yy_modelToJSONString {
NSData *jsonData = [self yy_modelToJSONData];
if (jsonData.length == 0) return nil;
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

从方法实现中不难看出,模型转JSON主要依赖于递归函数 ModelToJSONObjectRecursive,该函数最终将返回一个有效的JSON对象(NSArray/NSDictionary/NSString/NSNumber/NSNull)。

ModelToJSONObjectRecursive 内部实现代码拆解:

1
2
3
4
5
6
7
if (!model || model == (id)kCFNull) return model;
if ([model isKindOfClass:[NSString class]]) return model;
if ([model isKindOfClass:[NSNumber class]]) return model;
if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter( ) stringFromDate:(id)model];
if ([model isKindOfClass:[NSData class]]) return nil;

当模型值符合或接近目标类型时,可做简单的转换或直接返回.

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
// 字典
if ([model isKindOfClass:[NSDictionary class]]) {
if ([NSJSONSerialization isValidJSONObject:model]) return model;
NSMutableDictionary *newDic = [NSMutableDictionary new];
[((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
if (!stringKey) return;
id jsonObj = ModelToJSONObjectRecursive(obj);
if (!jsonObj) jsonObj = (id)kCFNull;
newDic[stringKey] = jsonObj;
}];
return newDic;
}

// 集合
if ([model isKindOfClass:[NSSet class]]) {
NSArray *array = ((NSSet *)model).allObjects;
if ([NSJSONSerialization isValidJSONObject:array]) return array;
NSMutableArray *newArray = [NSMutableArray new];
for (id obj in array) {
if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
[newArray addObject:obj];
} else {
id jsonObj = ModelToJSONObjectRecursive(obj);
if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
}
}
return newArray;
}

// 数组
if ([model isKindOfClass:[NSArray class]]) {
if ([NSJSONSerialization isValidJSONObject:model]) return model;
NSMutableArray *newArray = [NSMutableArray new];
for (id obj in (NSArray *)model) {
if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
[newArray addObject:obj];
} else {
id jsonObj = ModelToJSONObjectRecursive(obj);
if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
}
}
return newArray;
}

当模型值为字典、集合数组类型时,需要遍历和递归其内部元素,直至逐一转化成有效的JSON对象。

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
57
58
59
60
61
62
63
64
65
// 自定义类
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]];
if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil;
NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64];
__unsafe_unretained NSMutableDictionary *dic = result; // avoid retain and release in block
[modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
if (!propertyMeta->_getter) return;

id value = nil;
if (propertyMeta->_isCNumber) {
value = ModelCreateNumberFromProperty(model, propertyMeta);
} else if (propertyMeta->_nsType) {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
} else {
switch (propertyMeta->_type & YYEncodingTypeMask) {
case YYEncodingTypeObject: {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
if (value == (id)kCFNull) value = nil;
} break;
case YYEncodingTypeClass: {
Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromClass(v) : nil;
} break;
case YYEncodingTypeSEL: {
SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromSelector(v) : nil;
} break;
default: break;
}
}
if (!value) return;

if (propertyMeta->_mappedToKeyPath) {
NSMutableDictionary *superDic = dic;
NSMutableDictionary *subDic = nil;
for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
NSString *key = propertyMeta->_mappedToKeyPath[i];
if (i + 1 == max) { // end
if (!superDic[key]) superDic[key] = value;
break;
}

subDic = superDic[key];
if (subDic) {
if ([subDic isKindOfClass:[NSDictionary class]]) {
subDic = subDic.mutableCopy;
superDic[key] = subDic;
} else {
break;
}
} else {
subDic = [NSMutableDictionary new];
superDic[key] = subDic;
}
superDic = subDic;
subDic = nil;
}
} else {
if (!dic[propertyMeta->_mappedToKey]) {
dic[propertyMeta->_mappedToKey] = value;
}
}
}];

当模型值为自定义类型时,需要遍历和递归其映射表_mapper({属性名: 属性元}),通过消息发送 objc_msgSend 的方式,调用属性的 meta->_getter 方法进行取值,直至逐一转化成有效的JSON对象。

1
2
3
4
5
6
if (modelMeta->_hasCustomTransformToDictionary) {
// 校验数据
BOOL suc = [((id<YYModel>)model) modelCustomTransformToDictionary:dic];
if (!suc) return nil;
}
return result;

最后,判断使用者是否有额外的转换处理,并并校验数据的有效性。

注意:resultdic 指向的是同一个实例,所以如果 dic 在外部函数中被修改了,等同于修改了 result

总结

  • YYModel的使用无侵入性,采用Category的方式实现功能,比较灵活。
  • 容错方面,YYModel对数据类型做了详细的分类和判断,就算转换失败,也会自动留空(nil)。
  • 性能方面,使用 CoreFoundation、内联函数、runtime、缓存机制等方式,减少不必要的开销。

请我喝杯咖啡吧~

支付宝
微信