tripleCC的技术博客

ʕ•̫͡•ʔ-̫͡-ʕ•͓͡•ʔ-̫͡-ʕ•̫͡•ʔ-̫͡-ʕ•͓͡•ʔ-̫͡-ʔ

给 APP 添加悬浮助手

自组内力推业务组件化以来,项目组的业务组件数量也渐具规模。虽然在版本依赖和个别组件独立性等细节上略有欠缺,但是在一定程度上还是提升了开发效率。

既然是业务组件,在某些场景下就需要登录获取相关权限后才能进一步操作。由于项目中登录模块并没有重构过,代码风格和书写逻辑极差,加上针对 B 端的 APP 登录逻辑较为复杂,包括切店等一系列操作,导致给客户使用的登录逻辑界面,在开发阶段难以接入业务组件,即使接入了,使用体验(没错,开发时也讲究一个体验)和开发效率也是会打一定折扣。

那么问题来了,在开发阶段,怎样做才能让业务模块更加简便、优雅、无侵入地接入登录功能呢?进一步讲,如果不局限于登录功能,如何给业务模块提供通用工具入口?

使用usbmuxd连接越狱设备

最近通过 wifi ssh 到越狱设备上时,出了个奇怪的问题:
ssh 一直不结束,没提示成功,也没有提示失败。

下面是自己对越狱设备进行“自测”的过程:

操作 结果
iOS ssh mac ip 成功
iOS ssh localhost 成功 (sshd 是跑起来的)
iOS ssh 自身 ip 失败
mac ssh iOS ip 失败


上面的失败是指没有返回 ssh 的结果,一直卡在连接阶段。

如何愉快地使用命令行

古人云:

工欲善其事,必先利其器

从进入软件开发这一行业开始算起,我也算是陆陆续续地接触了三大主流操作系统。虽说不是很深入地使用过每个系统,但从自己的个人喜好及以往的使用经历来看, Mac 无疑是对开发者最友好的一个系统。

不过即便如此,作为一个 Vim 爱好者,我还是非常怀念当初在 Ubuntu 字符终端下编写代码的那种畅(zhuang)爽(bi)感,那种双手不需要离开键盘的紧凑感。

好的工具配置,既能极大地促进开发效率的提升,又能让开发者保持一个心情愉悦的状态。下面纪录下自己常用的一些工具软件,这些软件能减少让开发者对触控板 / 鼠标的依赖。

NSJSONSerialization和NSNumber的事故现场

最近同事在和后台联调时,出现了一个诡异的问题:后台传输的价格为 0.07 ,但是到了 iOS 这边,就变成了 0.07000000000000001 。奇怪的是安卓端并没有此问题,并且从 charles 抓包内容来看,后台传输的价格确实是0.07。

在排除了业务层转化因素后,我们进入了 AFNetworking 框架内部,看能不能找到一点线索,然后就定位到了以下方法:

1
2
3
- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error

在拿到原始的 data 后,我尝试在调试终端使用 NSJSONSerialization 对其进行反序列化,结果发现价格在这里已经变成了 0.07000000000000001 。

众所周知,由于计算机无法精确存储浮点数,实际上存储的浮点型变量实际数值就是取范围允许内最接近的值,也就是上面接收到的那样。(注:Float who stole your accuracy

难道是 NSJSONSerialization 在反序列化的时候出了问题?

利用runtime兼容老代码小记

通常来说,在项目的初期,因为各种原因,要么人力不够,要么项目周期过紧,会产生难以维护、阅读性较差的代码。而这种代码,我习惯称之为“老代码”。就比如现在手上的项目,初期是由一个被迫转到 iOS 的后端 java 哥们写的,所以工程里面到处都可以看见 java 的一些编码风格,比如模型以 Vo 结尾、接口以 I 开头等,甚至转场动画都是简单粗暴地通过 view 叠加再辅以动画实现的。“老代码”是项目特定时间段的产物,因此,也不能把锅全部推给写这些代码的人。不过前人埋坑,后人总得填啊。

好了,吐槽完毕,开始正题。

Objective-C使用范型实现map提示

在当前项目的一些内容加工逻辑较多的界面,我会使用ViewModel来对Model进行一层包装,这样可以保持Model的纯净,也可以减少Controller中弱逻辑代码的堆叠。当然,部分公用内容也是可以通过给Model添加分类来实现的,ViewModel更多是提供特定页面的定制化内容。

由于项目并没有采用MVVM模式,也就没有引入ReactiveCocoa,所以项目里面比较多这样的代码:

1
2
3
4
5
6
7
NSArray <TBVEmployee *> *employees = @[[TBVEmployee new]];

NSMutableArray *items = [NSMutableArray array];
for (TBVEmployee *employee in employees) {
    TBVEmployeeItemViewModel *item = [TBVEmployeeItemViewModel itemWithEmployee:employee];
    [items addObject:item];
}

这段代码主要是为了将Model 转化成ViewModel

一次短暂的mac开发之旅

回杭近一周,发现公司后台写的接口文档还是比较清晰的。特别是自己组负责的业务线,接口文档上的字段和实际返回的字段几乎没有差别。

询问了周围小伙伴如何写模型文件之后,发现无非三种方式:

  • 手写啦==
  • Xcode8以前的用ESJsonFormat插件,Xcode8以后手写
  • JSONExport生成

针对以上三种方式,我做了一个简短的分析:

  • 这个不用说了,耗时费力不讨好。量少好说,量大就比较蛋疼了。
  • xcode8之后,第三方插件被禁止了。虽说有方法能让xcode8重新用上这个插件,但是即使用上了这个插件,还是需要自己写注释,并且生成模型需要后台返回的json
  • 和上一个方式一样,只是从插件编程了mac软件

在打听完后,我随即产生了自己写一个转换工具的想法。

原因如下:

  • 后台文档已经写的比较清晰,可以从网页上把这些数据都爬下来,然后生成含有注释的模型
  • 可以自动将Vo结尾的模型和属性,转成Model结尾的模型和属性,并且生成YYModel需要的映射关系
  • 因为接口文档都处于一个统一的baseURL下,加上模型名称就是完整路径,所以可以很方便地进行批量处理
  • 不需要测试后台发布的接口后,再通过获取接口返回的json生成模型;只要接口文档一发布就可以生成模型

使用RxSwift+Moya+ObjectMapper接入模型

一般情况下,从业务方从API中请求JSON数据时,一般都会经过以下三步:

1
2
--------1------------2----------
原始数据 -> JSON/字典  -> Model

当然,大部分情况下,原始数据就是JSON,所以第一步基本上只是对接受数据的一个类型转换。一般在网络层中,由组件方提供1步骤,而业务方往往在网络组件的回调中提供步骤2。简单的转换逻辑明了了,接下来就可以试下用Moya实现步骤1,ObjectMapper实现步骤2。

在结合RxSwift+Moya+ObjectMapper三者之后,常规JSON数据的获取与解析变得更加精简。以近期编写的一个V2ex API为例,获取个人信息接口如下:

1
2
3
4
5
6
7
func fetchMemberInfo(_ username: String? = V2exAppContext.shared.currentUsername,
                     _ id: Int? = nil) -> Observable<V2exMember> {
    return V2exProvider
        .request(.ShowMembers(username: username, id: id))
        .mapObject()
        .shareReplay(1)
}

嗯,没错,最终的调用就是这么简单明了!

ReactiveX中Using操作的应用

create a disposable resource that has the same lifespan as the Observable,即创建一个和Observable具有相同生命周期的disposable资源。
这是ReactiveX对于Using的描述。

可以看出,当一个ObserverA订阅Using返回的Observable时,Using会使用调用者传入的Resource工厂方法[resourceFactory]创建对应的资源,并且使用Observable工厂方法[observableFactory]创建ObserverA实际上想要订阅的Observable。当ObserverA终止时,对应的Resource也会被释放[dispose]。

关于RxSwift中的DisposeBag

在RxSwift中,订阅者都会返回一个Disposable(默认是Disposables),以便使用者可以在后续的操作中,取消此次订阅。
使用者可以调用dispose方法来进行取消订阅:

1
2
3
4
5
6
7
let disposables = Observable
    .just(1)
    .delay(2, scheduler: MainScheduler.instance)
    .subscribe { event in
        print(event)
    }
disposables.dispose()

关于手动取消订阅后,对应的subscribe回调会不会调用,官方的手册是这么说的:

  • 当scheduler是串行调度器,并且使用者在此调度器上调用了dispose,那么回调就不会执行[MainScheduler是在主线程/UI线程的串行调度器]
  • 其他情况都不能保证回调的执行与否[并行情况下,执行顺序无法保证]