ruanpapa和又吉君写字的地方


  • 首页

  • 分类

  • 归档

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

2018年终总结

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

这么快又到了年底,仿佛上次写年终总结还是没多久之前的事情。
总感觉这一年过得特别快,也许是今年没有发生大事吧,更多的是在重复同样的事情,所以对时间的感知也变淡了。我想这起码不是坏事吧,就像我们总是生病了才来感慨健康的可贵,能意识到自己正处于平凡健康的状态并享受其中,这样的意境也不是人人都能做到的。

我的 2018

最想说的还是工作。这一年的角色不再是学生、实习生了,而是完全的职场人士了。需求没少做,从去年的些许慌乱,到今年的驾轻就熟,不会再因为需求的复杂绞尽脑汁而无所得了,得益于自己工作之中工作之外不断学习不断积累。能静下心来,拆解复杂的需求,理清依赖,并逐个完成,最后连成一线,这样的方式不仅让我能出色的完成任务,甚至有点享受其中。相比完成同样需求的其他同事,自己的速度比较快,测试时反馈的 bug 也比较少,看着自己写的代码规整、高效且正确地运行着,充当着庞大项目中的一员,这是工作带给我极强的正反馈。
年初用了近两个月钻研底层的 Clang,这段学习经历对我来说很重要,过程中对着十几篇文章试验下来都没办法正确运行 ,不停的失败,不停的自我怀疑,心里憋着一口气,真的不想放弃,很想要一窥这庞然大物的真面目。到终于运行成功了,还得对着枯燥庞大的 Clang 源码苦苦查找关键代码,最后终于完美实现了自己的需求,现在都还能记得那种欢心雀跃,回首望去,才长舒一口气,原来答案竟然是这么的简单,大道至简,事情的本质往往都是及其简单的。我想,这段学习经历能让我记很久。
年中用了四个月时间做了两个个人 APP,虽然第一个 APP 因为各种原因没办法上线,但是这两个多月的时间并没有白费,自己不仅学到了音视频处理的知识,还收获了一整套做个人 APP 的经验和节奏,这也是第二个 APP 能做的这么快的原因。在做个人 APP 的过程中,自己把产品、设计、服务端、客户端、测试各个角色都体验了一遍,相当的有挑战性。由于是个人项目,所以不再有明确的 deadline 催促着自己推进,完全靠自觉。在这四个月的时间里,基本上每个周六都会去公司做项目,甚至有时候连周日也会抽出几个小时接着写代码,希望这种专注的劲头自己能一直坚持下去。在这几种角色中,感觉自己服务端和测试还是比较薄弱,服务端用的是 LeanCloud 这种后端云,测试部分只能不断的进行功能测试,对项目的保障力度远远不够,明年要学习 Laravel 和单元测试,把这两个弱项补起来。我觉得自己最喜欢的还是客户端的部分,明年依然是深入钻研 iOS,同时拓展足够用的服务端的技能。
到现在年底了自己正在从基础的 runtime、runloop、多线程看起,平时做业务时对很多知识都是一知半解、不求甚解,以结果为导向,完成了需求就觉得万事大吉,对看起来正确运行的代码中的隐患一无所知,这样的状态时常让我觉得不安。自己现在的知识都是零散的,全是一个个的知识孤岛,这样记住的知识很容易就被遗忘,在短短一个多星期的复习中,已经能体会到其实很多技术之间都是有关联的,需要将它们串联起来,同时要主动创造这些底层技术的使用场景,在实践中用到的技术才能记得久。另一方面,感觉自己在当前所处的环境中还能学到的技术有限,通过自学或者自己做项目的确能提高能力,但是未必都适合用到工作中,我想应该是时候换个环境了,安逸的太久了容易产生惰性,丧失自己的竞争力。
由于去年体检时体重接近爆表了,所以今年花了几个月时间减肥,天天晚上水煮青菜,时不时出去跑步,一口气减了 10 几斤,减到自己高中时候的体重了🤣。不过这两个月公司开始加班制度,晚上不能回去自己弄水煮青菜吃了,跑步也没坚持下去,体重当然反弹了一点,所幸自己现在坚持每天上下班都走路,反弹的不是很多。明年还是得坚持跑跑步,感觉时不时运动精神头各方面都好一点。

上一年定下的目标

  • 写 6 篇技术文章,并更多的在社区中互动(完成):《iOS 表情键盘的完整实现》、《Source Editor Extension — Xcode 格式化 Import 的插件》、《[翻译]用 LLDB 调试 Swift 代码》、《Clang 之旅–使用 Xcode 开发 Clang 插件》、《Clang 之旅—[翻译]添加自定义的 attribute》、《Clang 之旅–实现一个自定义检查规范的 Clang 插件》、《iOS 截图的那些事儿》
  • 开发一个独立 APP(完成):《BBC NOW》、《打卡圈》
  • Clang、图像处理、逆向工程(完成度 66%)
  • 看 12 本书(基本没完成):《价值平均策略:获得高投资收益的安全简便方法》、《指数基金投资指南》、《Getting Started with LLVM Core Libraries》
  • 争取能做到 Github 的 contributions 亮起来一半(基本没完成)
  • UI 设计(完成)

我想要的 2019

  • 写 6 篇技术文章,并更多的在社区中互动:好习惯,继续坚持!
  • 研究 Runtime、Runloop 等底层技术,并探索实际应用场景
  • 维护个人 APP
  • 服务端 Laravel
ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之七

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

日常之七

ruanpapa和又吉君写字的地方

iOS 截图的那些事儿

发表于 2018-05-28 | 分类于 ruanpapa--技术贴 | | 阅读次数

同时按下 Home 键和电源键,咔嚓一声,就得到了一张手机的截图,这操作想必 iPhone 用户再熟悉不过了。我们作为研发人员,面对的是一个个的 View,那么该怎么用代码对 View 进行截图呢?
这篇文章主要讨论的是如何在包括 UIWebView 和 WKWebView 的网页中进行长截图,对应的示例代码在这儿:https://github.com/VernonVan/PPSnapshotKit。

UIWebView 截图

对 UIWebView 截图比较简单,renderInContext 这个方法相信大家都不会陌生,这个方法是 CALayer 的一个实例方法,可以用来对大部分 View 进行截图。我们知道,UIWebView 承载内容的其实是作为其子 View 的 UIScrollView,所以对 UIWebView 截图应该对其 scrollView 进行截图。具体的截图方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)snapshotForScrollView:(UIScrollView *)scrollView
{
// 1. 记录当前 scrollView 的偏移和位置
CGPoint currentOffset = scrollView.contentOffset;
CGRect currentFrame = scrollView.frame;
scrollView.contentOffset = CGPointZero;
// 2. 将 scrollView 展开为其实际内容的大小
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
// 3. 第三个参数设置为 0 表示设置为屏幕的默认缩放因子
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 4. 重新设置 scrollView 的偏移和位置,还原现场
scrollView.contentOffset = currentOffset;
scrollView.frame = currentFrame;
}

WKWebView 截图

虽然 WKWebView 里也有 scrollView,但是直接对这个 scrollView 截图得到的是一片空白的,具体原因不明。一番 Google 之后可以看到好些人提到 drawViewHierarchyInRect 方法, 可以看到这个方法是 iOS 7.0 开始引入的。官方文档中描述为:

Renders a snapshot of the complete view hierarchy as visible onscreen into the current context.

注意其中的 visible onscreen,也就是将屏幕中可见部分渲染到上下文中,这也解释了为什么对 WKWebView 中的 scrollView 展开为实际内容大小,再调用 drawViewHierarchyInRect 方法总是得到一张不完整的截图(只有屏幕可见区域被正确截到,其他区域为空白)。

不过,这样倒是给我们提供了一个思路,可以将 WKWebView 按屏幕高度裁成 n 页,然后将 WKWebView 一页一页的往上推,每推一页就调用一次 drawViewHierarchyInRect 将当前屏幕的截图渲染到上下文中,最后调用 UIGraphicsGetImageFromCurrentImageContext 从上下文中获取的图片即为完整截图。

核心代码如下(代码为演示用途,完整代码请从这里查看):

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
- (void)snapshotForWKWebView:(WKWebView *)webView
{
// 1
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
[webView.superview addSubview:snapshotView];
// 2
CGPoint currentOffset = webView.scrollView.contentOffset;
...
// 3
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
[webView removeFromSuperview];
[containerView addSubview:webView];
// 4
CGSize totalSize = webView.scrollView.contentSize;
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
webView.scrollView.contentOffset = CGPointZero;
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
[self drawContentPage:0 maxIndex:page completion:^{
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 8
[webView removeFromSuperview];
...
}];
}
- (void)drawContentPage(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
// 5
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(containerView.bounds), containerView.bounds.size.width, containerView.frame.size.height);
CGRect myFrame = webView.frame;
myFrame.origin.y = -(index * containerView.frame.size.height);
webView.frame = myFrame;
// 6
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
// 7
if (index < maxIndex) {
[self drawContentPage:index + 1 maxIndex:maxIndex completion:completion];
} else {
completion();
}
}

代码注意项如下(对应代码注释中的序号):

  1. 为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个“假”的 webView 到现在的位置上,并将真正的 webView “摘下来”。调用 snapshotViewAfterScreenUpdates 即可得到这样一个“假”的 webView
  2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后“还原现场”
  3. 用一个新的视图承载“真正的” webView,这个视图也是绘图所用到的上下文
  4. 将 webView 按照实际内容高度和屏幕高度分成 page 页
  5. 得到每一页的实际位置,并将 webView 往上推到该位置
  6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中
  7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成
  8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,“还原现场”

注意:我们的截图方法中有对 webView 的 frame 进行操作,如果其他地方如果有对 frame 进行操作的话,是会影响我们截图的。所以在截图时应该禁用掉其他地方对 frame 的改变,就像这样:

1
2
3
4
5
6
- (void)layoutWebView
{
if (!_isCapturing) {
self.wkWebView.frame = [self frameForWebView];
}
}

结语

当前 WKWebView 的使用越来越广泛了,我随意查看了内存占用:打开同样一个网页,UIWebView 直接占用了 160 MB 内存,而 WKWebView 只占用了 40 MB 内存,差距是相当明显的。如果我们的业务中用到了 WKWebView 且有截图需求的话,那么还是得老老实实完成的。

最后,本文对应的代码在这儿:https://github.com/VernonVan/PPSnapshotKit

ruanpapa和又吉君写字的地方

Clang 之旅--实现一个自定义检查规范的 Clang 插件

发表于 2018-04-17 | 分类于 ruanpapa--技术贴 | | 阅读次数

Clang 之旅系列文章:
Clang 之旅–使用 Xcode 开发 Clang 插件
Clang 之旅–[翻译]添加自定义的 attribute
Clang 之旅–实现一个自定义检查规范的 Clang 插件

前言

在 Clang 之旅系列文章开篇的时候,我说到过自己接触 Clang 的直接原因就是想实现一个自定义的检查需求:是否有办法在编译阶段检查某个方法的参数与返回值的类型相同,如果类型不一致的话能抛出编译错误的提示。现在我已经根据自己的需求完成了这个插件,这篇文章会讲解这个插件的实现思路,对应的代码在这里:https://github.com/VernonVan/SameTypeClangPlugin

具化需求

首先我先将需求具化一下,之前说的比较宽泛。

试想我们有这么一个函数 modelOfClass:

1
2
3
4
5
6
7
8
9
- (__kindof NSObject *)modelOfClass:(Class)modelClass
{
if ([modelClass isKindOfClass:[NSString class]]) {
return [[NSString alloc] init];
} else if ([modelClass isKindOfClass:[NSArray class]]) {
return [[NSArray alloc] init];
}
return nil;
}

modelOfClass 接受一个 Class 类型的参数,然后会根据 Class 对应的类进行不同的操作,最终返回处理好的 Class 对应类的实例对象。我们用 __kindof NSObject * 返回值类型来保证返回的一定是 NSObject 或者其子类,能保证的也只有这样而已。但是,存在这样一种错误的调用方式,但是却能通过编译:

1
2
3
4
5
6
7
8
@property (nonatomic, strong) NSString *myString;
@property (nonatomic, strong) NSArray *myArray;
- (void)someMethod
{
self.myString = [self modelOfClass:[NSString class]];
self.myArray = [self modelOfClass:[NSString class]];
}

可以发现,someMethod 中有两行 modelOfClass 的函数调用。第一行调用是正确的,NSString * 类型的属性 myString 调用时传入的是 [NSString class];第二行调用是错误的,NSArray * 类型的属性 myArray 调用时传入的是 [NSString class]。也就是说,在 Objective-C 语言中,并没有一种办法能够检查函数调用时参数类型和返回值类型是完全一致的。

这个需求是从我所在公司的项目中抽象简化出来的,大家看不出来这个函数究竟是用来干什么的,可能会觉得这个需求并不常见,没有什么通用性。但是这篇文章希望读者看了之后能以小见大,举一反三,更重要的是学到怎么样使用通用的方式,根据自己的需求实现自定义检查规范的 Clang 插件。

最终效果

我们来看看最终实现的效果:

演示效果

最终实现了上面所说的类型检查,同时还给出了对应的修改方法(FixIt),点击修改就能改成正确的参数类型🎉🎉🎉 下面就来说说具体是怎么实现的。

抽象语法树(Abstract syntax tree)

抽象语法树,英文简称为 AST,是编译过程中语法分析阶段的产物,也是我们作为外部开发者与 Clang 进行交互的最重要的方式。所以我们最重要的就是学会怎么样阅读、分析语法树。

在命令行中输入以下命令,打印 main.m 文件对应的语法树到命令行中:

1
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.3.sdk -fmodules -fsyntax-only -Xclang -ast-dump main.m

在我写这篇文章时 Xcode 版本是9.3,对应的是 iPhoneSimulator11.3.sdk,你需要进入该目录查看你的 sdk 版本,然后修改 -isysroot 命令后的 sdk 路径

打印出来的语法树如下图:

AST

编译前端 Clang 首先进行词法分析(Lexical Analysis),把源文件的字符流拆分一个一个的 token;然后 token 进入语法分析(Semantic Analysis),将这些 token 组合成语法树。左边的缩进代表了语法树节点的从属关系,语法树上的每一个节点的名字都能在 Clang 源码中找到对应的类。

从图中挑几个点来解释一下(对应图中的红色标注):

  1. ObjCImplementationDecl 节点代表了 Objective-C 类中的 @implementation 部分的内容

  2. ObjCMethodDecl 节点代表了 Objective-C 中的函数定义,我们在 Clang 源码中查看一下对应类的定义

    ObjCMethodDecl

    Clang 的文档注释可以说相当齐全了,ObjCMethodDecl 代表了一个类方法或者实例方法。所有的 public: 域中的方法都是我们可以用的,比如说 Selector getSelector() 可以获取该方法的 Selector,ArrayRef<ParmVarDecl*> parameters() 可以获取获取该方法的参数列表等等。

  3. 框中的语法块代表了源文件中 self.myString = [self modelOfClass:[NSString class]]; 语句,BinaryOperator 代表了二元操作符(包括赋值的“=”),可以通过 BinaryOperator 类的 Expr *getLHS() 和 Expr *getRHS() 分别取得“=”左右两边的语句。

详细的 AST 树的分析可以查看官方的教程:http://clang.llvm.org/docs/IntroductionToTheClangAST.html

那么多种的 AST 节点中应该怎么只获取自己感兴趣的节点呢?

Clang 提供了 ASTMatcher 类供我们进行 AST 节点的查找过滤,有一篇专门解释罗列各种各样的 ASTMatcher 的官方文档可以查看。

ASTMatcher

比如可以用 objcPropertyDecl 来匹配到 Objective-C 的类属性,ASTMatcher 可以用一种类似链式语法的方式将一系列的 Matcher 串起来,比如可以用 cxxRecordDecl(unless(hasName("X"))) 来匹配到满足类名不为 X 的所有 C++ 类。

具体的 ASTMatcher 的使用方法可以查看这篇教程:https://eli.thegreenplace.net/2014/07/29/ast-matchers-and-clang-refactoring-tools

实现思路

基础知识铺垫完了,现在我们来拆解一下我们的需求。首先我们需要有一种方式标记需要进行这种检查的函数,总不至于所有函数调用我们都去检查一遍吧😹 这时候就可以想到可以通过 attribute 的方式标记函数!

关于 attribute 的知识,可以查看孙源大神的这篇文章:Clang Attributes 黑魔法小记,讲解了多种常见不常见的 attribute 的使用场景

另外一篇就是官方关于如何在 Clang 中添加自定义的 attribute 的文档:How to add an attribute,我自己也翻译了这篇文档,请戳中文版。

这里不讲解怎么添加自定义的 attribute,比较简单,就是按最简单的模板添加的。添加完了之后,得在 modelOfClass 后面加上一句 __attribute__((objc_same_type)),代表 modelOfClass 在每次被调用时都会进行自定义的检查,这样才能出现上面演示效果图中的检查结果(objc_same_type 就是我所添加的 attribute 的名字)。

1
- (__kindof NSObject *)modelOfClass:(Class)modelClass __attribute__((objc_same_type))

####

具体该怎么检查呢?分成以下几个步骤:

  1. 首先判断语法树上的节点是否是赋值语句(Clang 中用 BinaryOperator 表征赋值语句)。如果是,进入第 2 步
  2. 用 BinaryOperator 的 getLHS() 、getRHS() 函数分别获得左右的表达式
  3. 如果左边表达式是 Objective-C 类的属性的话,获取该属性对应的类型 A。进入第 4 步
  4. 如果右边表达式是 Objective-C 的函数调用,且被调用的函数是有我们上面所定义 attribute((objc_same_type)) 的话(可以通过 ObjCMethodDecl 的 attrs() 方法获得 Objective-C 函数的所有的 attribute),获取该函数的参数对应的类型 B
  5. 对比 A 和 B 的类型是否一致,如果不一致,则弹出类型不一致的编译警告,并提出恰当的修改方法(如效果演示图所示)

具体的实现代码和使用方法查看 Github:https://github.com/VernonVan/SameTypeClangPlugin

结语

最终花了不到 200 行代码就完成了这个小小的功能,但是却花了我将近一个月的业余时间,中间也做了很多无用功,在错误的道路上走了一段时间才发现自己做的完全是错的,幸好最后还是成功找到了正确的方法。不过,自己也收获了很多的技能点,比如说阅读源码的能力,得益于 LLVM 良好的代码设计和模块化,让我一个门外汉也能比较快速的从庞大的代码中找到自己想要的部分;比如说 CMake 构建工程的知识、C++ 语言以及查找阅读英文文档的能力。收获还是比较多的🍹🍹🍹

接下来如果在 LLVM && Clang 这一块有其他的所得的话,会再撰文分享~

ruanpapa和又吉君写字的地方

Clang 之旅—[翻译]添加自定义的 attribute

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

Clang 之旅系列文章:
Clang 之旅–使用 Xcode 开发 Clang 插件
Clang 之旅–[翻译]添加自定义的 attribute
Clang 之旅–实现一个自定义检查规范的 Clang 插件

前言

这是 Clang 之旅系列的第二篇,自己想要完成的需求是:在编译阶段检查某个方法的参数与返回值的类型相同,如果类型不一致的话能抛出编译错误的提示。需要接触到 Clang 中关于 attribute 处理的代码,所以这篇先来翻译官方文档中添加自定义的 attribute 这一节,不得不说,虽然 Clang 的文档可以说是很标杆了,但是总有一种看了后面忘了前面的感觉,可能是 Clang 比较庞大,涉及专有词汇比较多的原因,所以我会偏向意译多一点,试图用更加易懂的表达组织语言,也是加深自己的记忆吧。

怎样添加 attribute

attribute 是一种可以附加到程序结构中的数据形式,允许开发人员传递信息给编译器来实现各种需求。例如,attribute 可以用来改变在程序构造时生成的代码,或者用来提供额外的信息给静态分析。本文档讲解如何添加一个自定义的 attribute 到 Clang 中。现有 attribute 列表的文档可以在这里找到。

attribute 基础知识

Clang 中的 attribute 涉及到三个阶段:解析 attribute 、从已解析的 attribute 转换成语法树上的 attribute、对 attribute 进行处理。

attribute 的解析可以采用多种语法形式,例如 GNU、C++ 11 和 Microsoft 形式,还由 attribute 提供的其他信息来确定。最终,解析好的 attribute 用一个 AttributeList 对象来表示。这些解析好的 attribute 会链成一个 attribute 链,加到声明或者定义上。attribtue 的解析是由 Clang 自动完成的,除了那些关键字 attribute。关键字的解析和 AttributeList 对象的生成必须由我们手动完成。

最后,Sema::ProcessDeclAttributeList() 带着 Decl 类型和 AttributeList 类型的参数被调用,此时解析好的 attribute 就会被转化成语法树上的 attribute。这个处理依赖于 attribute 的属性定义和语义要求。最后的结果就是语法树上的 attribute 对象可以从 Decl 对象获取到,也就是通过调用 Decl::getAttr<T>() 来获取。

语法树上的 attribute 的结构同样也受到 Attr.td 文件中的定义所限制。这个定义会自动生成 attribute 的实现所用到的功能,包括生成 clang::Attr 的子类、解析器所用到的信息和某些 attribute 自动进行的语义分析等等。

include/clang/Basic/Attr.td

添加新的 attribute 到 Clang 的第一个步骤就是把其定义添加到 include/clang/Basic/Attr.td。这个定义必须从 Attr 或者其子类继承。大多数 attribute 会直接从 InheritableAttr 继承,InheritableAttr 指定了这个 attribute 可以通过它所关联的 Decl 稍后进行重声明。如果这个 attribute 是作用于类型而不是声明,那么这种 attribute 应该从 TypeAttr 派生,并且通常不会被赋予 AST 表示(注意本文档并不讲解生成类型所用的 attribute)。一个继承于 IgnoredAttr 的 attribute 会被解析,但是会在被使用的时候产生一个 “被忽略的属性” 的警告,这种处理方法在某个属性支持别的前端而不支持 Clang 的情况下是很有用的。

这个定义能指定 attribute 的一些关键部分,比如 attribute 的名字、attribute 支持的拼写、attribute 的参数等等。Attr 类型中的大多数成员变量都不需要派生定义,缺省的就足够了。但是,每个 attribute 都需要至少指定 拼写列表、subject 列表和文档列表。

拼写

所有 attribute 都需要指定一个拼写列表,表示拼写 attribute 的方式。比如某个 attribute 可能会包含关键字拼写, C++11 拼写和 GNU 拼写。空的拼写列表也是允许的并且可能对隐式创建的 attribute 有用。以下是支持的拼写的表格:

拼写 描述
GNU 用 GNU 风格 __attribute__((attr)) 语法和位置拼写
CXX11 用 C++ 风格 [[attr]] 语法拼写。如果该 attribute 是由 Clang 所使用的,那么应该设置命名空间为 "clang"
Declspec 用 Microsoft 风格 __declspec(attr) 语法拼写
Keyword 这个 attribute 用关键字的方式拼写,并且需要自定义解析
GCC 指定两种拼写:首先是 GNU 风格拼写;然后是 C++ 风格拼写,命名空间为 gnu。只能为支持 GCC 的 attribute 指定这个拼写。
Pragma attribute 用 #pragma 的形式拼写,并且需要在预处理器中执行自定义的处理。如果该 attribute 是由 Clang 所使用的,那么应该设置命名空间为 "clang"。需要注意这个拼写并不能被用于声明语句中。
Subjects

每个 attribute 都有一个或者多个 subject。如果 attribute 被使用到了一个不在 subject 列表上的 subject,就会自动显示诊断信息。 这个信息是警告还是错误是由 attribute 中的 SubjectList 决定的,默认的是警告。显示给用户的诊断信息将根据 subject 列表自动确定,但是也可以在 SubjectList 中指定自定义诊断参数。不符合 subject 列表导致的诊断信息要么是 diag::warn_attribute_wrong_decl_type,要么是 diag::err_attribute_wrong_decl_type。具体参数的枚举值可以从 include/clang/Sema/AttributeList.h 找到。如果先前未使用的 Decl 节点被添加到 SubjectList 中,则可能需要更新用于自动确定 utils/TableGen/ClangAttrEmitter.cpp 中的诊断参数的逻辑。

所有在 SubjectList 中的 subject 要么是在 DeclNodes.td 中定义的 Decl 节点,要么就是在 StmtNodes.td 中定义的 statement 节点。不过,可以生成 SubsetSubject 对象来创建更加复杂的 subject。每个这样的对象都有一个它所属的基本对象(必须是一个 Decl 或 Stmt 节点,而不是一个 SubsetSubject 节点),还有一些自定义代码在确定某个 attribute 是否属于该对象时被调用。例如,一个 NonBitField SubsetSubject 关联到 FieldDecl 类,同时会测试给定的 FieldDecl 是否是一个位字段。当在 SubjectList 中指定了一个 SubsetSubject 时必须同时提供一个自定义的诊断信息参数。

attribute 的 subject 列表会在 HasCustomParsing 设为 1 的情况下自动进行诊断检查。

文档

所有的 attribute 都必须具有某种形式的文档。文档是通过每天运行的服务器端进程在公共服务器上生成的。通常来说,attribute 的文档是在 include/clang/Basic/AttrDocs.td 中单独定义的,以文档属性命名。

如果 attribute 不是通用的,或者是隐式创建的没有对应拼写的 attribuet,则可以将文档列表变量设置为 Undocumented。否则,该 attribute 应将其文档添加到 AttrDocs.td。

文档属性是从 Documentation tablegen 类型继承而来的,所有的派生类型都必须创建一个文档类别和设置文档本身内容。此外,它还可以为 attribute 指定一个自定义的标题,否则会选择默认的标题。

现在有四种预先定义好的文档类别:DocCatFunction 对应函数的 attribute,DocCatVariable 对应到变量的 attribute,DocCatType 对应类型的 attribute,DocCatStmt 对应声明的 attribute。自定义文档类别应该用于具有类似功能的 attribute 组。自定义类别非常适合用来为组中的 attribute 提供概述信息。

文档内容(包括 attribute 的内容或者类别的内容)是用 reStructuredText(RST)格式写的。

在编写该 attribute 的文档之后,应该对其在本地对其进行测试,以确保在服务器上生成文档不会有问题。本地测试需要重新构建 clang-tblgen。要生成 attribute 文档,请执行以下命令:

1
clang-tblgen -gen-attr-docs -I /path/to/clang/include /path/to/clang/include/clang/Basic/Attr.td -o /path/to/clang/docs/AttributeReference.rst

在本地进行测试时,不要对 AttributeReference.rst 提交更改。该文件是由服务器自动生成的,并且对该文件所做的任何更改都将被覆盖。

参数

attribute 可以选择指定可以传递给 attribute 的参数列表。attribute 的参数指定 attribute 的解析形式和语义形式。例如,如果 Args 是 [StringArgument<"Arg1">, IntArgument<"Arg2">],那么 __attribute__((myattribute("Hello", 3))) 就是一个合法的使用方式;这个 attribute 在解析时要求有两个参数:一个 string 类型一个 integer 类型。

每个参数都有个名字和一个用来指定这个参数是否为可选的标志。参数关联的 C++ 类型由参数定义类型确定。如果现有参数类型不足,则可以创建新类型,但需要修改 utils/TableGen/ClangAttrEmitter.cpp 才能正确支持该新类型。

其他属性

Attr 的定义还具有其他变量来控制 attribute 的行为。其中有很多是用于特殊用途的,超出了本文档的范围,但有一些还是值得提上一嘴的。

如果 attribute 的解析形式更加复杂或者和语义形式不同,则可以将 HasCustomParsing 变量设置为 1,并且可以针对特殊情况修改 Parser::ParseGNUAttributeArgs() 中的解析代码。请注意,这仅适用于具有 GNU 拼写的 attribute;__declspec 拼写的 attribute 现在是忽略这个标志的,并由 Parser::ParseMicrosoftDeclSpec 负责解析。

请注意,把 HasCustomParsing 设置为 1 将不再使用通用的 attribute 处理逻辑,需要额外的处理来确保该 attribute 能使用。

如果该 attribute 不通过模板声明实例化,则将 Clone 成员变量设置为 0。默认情况下,所有的 attribute 都将通过模板进行实例化。

不需要 AST 节点的 attribute 应该将 ASTNode 变量设置为 0 以避免污染 AST。请注意,从 TypeAttr 或 IgnoredAttr 继承的类都不会自动生成 AST 节点。所有其他属性默认会生成一个 AST 节点。该 AST 节点是 attribute 的语义表示。

LangOpts 变量指定了 attribute 所需的语言选项列表。例如,所有的 CUDA-specific 的 attribuet 都将 LangOpts 字段指定为 [CUDA],并且当 CUDA 语言选项未启用时,会发出“attribute ignored”的警告诊断。由于语言选项不是自动生成的节点,因此必须手动创建新的语言选项,并应指定 LangOptions 类所使用的拼写。

可以基于 attribute 的拼写列表为该 attribute 生成自定义的存取器。例如,如果某个 attribute 有两种不同的拼写:’foo’ 和 ‘bar’,则可以创建访问器:[Accessor<"isFoo", [GNU<"Foo">]>, Accessor<"isBar",[GNU<"Bar">]>]。这些存取器将在该 attribute 的语义形式上生成,不接受任何参数并返回一个布尔值。

不需要自定义语义分析的 attribute 应该将 SemaHandler 变量设为 0。请注意,任何从 IgnoredAttr 继承的 attribute 都不会自动进行语义处理。所有其他 attribute 都使用默认的语义处理。没有语义处理的 attribute 都不会有解析好的 attribute Kind 枚举器。

指定 Target 的 attribute 可能会与不同 Target 的 attribute 共用一个拼写。例如,ARM 和 msp430 Target 都有一个拼写为 GNU<"interrupt"> 的 attribute,但各自有不同的解析方式和语义要求。为了支持这个特性,继承自 TargetSpecificAttribute 的 attribute 可以指定 ParseKind 变量。这个变量在共用拼写的所有参数之间应该是相同的,并且对应于解析 attribute 的 Kind 的枚举器。这允许 attribute 共用一种解析类型,但具有不同的语义属性。例如,AttributeList::AT_Interrupt 是共用的解析类型,但 ARMInterruptAttr 和 MSP430InterruptAttr 是各自的语义属性。

默认情况下,当声明为 merging attribute 时,该 attributes 不会被复制。但是,如果在此合并阶段中可以复制某个 attribute,那么将 DuplicatesAllowedWhileMerging 变量设置为 1,该 attribute 就会被合并。

默认情况下,attribute 的参数在上下文中被解析。如果应该在上下文中解析 attribute 的参数(类似于解析 sizeof 表达式的参数的方式),请将 ParseArgumentsAsUnevaluated 设置为 1。

样板代码

声明 attribute 的所有的语义处理都在文件 lib/Sema/SemaDeclAttr.cpp 中,并且通常都从 ProcessDeclAttribute() 函数开始。如果这个 attribute 是一个“简单的” attribute,也就是说这个 attribute 除了自动生成的内容之外不需要自定义的语义处理,那么就添加 handleSimpleAttribute<YourAttr>(S, D, Attr); 函数到 switch 语句中。否则,编写一个新的 handleYourAttr() 函数,并将其添加到 switch 语句中。不要直接在 case 语句中实现处理逻辑。

除非 attribute 的定义中另有规定,否则将自动处理解析 attribute 的常见语义检查,包括诊断不属于给定 Decl 的解析的 attribute、确保传递正确的最小数量的参数等等。

如果 attribute 要加上额外的警告,那么在 include/clang/Basic/DiagnosticGroups.td 文件中定义一个 DiagGroup。如果只有一个诊断信息的话,直接在 DiagnosticSemaKinds.td 文件中使用 InGroup<DiagGroup<"your-attribute">> 也是可以的。

所有为你自定义的 attribute 所生成的诊断信息,包括自动生成的(比如 subject 和参数个数),都应该有一个对应的测试用例。

语义处理

大多数 attribute 被实现为对编译器有一定的影响。例如,修改生成代码的方式,或为分析过程添加额外的语义检查等,将 attribute 的定义和转换添加到该 attribute 的语义表示中,剩下的就是实现 attribute 的自定义逻辑。

可以使用 hasAttr<T>() 方法来查询 clang::Decl 对象中是否有 attribute。可以使用 getAttr<T> 来获取一个指向 attribute 的指针。

ruanpapa和又吉君写字的地方

Clang 之旅--使用 Xcode 开发 Clang 插件

发表于 2018-03-16 | 分类于 ruanpapa--技术贴 | | 阅读次数

Clang 之旅系列文章:
Clang 之旅–使用 Xcode 开发 Clang 插件
Clang 之旅–[翻译]添加自定义的 attribute
Clang 之旅–实现一个自定义检查规范的 Clang 插件

前言

最近在跟老大的聊天中聊到了一个比较特殊的需求:是否有办法在编译阶段检查某个方法的参数与返回值的类型相同,如果类型不一致的话能抛出编译错误的提示。这似乎已经不是 Objective-C 或者 Swift 的语言语法本身所能解决的了,老大还指点了可以从编译器等底层中进行研究。于是,我踏进了 Clang 和 LLVM 的大门。

我打算将 Clang 的研究心得分为几篇文章来写,这是 Clang 之旅的第一篇,主要讲如何用 Xcode 编译 Clang,以及实现一个简单的 Clang 插件并挂载到 Xcode 中参与编译流程,算是进入 Clang 的门槛。只是,这门槛就狠狠地让我吃了苦头,Google 找到好几篇博客讲怎么编译 Clang 的,但是也有一些年头了,版本比较旧,编译出来的 Clang 不能运行在现在的系统上;还有一些写的比较含糊,漏了某些关键步骤,导致花了好几个小时跟着教程做下来最后还是一堆 error;而且试错的成本还是比较高的,下载的源码有1G多(考虑从 Github 下载的速度🙄,需要挂个代理),完整编译出来有20G左右,我的15款 Macbook Pro 大概需要疯狂编译2个小时……如果不能接受这些的话,还是别尝试了,很遗憾,你连见到 Clang 真容的机会都没有┑( ̄Д  ̄)┍

llvm大小

编译源码

准备工作

Clang 需要用 CMake 来编译,CMake 的安装方法可以参考这篇文章:Mac 安装 CMake & CMake Command Line Tools,建议对 CMake 完全不了解的同学可以先补充一点 CMake 的基本知识,这样能更容易理解接下来要做的事情,CMake 的入门知识可以参考:CMake 入门实战

下载源码

首先创建 LLVM 的源码路径及编译路径:

1
2
3
4
5
cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm // 将 llvm 目录的所有者指定为当前用户
cd llvm
export LLVM_HOME=`pwd` // 设置当前目录(/opt/llvm)为 LLVM_HOME 目录

接下来从 Github clone 源代码(注意这几条语句中的 release_60,在当前时间2018.3.18时,我试过了 release_33、release_39,编译出来的 Clang 插件在运行的时候都会报 NSUUID 的 Nullability 错误,应该是这些版本不支持 Objective-C 后来加的 Nullability 特性,所以我下载了当前最新的 release_60 分支。一般来说,最新分支是兼容已有特性的,所以优先下载最新分支,分支查看可以参照下图):

1
2
3
4
git clone -b release_60 git@github.com:llvm-mirror/llvm.git llvm
git clone -b release_60 git@github.com:llvm-mirror/clang.git llvm/tools/clang
git clone -b release_60 git@github.com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_60 git@github.com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

llvm最新分支.png

编译源码

生成 Xcode 工程(也可以直接用命令行编译,不过大家平时可能看习惯了 Xcode 工程,所以用 Xcode 编译比较习惯)

1
2
mkdir llvm_build; cd llvm_build
cmake -G Xcode ../llvm -DCMAKE_BUILD_TYPE:STRING=MinSizeRel

生成的文件如下:

Xcode工程.png

打开 Xcode 工程,选择自动创建 Schemes:

自动创建Schemes.png

然后编译 Clang 和 libClang(可以随时终止编译,再次点击编译会从上次停止的地方继续进行):

编译Clang和libClang

这里可能需要1个多小时才能完成编译,如无意外,编译成功!

编写你的第一个插件

这个插件实现的功能就是打印语法树上所有节点的类名以及父类名,创建 Clang 插件的整体步骤如下图:

创建插件.png

首先修改源代码目录 /opt/llvm/llvm/tools/clang/tools 下的 CMakeLists.txt 文件,添加一个新的编译目标,直接在 CMakeLists.txt 的最后面添加上一行,如下图:
添加新的编译目标.png

然后在 tools 目录下添加 MyPlugin 文件夹,文件夹里面新增两个文件 CMakeLists.txt 和 MyPlugin.cpp,这里先不讲解具体文件中的内容,目的是想让插件跑起来,看到运行效果。

CMakeLists.txt 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add_llvm_loadable_module(MyPlugin
MyPlugin.cpp
PLUGIN_TOOL clang
)
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(MyPlugin PRIVATE
clangAST
clangBasic
clangFrontend
clangLex
LLVMSupport
)
endif()

MyPlugin.cpp 文件如下:

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
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
namespace MyPlugin
{
class MyASTVisitor: public
RecursiveASTVisitor < MyASTVisitor >
{
private:
ASTContext *context;
public:
void setContext(ASTContext &context)
{
this->context = &context;
}
bool VisitDecl(Decl *decl)
{
if (isa < ObjCInterfaceDecl > (decl)) {
ObjCInterfaceDecl *interDecl = (ObjCInterfaceDecl *)decl;
if (interDecl->getSuperClass()) {
string interName = interDecl->getNameAsString();
string superClassName = interDecl->getSuperClass()->getNameAsString();
cout << "-------- ClassName:" << interName << " superClassName:" << superClassName << endl;
}
}
return true;
}
};
class MyASTConsumer: public ASTConsumer
{
private:
MyASTVisitor visitor;
void HandleTranslationUnit(ASTContext &context)
{
visitor.setContext(context);
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
};
class MyASTAction: public PluginASTAction
{
public:
unique_ptr < ASTConsumer > CreateASTConsumer(CompilerInstance & Compiler, StringRef InFile) {
return unique_ptr < MyASTConsumer > (new MyASTConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector < std::string >& args)
{
return true;
}
};
}
static clang::FrontendPluginRegistry::Add
< MyPlugin::MyASTAction > X("MyPlugin",
"MyPlugin desc");

再次在 llvm_build 目录下 CMake 一下

1
cmake -G Xcode ../llvm -DCMAKE_BUILD_TYPE:STRING=MinSizeRel

然后重新打开 LLVM.xcodeproj 工程,会发现多了一个 MyPlugin 的编译目标,选中进行编译。

编译myPlugin.png

编译成功之后,就可以得到一个 MyPlugin.dylib 的 Clang 插件了~为了方便,我将 MyPlugin.dylib 放在桌面上:

MyPlugin插件.png

使用插件

命令行中使用插件

首先用命令行对单文件测试一下刚刚生成的 Clang 插件是否正确,新建一个测试用文件 test.m 放在桌面,test.m 如下:

1
2
3
4
5
6
7
8
9
10
11
#import<UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
@implementation ViewController
- (instancetype)init
{
if(self = [super init]){
}
return self;
}
@end

现在我的 test.m 和 MyPlugin.dylib 都在桌面上了(当然也可以放在不同的目录下,只要在待会用到这两个文件的地方指定各自的绝对路径就行,这里是为了方便叙述)

文件结构

接着命令行 cd 到桌面,然后执行以下命令就可以看到结果了:

1
/opt/llvm/llvm_build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk -Xclang -load -Xclang ./MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin -c ./test.m

注意:

  1. 我编译出来的 clang 在 /opt/llvm/llvm_build/Debug/bin/clang 目录中,如果你与我的路径不一样则指定为你对应的路径

  2. 在我写这篇文章时 Xcode 版本是9.2,对应的是 iPhoneSimulator11.2.sdk,你需要进入该目录查看你的 sdk 版本

如无意外,命令行中会出现一大堆输出:

命令行输出

Xcode 中使用插件

接下来讲怎么样在 Xcode 使用我们刚刚编译出来的插件(随着 Xcode 变得封闭,插件挂载到 Xcode 上运行在未来的版本中可能会被禁止)。

首先 hack Xcode,才能使 Xcode 指向我们自己编译的 Clang:

下载 XcodeHacking.zip 并解压,里面有 HackedBuildSystem.xcspec 和 HackedClang.xcplugin 两个文件,这里可能需要修改一下 HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec 文件,将 ExecPath 的值修改为你编译出来的 Clang 的目录:
修改HackedClang.xcspec

然后 cd 到解压的 XcodeHacking 目录,将这两个文件用命令行移动到对应的目录下:

1
2
sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

​

然后重启 Xcode,点击 Target 的 Build Settings,修改 Compiler for C/C++/Objective-C 项为 Clang LLVM Trunk(不进行第1步中 hack Xcode 操作的话是不会有这个选项的)

Complier.png

然后修改 OTHER_CFLAGS 选项:
OTHER_CFLAGS.png

1
-Xclang -load -Xclang /Users/Vernon/Desktop/MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin

注意

  1. 将 /Users/Vernon/Desktop/MyPlugin.dylib 修改为你生成的插件对应的目录
  2. 如果编译中出现一大堆系统库的 symbol not found 错误的话,可以在上述命令的最后手动指定你的 SDK 目录:-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk

最后编译你的项目,然后快捷键 Command+9 跳到 Show the Report navigator,选中刚刚的编译报告,注意下图中每个文件右上角都有可以点击展开的按钮,展开后就能看到我们插件的输出了(下图4为对应输出)。Nice~
查看结果

结语

文章不长,只是这看似简单的过程也花了我一个多星期的业余时间,写下这个系列文章一是为了记录自己这钻研的过程,以后也可查询,二是希望如果有人能看到这篇拙文可以省下一点时间,更快的踏进 LLVM 和 Clang 的世界探索。

接下来会根据我的个人需求尝试给 Clang 添加自定义的 attribute,如果有所心得,会撰文分享,敬请期待~

ruanpapa和又吉君写字的地方

《指数基金投资指南》读后感

发表于 2018-03-02 | 分类于 投资理财 | | 阅读次数

自己正式工作也已经半年多了,刚参加工作虽然工资不是很高,但是自己还是比较有规划的,没有比较花钱的嗜好,也有记账的习惯,所以慢慢也攒下了一点钱。有了钱就要考虑怎么安置的问题,自己经过了几个月业余时间的学习与权衡,最终确定了自己的投资理财方案:

  1. 这半年多攒下的钱留1万左右放到余额宝作为日常花销(餐饮、房租水电、购物等),以后每个月发了工资都会补充这部分到1万左右,足以应付自己的2~3个月的日常生活所需。最近因为国家把货币基金纳入了M2,总量1.6万亿的余额宝也开始了每日限额发售,不过现在还是有其他的替代品的,比如微信零钱通、支付宝中的余利宝、天天基金的活期宝等,这些都属于低风险、收益稳定的货币基金,七日年化收益率都有4.2%~4.4%,高过余额宝的4.1%,T+1日开始计算收益,同时实时转出,从资金灵活度、收益等角度都不输余额宝。

  2. 其余攒下的钱都购买银行的低风险的理财产品,年化收益能到4.8%~5.2%左右,通常期限都为90天以上,一年以下。不过在到期日之前无法赎回,可以说是完全没有灵活度的,不过相当的稳定。考虑到自己暂时不用添置大件,所以这笔钱放在低风险中比较高收益的产品中是最为理想的。

  3. 以后每个月发了工资,会填充一部分到第1点中,其余的大部分会根据自己的眼光进行基金定投。基金定投自己之前实习期间已经进行过了一段时间的学习与尝试,后来为了买电脑全取了出来,所以那次3个月定投没有赚到什么钱。现在有了稳定的收入,自己又再进行了一番学习。比较看好价值平均策略,这一部分留到关于《价值平均策略:获得高投资收益的安全简便方法》的读后感再分享,另一个就是一直都有留意的雪球大V银行螺丝钉的”盈利收益率”获取超额收益的方法,接下来讲讲自己看完钉大的《指数基金投资指南》感想。

    ​

本书作者@银行螺丝钉是一个90后,看起来有着超出年龄的成熟稳重,不仅从外观长相、说话谈吐,还从在雪球、微信公众号的耐心回答都能体现,同时他还坚持每天更新基金的估值表3年多,这样稳重、有毅力的人总是比较让人放心。同时他专注于低估值指数基金投资,以格雷厄姆和巴菲特为师,建立起基于估值的投资体系,以我个人对巴菲特的了解,他所坚持的价值投资、长期持有的风格是比较适合我的,而不是通过大量的技术分析,对赌高风险博取高收益,所以我选择尝试钉大所提倡的“盈利收益率”进行估值以期获得超额收益的定投方法。

盈利收益率的定义是:盈利收益率=股票盈利/股票市值,其实也就是市盈率的倒数,这个指标反映了该股票的单位盈利能力。根据对全世界各个国家的股市历史数据进行观察,发现绝大多数股市,在熊市最低谷的时候,盈利收益率都会在10%以上,这是格雷厄姆买股票的第一个标准。第二个标准是只有当股票的盈利收益率是国债利率的两倍以上时,我们才会去考虑股票。

(1). 盈利收益率要大于10%

(2). 盈利收益率要在国债利率的两倍以上

符合这两个标准的就是当前低估的股票基金,我们可以不断定投符合这两个标准的基金,当越是低估时,我们定投对应的那一期的额度越要增加,这样我们在低位买入的份额会更多,当基金到了高位,我们就能获得更多的收益。

当基金估值从低估回到正常估值的时候,我们就停止定投,转为持有基金,等到估值到了高估的时候,我们就可以分批卖出我们持有的基金。以沪深300指数为例,估值主要集中在10-18PE,低于10PE就是低估阶段,此时坚持定投;10-18PE为正常估值,持有基金;高于18PE就是高估阶段,分批卖出。具体哪些基金处于估值的哪个阶段,可以关注钉大的微信公众号,每个交易日更新估值数据。

我个人还是比较看好这种方式的,也解决了之前普通定投的卖出止损等的一些问题。最后就是心态的问题,短期内要有拥抱下跌的准备,现在的下跌亏损意味着我们在低点多买入了份额,而这不正是很多人在回测复盘的时候想要做到的吗,千万不要被黎明前的黑暗所吓倒!

ruanpapa和又吉君写字的地方

(翻译)用 LLDB 调试 Swift 代码

发表于 2018-02-06 | 分类于 ruanpapa--技术贴 | | 阅读次数
  • 原文地址:Debugging Swift code with LLDB
  • 原文作者:Ahmed Sulaiman

用 LLDB 调试 Swift 代码

作为工程师,我们花了差不多 70% 的时间在调试上,剩下的 20% 用来思考架构以及和组员沟通,仅仅只有 10% 的时间是真的在写代码的。

调试就像是在犯罪电影中做侦探一样,同时你也是凶手。

— Filipe Fortes 来自 Twitter

所以让我们在这70%的时间尽可能愉悦是相当重要的。LLDB 就是来打救我们的。奇妙的 Xcode Debugger UI 展示了所有你可用的信息,而不用敲入任何一个 LLDB 命令。然而,控制台在我们的工作中同样也是很重要的一部分。现在让我们来分析一些最有用的 LLDB 技巧。我自己每天都在用它们进行调试。

从哪里开始呢?

LLDB 是一个庞大的工具,内置了很多有用的命令。我不会全部讲解,而是带你浏览最有用的命令。这是我们的计划:

  1. 获取变量值: expression, e, print, po, p
  2. 获取整个应用程序的状态以及特定语言的命令:bugreport, frame, language
  3. 控制应用的执行流程:process, breakpoint, thread, watchpoint
  4. 荣誉奖:command, platform, gui

我还准备好了有用的 LLDB 命令说明和实例的表格,有需要的可以把它贴在 Mac 上面记住这些命令 🙂

通过这条链接下载全尺寸的版本 —  https://www.dropbox.com/s/9sv67e7f2repbpb/lldb-commands-map.png?dl=0

1. 获取变量值和状态

命令:expression, e, print, po, p

调试器的一个基础功能就是获取和修改变量的值。这就是 expression 或者 e 被创造的原因(当然他们还有更高级的功能)。您可以简单的在运行时执行任何表达式或命令。

假设你现在正在调试方法 valueOfLifeWithoutSumOf() :对两个数求和,再用42去减得到结果。

继续假设你一直得到错误的结果并且你并不知道是什么原因。所以你可以做以下的事来找到问题:

或者。。。使用 LLDB 表达式在运行时修改值才是更好的方法,同时可以找出问题是在哪里出现的。首先,在你感兴趣的地方设置一个断点,然后运行你的应用。

为了用 LLDB 格式打印指定的变量你应该调用:

1
(lldb) e <variable>

使用相同的命令来执行一些表达式:

1
(lldb) e <expression>

1
2
3
4
5
6
7
(lldb) e sum
(Int) $R0 = 6 // 下面你也可以用 $R0 来引用这个变量(在本次调试过程中)
(lldb) e sum = 4 // 修改变量 sum 的值
(lldb) e sum
(Int) $R2 = 4 // 直到本次调试结束变量 sum 都会是 "4"

expression 命令也有一些标志。在 expression 后面用双破折号 -- 将标志和实际的表达式分隔开,就像这样:

1
(lldb) expression <some flags> -- <variable>

expression 命令差不多有30种不同的标志。我鼓励你多去探索它们。在终端中键入以下命令可以看到完整的文档:

1
2
3
> lldb
> (lldb) help # 获取所有变量的命令
> (lldb) help expression # 获取所有表达式的子命令

我会在下列 expression 的标志上多停留一会儿:

  • -D <count> (--depth <count>)  — 设置在转储聚合类型时的最大递归深度(默认为无穷大)。
  • -O (--object-description)  — 如果可能的话,使用指定语言的描述API来显示。
  • -T (--show-types)  — 在转储值的时候显示变量类型。
  • -f <format> (--format <format>) — 指定一种用于显示的格式。
  • -i <boolean> (--ignore-breakpoints <boolean>) — 在运行表达式时忽略断点。

假设我们有一个叫 logger 的对象,这个对象有一些字符串和结构体类型的属性。比如说,你可能只是想知道第一层的属性,那只需要用 -D 标志以及恰当的层级深度值,就像这样:

1
2
3
4
5
6
(lldb) e -D 1 -- logger
(LLDB_Debugger_Exploration.Logger) $R5 = 0x0000608000087e90 {
currentClassName = "ViewController"
debuggerStruct ={...}
}

默认情况下,LLDB 会无限地遍历该对象并且给你展示每个嵌套的对象的完整描述:

1
2
3
4
5
6
(lldb) e -- logger
(LLDB_Debugger_Exploration.Logger) $R6 = 0x0000608000087e90 {
currentClassName = "ViewController"
debuggerStruct = (methodName = "name", lineNumber = 2, commandCounter = 23)
}

你也可以用 e -O -- 获取对象的描述或者更简单地用别名 po,就像下面的示例一样:

1
2
3
(lldb) po logger
<Logger: 0x608000087e90>

并不是很有描述性,不是吗?为了获取更加可阅读的描述,你自定义的类必须遵循 CustomStringConvertible 协议,同时实现 var description: String { return ...} 属性。接下来只需要用 po 就能返回可读的描述。

在本节的开始,我也提到了 print 命令。基本上 print <expression/variable> 就等同于 expression -- <expression/variable>。但是 print 命令不能带任何标志或者额外的参数。

2. 获取整个 APP 的状态和指定语言的命令

bugreport, frame, language

你是否经常复制粘贴崩溃日志到任务管理器中方便稍后能考虑这个问题吗?LLDB 提供了一个很好用的命令叫 bugreport,这个命令能生成当前应用状态的完整报告。在你偶然触发某些问题但是想在稍后再解决它时这个命令就会很有帮助了。为了能恢复应用的状态,你可以使用 bugreport 生成报告。

1
(lldb) bugreport unwind --outfile <path to output file>

最终的报告看起来就像下面截图中的例子一样:


bugreport 命令输出的示例。

假设你想要获取当前线程的当前栈帧的概述,frame 命令可以帮你完成:

使用下面的代码片段来快速获取当前地址以及当前的环境条件:

1
2
3
(lldb) frame info
frame #0: 0x000000010bbe4b4d LLDB-Debugger-Exploration`ViewController.valueOfLifeWithoutSumOf(a=2, b=2, self=0x00007fa0c1406900) -> Int at ViewController.swift:96

这些信息在本文后面将要说到的断点管理中非常有用。

LLDB 有几个指定语言的命令,包括C++,Objective-C,Swift 和 RenderScript。在这篇文章中,我们重点关注 Swift。这是两个命令:demangle 和 refcount。

demangle 正如其名字而言,就是用来重组 Swift 类型名的(因为 Swift 在编译的时候会生成类型名来避免命名空间的问题)。如果你想了解多一点的话,我建议你看 WWDC14 的这个分享会 —  “Advanced Swift Debugging in LLDB”。

refcount 同样也是一个相当直观的命令,能获得指定对象的引用数量。一起来看一下对象输出的示例,我们用了上一节讲到的对象 — logger:

1
2
3
(lldb) language swift refcount logger
refcount data: (strong = 4, weak = 0)

当然了,在你调试某些内存泄露问题时,这个命令就会很有帮助。

3. 控制应用的执行流程

process, breakpoint, thread

这节是我最喜欢的一节,因为在 LLDB 使用这几个命令(尤其是 breakpoint 命令),你可以在调试的时候使很多常规任务变得自动化,这样就能大大加快你的调试工作。

通过 process 基本上你就可以控制调试的过程了,还能链接到特定的 target 或者停止调试器。 但是因为 Xcode 已经自动地帮我们做好了这个工作了(Xcode 在任何时候运行一个 target 时都会连接 LLDB)。我不会在这儿讲太多,你可以在这篇 Apple 的指南中阅读一下如何用终端连接到一个 target — “Using LLDB as a Standalone Debugger”。

使用 process status 的话,你可以知道当前调试器停住的地址:

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) process status
Process 27408 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x000000010bbe4889 LLDB-Debugger-Exploration`ViewController.viewDidLoad(self=0x00007fa0c1406900) -> () at ViewController.swift:69
66
67 let a = 2, b = 2
68 let result = valueOfLifeWithoutSumOf(a, and: b)
-> 69 print(result)
70
71
72

想要继续 target 的执行过程直到遇到下次断点的话,运行这个命令:

1
2
3
(lldb) process continue
(lldb) c // 或者只键入 "c",这跟上一条命令是一样的

这个命令等同于 Xcode 调试器工具栏上的”continue“按钮:

breakpoint 命令允许你用任何可能的方式操作断点。我们跳过最显而易见的命令:breakpoint enable, breakpoint disable 和 breakpoint delete。

首先,查看你所有断点的话可以用如下示例中的 list 子命令:

1
2
3
4
5
6
7
8
9
10
(lldb) breakpoint list
Current breakpoints:
1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 95, exact_match = 0, locations = 1, resolved = 1, hit count = 1
1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 27 at ViewController.swift:95, address = 0x0000000107f3eb3b, resolved, hit count = 1
2: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 1
2.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000107f3e609, resolved, hit count = 1

列表中的第一个数字是是断点的 ID,你可以通过这个 ID 引用到指定的断点。现在让我们在控制台中设置一些新的断点:

1
2
3
(lldb) breakpoint set -f ViewController.swift -l 96
Breakpoint 3: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x0000000107f3eb4d

这个例子中的 -f 是你想要放置断点处的文件名,-l 是新断点的行数。还有一种更简洁的方式设置同样的断点,就是用快捷方式 b:

1
(lldb) b ViewController.swift:96

同样地,你也可以用指定的正则(比如函数名)来设置断点,使用下面的命令:

1
2
3
(lldb) breakpoint set --func-regex valueOfLifeWithoutSumOf
(lldb) b -r valueOfLifeWithoutSumOf // 上一条命令的简化版本

有些时候设置断点只命中一次也是有用的,然后指示这个断点立即删除自己,当然啦,有一个命令来处理这件事:

1
2
3
(lldb) breakpoint set --one-shot -f ViewController.swift -l 90
(lldb) br s -o -f ViewController.swift -l 91 // 上一条命令的简化版本

现在我们来到了最有趣的部分 — 自动化断点。你知道你可以设置一个特定的动作使它在断点停住的时候执行吗?是的,你可以!你是否会在代码中用 print() 来在调试的时候得到你感兴趣的值?请不要再这样做了,这里有一种更好的方法。🙂

通过 breakpoint 命令,你可以设置好命令,使其在断点命中时可以正确执行。你甚至可以设置”不可见“的断点,这种断点并不会打断运行过程。从技术上讲,这些“不可见的”断点其实是会中断执行的,但如果在命令链的末尾添上“continue”命令的话,你就不会注意到它。

1
2
3
4
5
6
7
8
9
10
(lldb) b ViewController.swift:96 // Let's add a breakpoint first
Breakpoint 2: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x000000010c555b4d
(lldb) breakpoint command add 2 // 准备某些命令
Enter your debugger command(s). Type 'DONE' to end.
> p sum // 打印变量 "sum" 的值
> p a + b // 运行 a + b
> DONE

为了确保你添加的命令是正确的,可以使用 breakpoint command list <breakpoint id> 子命令:

1
2
3
4
5
6
(lldb) breakpoint command list 2
Breakpoint 2:
Breakpoint commands:
p sum
p a + b

当下次断点命中时我们就会在控制台看到下面的输出:

1
2
3
4
5
6
Process 36612 resuming
p sum
(Int) $R0 = 6
p a + b
(Int) $R1 = 4

太棒了!这正是我们想要的。你可以通过在命令链的末尾添加 continue 命令让执行过程更加顺畅,这样你就不会停在这个断点。

1
2
3
4
5
6
7
(lldb) breakpoint command add 2 // 准备某些命令
Enter your debugger command(s). Type 'DONE' to end.
> p sum // 打印变量 "sum" 的值
> p a + b // 运行 a + b
> continue // 第一次命中断点后直接恢复
> DONE

结果会是这样:

1
2
3
4
5
6
7
8
9
p sum
(Int) $R0 = 6
p a + b
(Int) $R1 = 4
continue
Process 36863 resuming
Command #3 'continue' continued the target.

通过 thread 命令和它的子命令,你可以完全操控执行流程:step-over, step-in, step-out 和 continue。这些命令等同于 Xcode 调试器工具栏上的流程控制按钮。

LLDB 同样也对这些特殊的命令预先定义好了快捷方式:

1
2
3
4
5
6
7
(lldb) thread step-over
(lldb) next // 和 "thread step-over" 命令效果一样
(lldb) n // 和 "next" 命令效果一样
(lldb) thread step-in
(lldb) step // 和 "thread step-in" 命令效果一样
(lldb) s // 和 "step" 命令效果一样

为了获取当前线程的更多信息,我们只需要调用 info 子命令:

1
2
3
(lldb) thread info
thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in

想要看到当前所有的活动线程的话使用 list 子命令:

1
2
3
4
5
6
7
8
9
10
11
(lldb) thread list
Process 50693 stopped
* thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in
thread #2: tid = 0x17df4a, 0x000000010daa4dc6 libsystem_kernel.dylib`kevent_qos + 10, queue = 'com.apple.libdispatch-manager'
thread #3: tid = 0x17df4b, 0x000000010daa444e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #5: tid = 0x17df4e, 0x000000010da9c34a libsystem_kernel.dylib`mach_msg_trap + 10, name = 'com.apple.uikit.eventfetch-thread'

荣誉奖

command, platform, gui

在 LLDB 中你可以找到一个命令管理其他的命令,听起来很奇怪,但实际上它是非常有用的小工具。首先,它允许你从文件中执行一些 LLDB 命令,这样你就可以创建一个储存着一些实用命令的文件,然后就能立刻允许这些命令,就像是单个命令那样。这是所说的文件的简单例子:

1
2
thread info // 显示当前线程的信息
br list // 显示所有的断点

下面是实际命令的样子:

1
2
3
4
5
6
7
8
9
10
11
(lldb) command source /Users/Ahmed/Desktop/lldb-test-script
Executing commands in '/Users/Ahmed/Desktop/lldb-test-script'.
thread info
thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in
br list
Current breakpoints:
1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 0
1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000109429609, resolved, hit count = 0

遗憾的是还有一个缺点,你不能传递任何参数给这个源文件(除非你在脚本文件本身中创建一个有效的变量)。

如果你需要更高级的功能,你也可以使用 script 子命令,这个命令允许你用自定义的 Python 脚本 管理(add, delete, import 和 list),通过 script 命令能实现真正的自动化。请阅读这个优秀的教程 Python scripting for LLDB。为了演示的目的,让我们创建一个脚本文件 script.py,然后写一个简单的命令 print_hello(),这个命令会在控制台中打印出“Hello Debugger!“:

1
2
3
4
5
6
7
8
import lldb
def print_hello(debugger, command, result, internal_dict):
print "Hello Debugger!"
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f script.print_hello print_hello') // 控制脚本的初始化同时从这个模块中添加命令
print 'The "print_hello" python command has been installed and is ready for use.' // 打印确认一切正常

接下来我们需要导入一个 Python 模块,就能开始正常地使用我们的脚本命令了:

1
2
3
4
5
6
7
(lldb) command import ~/Desktop/script.py
The "print_hello" python command has been installed and is ready for use.
(lldb) print_hello
Hello Debugger!

你可以使用 status 子命令来快速检查当前的环境信息,status 会告诉你:SDK 路径、处理器的架构、操作系统版本甚至是该 SDK 可支持的设备的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) platform status
Platform: ios-simulator
Triple: x86_64-apple-macosx
OS Version: 10.12.5 (16F73)
Kernel: Darwin Kernel Version 16.6.0: Fri Apr 14 16:21:16 PDT 2017; root:xnu-3789.60.24~6/RELEASE_X86_64
Hostname: 127.0.0.1
WorkingDir: /
SDK Path: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
Available devices:
614F8701-3D93-4B43-AE86-46A42FEB905A: iPhone 4s
CD516CF7-2AE7-4127-92DF-F536FE56BA22: iPhone 5
0D76F30F-2332-4E0C-9F00-B86F009D59A3: iPhone 5s
3084003F-7626-462A-825B-193E6E5B9AA7: iPhone 6
...

你不能在 Xcode 中使用 LLDB GUI 模式,但你总是可以从终端使用(LLDB GUI 模式)。

1
2
3
(lldb) gui
// 如果你试着在 Xcode 中执行这个 gui 命令的话,你将会看到这个错误:the gui command requires an interactive terminal。

这就是 LLDB GUI 模式看起来的样子。

结论:

在这篇文章中,我只是浅析了 LLDB 的皮毛知识而已,即使 LLDB 已经有好些年头了,但是仍然有许多人并没有完全发挥出它的潜能。我只是对基本的方法做了一个概述,以及谈了 LLDB 如何自动化调试步骤。我希望这会是有帮助的。

还有很多 LLDB 的方法并没有写到,然后还有一些视图调试技术我没有提及。如果你对这些话题感兴趣的话,请在下面留下你的评论,我会更加乐于写这些话题。

我强烈建议你打开终端,启动 LLDB,只需要敲入 help,就会向你展示完整的文档。你可以花费数小时去阅读,但是我保证这将是一个合理的时间投资。因为了解你的工具是工程师真正产出的唯一途径。


  • LLDB 官方网站 —  你会在这里找到所有与 LLDB 相关的材料。文档、指南、教程、源文件以及更多。
  • LLDB Quick Start Guide by Apple — 同样地,Apple 提供了很好的文档。这篇指南能帮你快速上手 LLDB,当然,他们也叙述了怎样不通过 Xcode 地用 LLDB 调试。
  • How debuggers work: Part 1 — Basics — 我非常喜欢这个系列的文章,这是对调试器实际工作方式很好的概述。文章介绍了用 C 语言手工编写的调试器代码要遵循的所有基本原理。我强烈建议你去阅读这个优秀系列的所有部分(第2部分, 第3部分)。
  • WWDC14 Advanced Swift Debugging in LLDB — 关于在 LLDB 中用 Swift 调试的一篇不错的概述,也讲了 LLDB 如何通过内建的方法和特性实现完整的调试操作,来帮你变得更加高效。
  • Introduction To LLDB Python Scripting — 这篇介绍 LLDB Python 脚本的指南能让你快速上手。
  • Dancing in the Debugger. A Waltz with LLDB  — 对 LLDB 一些基础知识的介绍,有些知识有点过时了(比如说 (lldb) thread return 命令)。遗憾的是,它不能直接用于 Swift,因为它会对引用计数带了一些潜在的隐患。但是,这仍然是你开始 LLDB 之旅不错的文章。
ruanpapa和又吉君写字的地方

Source Editor Extension — Xcode 格式化 Import 的插件

发表于 2018-02-01 | 分类于 ruanpapa--技术贴 | | 阅读次数

背景

Xcode 秉承了 Apple 封闭的传统,提供的可自定义的选项比起其他 IDE 来说是比较少的,不过在 Xcode 7 之前(包含 Xcode 7)我们还是可以通过插件实现 Xcode 的自定义,甚至还出现了像 Alcatraz 的专门的插件管理工具,开源社区中也有诸如 VVDocumenter-Xcode、CocoaPods 等知名的插件,不过这些便利随着 Xcode 8 的发布成为了过去式。
出于安全性考虑(比如说 Xcode ghost 事件),Apple 从 Xcode 8 开始不再支持第三方的插件。Apple 方面提供了基于 App Extension 的解决方案 – Xcode Source Editor Extension,这是一个相当简单的方案,能且仅能完成有限的文本编辑辅助,很大部分之前第三方插件能完成的任务都没办法实现了。聊胜于无吧 😑
(本文会介绍 Source Editor Extension 的开发以及分发相关的知识,本文对应的 Demo 在:https://github.com/VernonVan/PPImportArrangerExtension)

创建插件

  1. 创建一个 Cocoa App:Source Editor Extension 不能独立存在,必须依附于 Cocoa App。
    Cocoa App

    ​

  2. File -> New -> Target -> Xcode Source Editor Extension 添加一个 Target,并激活这个 Target。
    Xcode Source Editor Extension
    激活 target

这样就创建好了一个可运行的 Source Editor Extension,相当的简单。🧐

关键概念

文件结构

  • SourceEditorExtension 类:遵循 XCSourceEditorExtension 协议的类,XCSourceEditorExtension 协议的头文件如下:
1
2
3
4
5
6
7
8
9
@protocol XCSourceEditorExtension <NSObject>
@optional
- (void)extensionDidFinishLaunching;
@property (readonly, copy) NSArray <NSDictionary <XCSourceEditorCommandDefinitionKey, id> *> *commandDefinitions;
@end

XCSourceEditorExtension 协议只有一个方法和一个属性,extensionDidFinishLaunching 方法是用来在插件加载好后是对插件进行一些准备工作的,根据 WWDC 的说法,各个插件与 Xcode 本身的初始化过程是在不同进程上进行的,同样地,插件的崩溃并不会引起 Xcode 的崩溃。commandDefinitions 属性则可以动态返回插件的菜单项。

SourceEditorCommand 类:遵循 XCSourceEditorCommand 协议的类,实现插件功能的核心类,对应到插件的菜单项,可以一个菜单项对应到一个 Command 类,也可以多个菜单项对应到一个 Command 类,XCSourceEditorCommand 协议头文件定义如下:

1
2
3
4
5
6
7
@protocol XCSourceEditorCommand <NSObject>
@required
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler;
@end

XCSourceEditorCommandInvocation 类型的参数 invocation 主要是点击的菜单项的标识、当前文本信息(文本字符串数组、选中区间等)以及点击取消按钮的回调事件,completionHandler 参数则是用来通知 Xcode 本插件已经完成了自己的操作,需要保证一定要调用 completionHandler!否则会出现下图所示的提示,然后菜单项就会变灰不能再点击:
插件 busy
菜单项变灰

  • Info.plist:Info.plist 文件用于静态配置插件对应的菜单项,如下图所示,XCSourceEditorExtensionPrincipalClass 对应到上文说的 XCSourceEditorExtension 类,XCSourceEditorCommandDefinitions 指定菜单项,XCSourceEditorCommandClassName 对应到上文说的 SourceEditorCommand 类,XCSourceEditorCommandIdentifier 是每个具体菜单项的标识,XCSourceEditorCommandName 是菜单项的描述。

Info.plist

  • 保证 TARGETS 组下的两个 Target 用的同一个签名。

实现步骤

本 Demo 要实现的功能就是按照字母顺序重新排列当前文件的所有 Import,强迫症们一定知道我在说什么🤣,先来看一下效果:
效果图
演示效果
可以点击 Editor -> ImportArranger -> Arrange Imports 重新排列所有的 Imports,甚至还可以为其设置快键键。

实现步骤反而没有什么可说的,主要是操作 invocation.buffer.lines 和 invocation.buffer.selections,分别对应的是当前文件的所有行和当前文件的选择区域,都是可变类型的数组,做完自定义的操作后操作数组即可更新当前文件。注意:不管是哪条执行路径,一定要保证调用到 completionHandler。其他需要留意的地方都在代码中的注释中给出:

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
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError *_Nullable nilOrError))completionHandler
{
NSMutableArray<NSString *> *lines = invocation.buffer.lines;
if (!lines || !lines.count) {
completionHandler(nil);
return;
}
NSMutableArray<NSString *> *importLines = [[NSMutableArray alloc] init];
NSInteger firstLine = -1;
for (NSUInteger index = 0, max = lines.count; index < max; index++) {
NSString *line = lines[index];
NSString *pureLine = [line stringByReplacingOccurrencesOfString:@" " withString:@""]; // 去掉多余的空格,以防被空格干扰没检测到 #import
// 支持 Objective-C、Swift、C 语言的导入方式
if ([pureLine hasPrefix:@"#import"] || [pureLine hasPrefix:@"import"] || [pureLine hasPrefix:@"@class"]
|| [pureLine hasPrefix:@"@import"] || [pureLine hasPrefix:@"#include"]) {
[importLines addObject:line];
if (firstLine == -1) {
firstLine = index; // 记住第一行 #import 所在的行数,用来等下重新插入的位置
}
}
}
if (!importLines.count) {
completionHandler(nil);
return;
}
[invocation.buffer.lines removeObjectsInArray:importLines];
NSArray *noRepeatArray = [[NSSet setWithArray:importLines] allObjects]; // 去掉重复的 #import
NSMutableArray<NSString *> *sortedImports = [[NSMutableArray alloc] initWithArray:[noRepeatArray sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]];
// 引用系统文件在前,用户自定义的文件在后
NSMutableArray *systemImports = [[NSMutableArray alloc] init];
for (NSString *line in sortedImports) {
if ([line containsString:@"<"]) {
[systemImports addObject:line];
}
}
if (systemImports.count) {
[sortedImports removeObjectsInArray:systemImports];
[sortedImports insertObjects:systemImports atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, systemImports.count)]];
}
if (firstLine >= 0 && firstLine < invocation.buffer.lines.count) {
// 重新插入排好序的 #import 行
[invocation.buffer.lines insertObjects:sortedImports atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(firstLine, sortedImports.count)]];
// 选中所有 #import 行
[invocation.buffer.selections addObject:[[XCSourceTextRange alloc] initWithStart:XCSourceTextPositionMake(firstLine, 0) end:XCSourceTextPositionMake(firstLine + sortedImports.count, sortedImports.lastObject.length)]];
}
completionHandler(nil);
}

选择这个插件作为当前 Scheme,选择 Xcode 运行,然后就会弹出一个黑色的 Xcode 供你调试了。
image.png
调试插件

分发

插件开发测试完成之后,最重要的当然是将插件分发出去,供他人使用。Apple 在 WWDC 说到 Xcode Source Editor Extension 是可以上架 Mac App Store 的,不过受限于 Source Editor Extension 功能实在太少,目前也没有在 Mac App Store 上看到很火的插件。更多是直接把 .app 文件上传到 Github 上供人下载(这里有人整理了一些不错的插件:https://github.com/theswiftdev/awesome-xcode-extensions),具体步骤如下:

打包

测试完成后,找到 Products 下面的 .app 文件,注意需要保证上文中说的两个签名是一致的。然后就可以把这个 .app 上传到个人网站或者 Github 上供人下载使用了。
.app 文件

安装

当我们下载好了一个 .app 格式的插件之后,直接双击这个 .app 文件,然后在 系统偏好设置-> 扩展 -> Xcode Source Editor Extension 勾选该插件,最后重启 Xcode 就可以在 Editor 菜单中找到该插件了。
勾选插件

还可以在 Xcode 中为插件的菜单项设置快捷键。
设置快键键

结语

至少现有的 Xcode Source Editor Extension 还是比较受限的,接口少的可怜,可想象的空间不是很多,大部分之前第三方插件能做的事情都没办法完成了🤷‍♀️。还是默默希望 Apple 能以更加开放的姿态,提供更多的接口给开发者,Xcode 没办法满足所有人的喜好,起码,能让喜欢折腾的人把它变得更好 :-D

ruanpapa和又吉君写字的地方

iOS表情键盘的完整实现

发表于 2018-01-19 | 分类于 ruanpapa--技术贴 | | 阅读次数

最近在公司做了个表情键盘的需求,这个需求的技术难度不会很大,比较偏向业务。但是要把用户体验做的好也是不容易的,其中有几个点需要特别注意。话不多说,下面开始正文(注:本文对应的Demo放在Github上:https://github.com/VernonVan/PPStickerKeyboard)。

市面上的表情键盘的分析

首先来看一下市面上主要的几个APP上的表情键盘,平时使用的时候不会去关注细节,这次特意去使用了表情键盘,发现各个APP的体验还是有优有劣的。

首先是QQ和微信,这两者差不多,切换到表情键盘的时候都是没有光标的,这样的用户体验是非常不好的,没有办法在输入表情的时候框选区域,也不能拖动光标进行特定位置的复制黏贴删除等操作,微信甚至在输入框里显示的都不是点击的表情图片,而是文字描述。

微信QQ表情键盘.JPG

接下来看一下微博国际版,国际版调起表情键盘时是有光标的,是一个”真正的”键盘,但是想要拖拽光标的时候,很大概率上会触发到保存图片的行为(如下图所示),导致根本没办法拖动光标。
微博国际版误触.JPG

同时微博国际版输入框表情黏贴后的光标定位是错误的,如下图,开始时光标是在第4个表情后面,然后复制狗头+害羞两个表情黏贴到光标后,光标还是在第4个表情后,同时黏贴的表情前后都莫名多了空格。
微博国际版黏贴.JPG

最后是微博,微博客户端的表情键盘的体验是非常好的,上面说到的问题都不存在,而且表情键盘的删除按钮还能长按删除输入框的内容。
微博表情键盘.jpg

表情键盘的实现

实现效果

主要实现了以下几个功能

  • 能输入表情,有光标,支持复制黏贴删除表情等
  • 长按预览表情
  • 删除表情、长按连续删除表情
  • 适配 iPhone X
    演示.GIF

基本思路

首先,表情包的图片是用bundle的形式组织的,用PPSticker类表征一套表情包,用PPEmoji类表征某一个表情,用一个plist作为配置文件,存储表情包的信息。
表情的组织.jpg

PPStickerDataManager类主要负责数据部分,用单例的形式,这样可以在初始化的时候只会读取一次plist文件中的所有表情信息;同时我们把输入框内容发到服务端以及从服务端请求到的都是纯文本的,比如会把 “笑死了🤣” 转成 “笑死了[笑哭]” 这样的纯文本,而不是直接把表情图片直接发到服务端,也就是说项目中有大量的地方会有把文本->表情的操作,所以PPStickerDataManager类也提供匹配某段纯文本中的表情,并把文本替换为图片的功能,PPStickerDataManager类的头文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface PPStickerDataManager : NSObject
+ (instancetype)sharedInstance;
/// 所有的表情包
@property (nonatomic, strong, readonly) NSArray<PPSticker *> *allStickers;
/* 匹配给定attributedString中的所有emoji,如果匹配到的emoji有本地图片的话会直接换成本地的图片
*
* @param attributedString 可能包含表情包的attributedString
* @param font 表情图片的对齐字体大小
*/
- (void)replaceEmojiForAttributedString:(NSMutableAttributedString *)attributedString font:(UIFont *)font;
@end

“真正的”键盘

真正的键盘也就是说调起表情键盘时输入框是有光标的,能进行拖拽光标、选中区域等的操作,这样的体验才是与系统键盘一致的。其实系统已经提供好了接口给我们直接使用,UITextView和UITextField都有的inputView和inputAccessoryView就是用来实现自定义键盘的,这两个属性的定义如下:

1
2
3
4
// Presented when object becomes first responder. If set to nil, reverts to following responder chain. If
// set while first responder, will not take effect until reloadInputViews is called.
@property (nullable, readwrite, strong) UIView *inputView;
@property (nullable, readwrite, strong) UIView *inputAccessoryView;

同时系统键盘在 设置->声音->按键音 选项打开且手机非静音状态下输入是有按键的声音的,这个按键音也是可以支持的,只要自定义键盘类遵循UIInputViewAudioFeedback协议,同时实现 enableInputClicksWhenVisible方法并返回YES,这样就可以在点击表情的时候调用[[UIDevice currentDevice] playInputClick]方法发出按键音了,详情请查看苹果的官方文档。

下面是Demo中键盘切换方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)changeKeyboardTo:(PPKeyboardType)toType
{
switch (toType) {
case PPKeyboardTypeSystem:
self.textView.inputView = nil; // 切换到系统键盘
[self.textView reloadInputViews]; // 调用reloadInputViews方法会立刻进行键盘的切换
break;
case PPKeyboardTypeSticker:
self.textView.inputView = self.stickerKeyboard; // 切换到自定义的表情键盘
[self.textView reloadInputViews];
break;
default:
break;
}
}

去除表情的拖拽交互

在iOS11上,UITextView上的NSTextAttachment(表情)默认可以进行拖拽交互,但是却导致拖动光标时很容易触发这个交互(图示可以查看上面说到的微博国际版中的误触)。一番查找之后才找到一个比较隐蔽的属性:textDragInteraction,直接设置为NO就能禁止掉NSTextAttachment的拖拽交互。

1
2
3
if (@available(iOS 11.0, *)) { // 只在iOS11及以上才有这个属性
_textView.textDragInteraction.enabled = NO;
}

与服务端的交互

我们在输入框中输入的内容与服务端进行交互的时候都是用纯文本的,比如会把 “笑死了🤣” 转成 “笑死了[笑哭]” 这样的纯文本发到服务端,而不是直接发表情图片,向服务端请求内容的时候也是传回 “笑死了[笑哭]”,然后客户端再根据正则匹配找出表情替换成对应的表情图片,然后显示到页面上。具体过程可以看下图:
与服务端的交互.png

也就是说,我们设置到输入框的NSAttributedString中的每一个NSTextAttachment都有一个”隐藏的”属性—表情的文本描述,这里对NSAttributedString进行拓展就能实现。pp_setTextBackedString可以对NSAttributedString的指定range设置一个PPTextBackedString类型的属性,而pp_plainTextForRange能拿到NSAttributedString指定range的纯文本。具体实现如下:

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
@implementation NSAttributedString (PPAddition)
- (NSString *)pp_plainTextForRange:(NSRange)range
{
if (range.location == NSNotFound || range.length == NSNotFound) {
return nil;
}
NSMutableString *result = [[NSMutableString alloc] init];
if (range.length == 0) {
return result;
}
NSString *string = self.string;
[self enumerateAttribute:PPTextBackedStringAttributeName inRange:range options:kNilOptions usingBlock:^(id value, NSRange range, BOOL *stop) {
PPTextBackedString *backed = value;
if (backed && backed.string) {
[result appendString:backed.string];
} else {
[result appendString:[string substringWithRange:range]];
}
}];
return result;
}
@end
@implementation NSMutableAttributedString (PPAddition)
- (void)pp_setTextBackedString:(PPTextBackedString *)textBackedString range:(NSRange)range
{
if (textBackedString && ![NSNull isEqual:textBackedString]) {
[self addAttribute:PPTextBackedStringAttributeName value:textBackedString range:range];
} else {
[self removeAttribute:PPTextBackedStringAttributeName range:range];
}
}
@end

灵活的光标

表情功能,UITextView都是用NSAttributedString进行赋值的,并且我们底层其实还是用上面说到的纯文本进行实现的,那么把 [笑死] 转成 🤣 就会从4个字符变成1个字符,这里是有差值的,如果不处理的话就会出现上面提到的微博国际版中复制黏贴输入框的表情会导致光标位置不对,甚至莫名其妙多出前后空格的问题。为了精准的定位光标,我们需要自行处理好这些问题。

这里自己继承并实现了UITextView的子类PPStickerTextView,在这个类中重载复制、黏贴、剪切等操作,分别对应的方法如下:

1
2
3
4
5
- (void)cut:(id)sender; // 剪切
- (void)copy:(id)sender; // 复制
- (void)paste:(id)sender; // 黏贴

下面以剪切方法举例,看看怎么处理光标的问题,需要注意的地方请看对应的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)cut:(id)sender
{
// 1.从textView中拿到对应的纯文本,比如:笑死了[笑死]
NSString *string = [self.attributedText pp_plainTextForRange:self.selectedRange];
if (string.length) {
// 2. 将纯文本写入到剪贴板中
[UIPasteboard generalPasteboard].string = string;
// 3. 记住当前的光标位置
NSRange selectedRange = self.selectedRange;
NSMutableAttributedString *attributeContent = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
// 4. 将检测到是表情的文本替换成对应的图片
[attributeContent replaceCharactersInRange:self.selectedRange withString:@""];
self.attributedText = attributeContent;
// 5. 重新设置光标
self.selectedRange = NSMakeRange(selectedRange.location, 0);
}
}

技术点的分析就是以上这些,详细的代码可以clone代码查看:https://github.com/VernonVan/PPStickerKeyboard

123
ruanpapa & 又吉君

ruanpapa & 又吉君

ruanpapa和又吉君写字的地方

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