Skip to content

Latest commit

 

History

History
465 lines (272 loc) · 26.9 KB

01.md

File metadata and controls

465 lines (272 loc) · 26.9 KB

2019.01

在 UILabel 中渲染 HTML

作者: 南峰子

我们可以使用 NSAttributedString 在 UILabel 中渲染 HTML 字符串,不过需要使用 NSAttributedString 特定的初始化方法

init(data: Data, 
options: [NSAttributedString.DocumentReadingOptionKey : Any] = [:], 
documentAttributes dict: AutoreleasingUnsafeMutablePointer<NSDictionary?>?) throws

在这个初始化方法的 options 参数中,指定 .documentType 的值为 NSAttributedString.DocumentType.html。不过大多数情况下这还不够,我们可能还需要指定文本的样式,例如指定文本的字体或颜色,这时我们就需要在 html 文本中通过 css 来设置 style,如下代码所示:

import UIKit

extension String {

    func htmlAttributedString(with fontName: String, fontSize: Int, colorHex: String) -> NSAttributedString? {
        do {
            let cssPrefix = "<style>* { font-family: \(fontName); color: #\(colorHex); font-size: \(fontSize); }</style>"
            let html = cssPrefix + self
            guard let data = html.data(using: String.Encoding.utf8) else {  return nil }
            return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
        } catch {
            return nil
        }
    }
}

let html = "<strong>Dear Friend</strong> I hope this <i>tip</i> will be useful for <b>you</b>."
let attributedString = html.htmlAttributedString(with: "Futura", fontSize: 14, colorHex: "ff0000")

效果如下图所示

iOS App 异常捕获相互覆盖问题

作者: KANGZUBIN

在开发和维护 App 过程中,我们通常需要去捕获并上报导致 App 崩溃的异常信息,以便于分析,一般我们会使用一些成熟的第三方 SDK,例如 Bugly 或者友盟等。

但如果我们想自己捕获异常信息,做一些相关处理,其实也很简单,苹果为开发者提供了两个异常捕获的 API,如下:

typedef void NSUncaughtExceptionHandler(NSException *exception);

NSUncaughtExceptionHandler * NSGetUncaughtExceptionHandler(void);
void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler *);

其中,NSSetUncaughtExceptionHandler 函数用于设置异常处理的回调函数,在程序终止前的最后一刻会调用我们设置的回调函数(Handler),进行崩溃日志的记录,代码如下:

但是,大部分第三方 SDK 也是通过这种方式来收集异常的,当我们通过 NSSetUncaughtExceptionHandler 设置异常处理函数时,会覆盖其它 SDK 设置的回调函数,导致它们无法上报统计,反之,也可能出现我们设置的回调函数被其他人覆盖。

那如何解决这种覆盖的问题呢?其实很简单,苹果也为我们提供了 NSGetUncaughtExceptionHandler 函数,用于获取之前设置的异常处理函数。

所以,我们可以在调用 NSSetUncaughtExceptionHandler 注册异常处理函数之前,先通过 NSGetUncaughtExceptionHandler 拿到已有的异常处理函数并保存下来。然后在我们自己的处理函数执行之后,再调用之前保存的处理函数就可以了。

完整的示例代码如下图所示:

最后,如果你的 App 接入了多个异常捕获 SDK,而出现了其中一个异常上报不生效的情况,有可能就是因为这个覆盖问题导致的。

参考连接:https://mp.weixin.qq.com/s/vmwj3Hs8JTg3WmB70xhqIQ

Debug Memory Graph检查内存泄漏

作者: 这个汤圆没有馅

在日常检查内存泄漏时,除了 Instruments 里的 Leaks,还有一个就是 Xcode 8推出的 Debug Memory Graph。

为了能看到内存详细信息,先打开 Edit Scheme-->Diagnostics, 勾选 Malloc Scribble 和 Malloc Stack。为了避免过多的性能消耗,在 Malloc Stack 中直接选择 Live Allocations Only 即可。

运行 App,找到查看视图层级 Debug View Hierarchy 边上的三个小圈圈的按钮,这个就是Debug Memory Graph按钮,点击后页面变化如下图。

左边栏会有当前运行 App 的文件信息,若有内存泄漏,边上会有一个紫色的感叹号。也可以通过下方的 show only leaked blocks 过滤文件。

中间区域内容是当前文件内存详细信息及对象之间的关联关系。黑色线条代表强引用,不过灰色的线不代表弱引用,只是一些系统级别的引用或者苹果为了优化显示效果而添加的,可直接忽略。

右边栏点击右上角的 Show the Memory Inspector,会有堆栈信息,并且能直接定位到内存泄漏的代码块。

当然,在 Runtime Issue navigator 中也可以直接看到内存泄漏的地方。

Debug Memory Graph 它能很方便的定位到内存泄漏的地方,但同时它会有误报的情况。例如,当创建 UIButton 对象并将其添加到 UIToolBars 项目数组时,会发现它被识别为内存泄漏,但我们不明白为什么。它也会将一些系统的信息识别为内存泄漏,如下图,定位到了一个叫UIKeyboardPredictionView的地方。代码中未用到三方键盘,纯系统键盘唤起。个人理解为系统键盘回收后,其实并没有真正被释放,等到下次唤起键盘时再次使用。我觉得类似这种内存泄漏可以不用管。

如有表述不当,欢迎指出~~

Aspects hook 类方法的正确姿势

作者: Vong_HUST

说起 AOP,相信大家对 Aspects 都有所耳闻,这里不再做原理解读,如果对其原理感兴趣推荐自行阅读源码或者阅读网上大神写的文章。

根据其 README,我们知道它对类方法和实例方法都能 hook,那么 hook 类方法第一感觉,直接用类名去调用 Aspects 提供的分类类方法就好,大概像图1这样。

运行起来发现,没有并没有打印我们想要输出的内容,反而输出了一段 Aspects 的错误日志 “Aspects: Blog signature <NSMethodSignature: 0x600001a58c00> doesn't match (null).”(我猜 Blog 应该是作者笔误,实际上是 Block)。即我们指定的 block 签名和我们要 hook 的方法签名不一致。查看源码,发现用图1这种方式,Aspects 在获取方法签名的时候,使用的是 [[object class] instanceMethodSignatureForSelector:selector],这个时候获取到的方法签名是 nil。这是为什么呢?

这里主要是 class 方法和 object_getClass 方法的区别,前者当 object 是类时,则返回本身,当 object 为实例时,则返回类;后者则返回的是 isa 指针的指向,如图2所示。由于这里 object 是类,所以 object.class 返回自身,而自身是没有 selector 对 应的实例方法,所以方法签名返回了 nil。

因此,如果我们如果要 hook 类方法正确的姿势应该如图3所示。

即对其 metaClass 进行 hook,因为其实 class 也可以理解成 metaClass 的实例对象。回到上面的例子对 metaClass 调用 class 方法时,返回的是 metaClass 本身,所以 [[object class] instanceMethodSignatureForSelector:selector] 实际上是去 metaClass 的 selector 对应的实例方法,也就是类方法,而 selector 对应的类方法是存在的,所以一切流程正常。这里说的比较绕,推荐一下这张经典的图供(图4)大家参考。

Xcode更新输入账号密码,账号却不是自己的

作者: Lefe_x

更新Xcode的时候,需要输入Apple账号和密码,以前一直正常,这次更新的时候遇到一个问题。更新的时候提示输入账号和密码,可是账号并不是我自己的账号,关键这个账号也不能修改,只能输入密码。刚开始以为是系统的问题,把Mac系统升级后发现并不管用。最后想了下,我电脑上的Xcode安装的时候是直接和同事拷贝的,那么这个账号应该就是他的。直接和同事要账号密码并不合适。

最后找到一个解决办法:

在应用中找到Xcode -> 显示包内容 -> 找到 _MASReceipt 文件夹,把它删除 -> 更新即可。

Framework 中混合编程时 umbrella header 设置注意事项

作者: 南峰子

Swift 和 Objective-C 混合编程,当需要在 Swift 中调用 Objective-C 代码时,在 App Target 中,我们依托的是 Objective-C Bridging Header,而在 Framework Target 中,依托的是 unbrella header ,即 Framework 的主头文件。我们需要做如下配置:

  • 在 Build Setting -> Packaging 中将 Defines Module 设置为 YES,如下图所示;

  • 在 unbrella header 中导入需要暴露的 Objective-C 头文件

如果这样配置后,发现编译器还是报 Use of undeclared type '**' 错误,则确认以下两点:

  • unbrella header 和需要暴露的 Objective-C 头文件是否包含在 Framework Target 中,如下图所示;

  • 在 Build Phases -> Headers 中,将 unbrella header 和需要暴露的 Objective-C 头文件放置在 Public 区域中,所下图所示!

这样确认后,基本就没什么问题了。

This block declaration is not a prototype 编译警告处理

作者: 南峰子

在 Objective-C 中,经常会使用到 block,在声明 block 时,如果没有参数,我们经常是会将参数省略,而不写 void,如

typedef void (^Completion)();

特别是在老代码中,这样的情况应该是多数。

而到了 Xcode 9 之后,编译器对这样的代码给出一个警告:

This block declaration is not a prototype

即编译器希望你把参数 void 给加上。

最直接的方法当然是声明 block 时,对无参的 block 加上 void,但对于老代码或者是第三方的代码,我想很少有人想去改。如果想过滤这种烦人的提示又想偷懒,那就只能借助编译器配置了,如下图,将 Strict Prototypes 的值设置为 NO,警告就不会再出现了。

使用 strong 而不是 assign 修饰 dispatch 对象

作者: NotFound--

当运行系统是在 iOS6 以下时,是需要通过 dispatch_retaindispatch_release 来管理 dispatch queue 的生命周期的,此时应该使用 assign 来修饰 dispatch_queue_t 类型的对象。在 iOS6 及以后是通过 ARC 来管理 dispatch queue 对象的生命周期的,所以应该使用 strong 来修饰 dispatch_queue_t 类型的对象。这里以支持 iOS5 系统的 SDWebImage(version:3.7.6) 的代码举例:

#if OS_OBJECT_USE_OBJC
    #define SDDispatchQueueSetterSementics strong
#else
#define SDDispatchQueueSetterSementics assign
#endif

@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

OS_OBJECT_USE_OBJC 是一个编译器选项,当我们工程里面设置的 Deployment target 大于或等于 iOS 6 时,OS_OBJECT_USE_OBJC 的值会是 1,否则会是 0。因为我们现在的 app 普遍都是支持到 iOS9 或者 iOS8,所以 dispatch_queue_t 类型的对象都是使用 ARC 来进行管理的,我们使用 strong 来修饰就好了。

【示例】

在美团近期开源的 UI 渲染框架 Graver 中也发现,错误得使用 assign 来修饰 dispatch_queue_t 类型的属性(如图一所示),

对 Graver 框架实际测试时,发现将一个 dispatch_queue_t 类型的局部变量赋值给对 assign 修饰的 dispatch_queue_t 后(如图二所示),

会抛出了野指针异常(如图三所示)。

然后去 github 上搜了一下“assign dispatch_queue_t”,发现很多代码也是使用这种错误的写法,所以觉得有必要写个 tip,提醒一下大家。

关于UIStackView的一个小知识点

作者: 高老师很忙

今天分享一个UIStackView的小知识点,用UIStackView做水平或垂直布局很方便,搜了大多数UIStackView的资料,大多是教大家如何使用的axis、alignment等属性的。最近使用时遇到了:把UIStackView中某个视图hidden后,UIStackView的布局会进行更新,只展示没有hidden的视图(官方文档截图:图1),

例如,你有5个视图平等分显示,设置某个视图hidden之后,就会变成4个视图平等分了。

有的时候这是我们期许的,而有的时候并不是;如果hidden某个视图后,不想更改其他视图布局,那么可以设置alpha,或者使用Masonry的方法,之前小集也提过(图2)。

了解这个属性之后,免得大家开发过程走弯路,根据情况选择适当的方式布局。

Command Not Found

作者: Lefe_x

使用终端命令的时候常会出现Command Not Found这个错误,我们今天聊一聊这个错误出现的基本原因,出现这个问题一般因为下面4种原因;

  • 输入命令时语法错误,命令行有语法规则,必须按语法规则写;
  • 命令并没有安装,有时候安装的时候忽略了错误;
  • 命令被删除或破坏了;
  • 用户的 $PATH 不正确,大部分原因都是这个导致的;

出现前三种错误都比较好解决,第四中错误比较常见,有时候明明安装完成了,却还会报这个错误。命令行程序之所以可以执行是因为它本身是一个可执行程序或者是一个脚本。当在终端中输入命令的时候,操作系统会找对应的可执行文件并执行。操作系统会从环境变量$PATH中依次查找可执行文件,直到找到,如果找不到将报 Command Not Found 这个错误。

查看我电脑的环境变量 $PATH 中包含了(每个路径通过冒号分割):

➜  ~ echo $PATH
/opt/MonkeyDev/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

如果报 Command Not Found 这个错误,首先通过echo $PATH查看环境变量中是否已经存在了可执行文件的路径。如果没有打开.bash_profile把可执行文件地绝对路径写进去即可。

➜ vi $HOME/.bash_profile
export PATH="$HOME/Library/Android/flutter/bin:$PATH"

// 想让刚配置的 PATH 生效,需要刷新终端
➜  source $HOME/.bash_profile

针对 objc_exception_throw 的实用调试技巧

作者: Vong_HUST

相信调试过程中发生崩溃这种事情,大家肯定都遇到过,一般也会给 Xcode 设一个全局共享的异常断点,如图1所示,(如果没有的话,可以设置一波)。当我们调试遇到抛出异常时,Xcode 会自动断点,输出一些关于 Exception 的日志信息。但是有些时候并不见得会输出有用的日志(或者压根就没有日志)只有对应的崩溃栈,如图2所示。

示例中向 NSArray 发了一条无法响应的消息,崩溃后 Xcode 自动断点到了相应的断点位置(这里其实 Xcode 已经在 console 中输入了对应的崩溃信息,因为一时半会不知道该怎么制造 Xcode 不输出日志的环境,所以将就用这个示例来代替下),同时左边也有了对应的崩溃调用栈。我们可以将调用栈切到最上方的 objc_exception_throw,然后在 console 中输入 po $arg1,因为 arg1 代表的是对象本身,在这里就是 NSException,而它又复写了 description 方法,所以对其 print 输出的是对应的崩溃信息。

以上其实我们还可以节省一个步骤,就是编辑一下这个全局异常端点,给起加一个 Debugger Command 的 Action,如图3所示,这样就可以在发生 objc_exception_throw 崩溃的时候,就可以自动输出对应的崩溃信息了,而不用再手动切换到栈顶的 objc_exception_throw 再输一遍 po $arg1。需要明确一点的是,这种方式仅适用于 objc_exception_throw 类型的崩溃(模拟器、真机都适用)。

其他几个有意思的参数值,上面说到 arg1 是当前断点所在方法的接收对象,arg2 是被调用的方法名(在 po 的时候要做一个强转,如 po (SEL)$arg2),如果有参数则 arg 依次递增。

另外 lldb 的其它更多命令及便捷或扩展的方式,推荐 Facebook 的 Chisel 个人使用频率最高的就是真机调试动画,放慢动画速度的命令,运行过程中触发任意一个断点,执行 slowanim 即可(默认10倍速慢放,可自行在后面指定慢放倍数,如 slowanim 0.2 就是慢放5倍)。

如果你有更多的小技巧欢迎分享,欢迎交流~

参考链接:Xcode: One Weird Debugging Trick That Will Save Your Life

Xcode 工程设置构建版本号自动递增

作者: KANGZUBIN

在一个 iOS 工程中,通常有两种“版本号”,即 VersionBuild,如图 1 所示:

  • Version 为发布版本号,标识应用程序发布的正式版本号,通常为两段式或者三段式,例如:1.2.11.0 等,其 Key 为 CFBundleShortVersionString,在 Info.plist 文件中对应 "Bundle versions string, short";

  • Build 为构建版本号,标识应用程序构建(编译)的内部版本号,可以有多种方法表示:时间表示(e.g. "20190122080211")、字母表示(e.g "ABC")、以及递增的数字(e.g. "100")等。它一般不对外公开,在开发团队内部使用。其 Key 为 CFBundleVersion,在 Info.plist 文件中对应 "Bundle version";

在 App Store 发布应用时,使用的是 “Version” 版本号,在同一个 “Version” 号下, 开发者可以上传不同 “Build” 构建版本。此外,对于 “Build” 号,我们最常使用 “递增的数字” 来表示。

同时,苹果为我们提供了一个 agvtool 命令行工具,用于自动增加版本号,具体使用方式如下:

首先,在 Build Settings 配置项中,设置 Current Project Version 为选定的值,例如 100(可以为整数或浮点数,新工程一般设为 1),agvtool 命令会根据这个值来递增 “Build” 号。另外需要再选择 Versioning System 的值为 Apple Generic,如图 2 所示。

然后,在 Build Phases 中,点击 “+” 号,选择 “New Run Script Phase” 添加一个执行脚本,并设置以下脚本代码,如图 3 所示:

xcrun agvtool next-version -all

以上,我们在每次编译工程时,“Build” 号就会自动递增加 1 了。

关于 agvtool 命令的更多使用方式,可以参考这里

最后,上述配置在多人开发或者多分支开发时,可能会导致 “Build” 号冲突,因此,我们可以只在日常给测试人员打包的机器上配置就好了。

Swift 中实现 synchronized

作者: 南峰子

Objective-C 中的 @synchronized 大家都应该很熟悉,用来对一段代码块加锁。不过在 Swift 中没有提供对应的关键字执行相同的操作。所以如果要�使用类似的 synchronized,则需要自己动手。

以下是 RxSwift 中的实现方式:

extension Reactive where Base: AnyObject {
    func synchronized<T>(_ action: () -> T) -> T {
        objc_sync_enter(self.base)
        let result = action()
        objc_sync_exit(self.base)
        return result
    }
}

可以看到是通过 objc_sync_enterobjc_sync_exit 来对代码块加锁。而实际上 Objective-C 中的 @synchronized 也是基于这两个函数来实现的。如果有兴趣,可以查看一下源代码

参考链接

点击cell不执行-[UITableView didSelectRowAtIndexPath:]方法的几种方式

作者: 高老师很忙

今天分享一个比较常用的知识点,点击某个UITableViewCell不执行-[UITableView didSelectRowAtIndexPath:]方法的几种方式:

  • 可以直接设置cell.userInteractionEnabled = NO

  • 可以实现UITableViewDelegate中的-[UITableView shouldHighlightRowAtIndexPath:]方法,设置对应indexPath返回NO;

  • 可以实现UITableViewDelegate中的-[UITableView willSelectRowAtIndexPath:]方法,设置对应indexPath返回nil,不过这种方式cell还是会有高亮效果,需要手动设置对应cell.selectionStyle = UITableViewCellSelectionStyleNone

以上三个方法,都不会进UITableViewDelegate的-[UITableView didSelectRowAtIndexPath:]方法。用第一种方式设置后,cell上的所有子View都不能被点击了;而第二种方式不会影响cell的子View的响应事件的传递,如果cell上有UIControl的子类,依然可以被点击;第三种方式也不会影响cell的子视图的响应事件,但是需要额外设置不显示高亮效果。当然,你也可以在-[UITableView didSelectRowAtIndexPath:]方法的对应indexPath直接return,只要你高兴😂,可以根据实际情况选择合适的方法。

有更优雅的方式,欢迎一起讨论。

NSScanner 过滤字符串

作者: 这个汤圆没有馅

在使用<ContactsUI/ContactsUI.h>框架获取通讯录手机号码时,不同的 iOS 系统最后得到的手机号码也不同。有的是xxx-xxxx-xxxx,有的是 xxx xxxx xxxx。为了得到有效的手机号码,可以用正则过滤字符串。如以下代码。

NSMutableString *mobile = [NSMutableString stringWithString:@"131-0000-2222"];
NSMutableString *phone = [NSMutableString string];
for(int i =0; i < [mobile length]; i++) {
    NSString *temp = [mobile substringWithRange:NSMakeRange(i,1)];
    NSString *regex = @"^[0-9]+$";
    NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];
    if ([pred evaluateWithObject:temp]) {
       [phone appendString:temp];
    }
}

除了正则外,今天要介绍的是NSScanner 过滤器。先看一下 apple 文档里对 NSScanner 的说明。【一个字符串解析器,用于扫描字符集中的子字符或字符,以及十进制、十六进制和浮点表示形式的数值。】

常用属性有以下几个:

charactersToBeSkipped,设置忽略指定字符,默认是空格和回车。

isAtEnd,是否扫描结束。

scanLocation,扫描开始的位置。

NSScanner 扫描字符串得到有效的手机号码,代码如下:

NSString *originalStr = @"131-0000-2222";
NSMutableString *stripStr = [NSMutableString stringWithCapacity:originalStr.length];
NSScanner *scanner = [NSScanner scannerWithString:originalStr];
NSCharacterSet *numbers = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];
while ([scanner isAtEnd] == NO) {
    NSString *buffer;
    if ([scanner scanCharactersFromSet:numbers intoString:&buffer]) {
        [stripStr appendString:buffer];
    } else {
        [scanner setScanLocation:[scanner scanLocation] + 1];
    }
}

平时我们用的条件判断一般以 if正则表达式 居多,NSScanner 其实也是一个陌生且又强大的条件判断器。

如有表述不当,欢迎指出~~

聊聊 iPad 适配

作者: halohily

在最新版本中,我们为网易有道词典做了完全的 iPad 适配,你可以在 iPad 上横屏、分屏使用有道词典,也完全支持了屏幕旋转后的 UI 调整。今天来聊聊这次的适配体验。

千言万语总结成一句话:如果你正确使用了 autolayout(使用比例而非固定值),那么即使是原本仅支持竖屏的页面,在打开转屏开关后,页面也不会有太大的问题。况且苹果已经提升了 autolayout 的性能,所以如果你的 app 未来有支持 iPad 的潜在可能,那么尽可能地全部使用 autolayout 来布局吧。

如果现有的大量页面都已经使用了计算 frame 的方式来布局,也有解决办法。UIView 的子类 要保证在 layoutSubviews 方法内进行布局(根据 self 宽高而不是屏幕或 window 尺寸),这本来也是一个标准做法。对于 ViewController,系统提供了 viewWillLayoutSubviews 方法,类似于 layoutSubviews方法,你可以在这里进行 vc.view 及其子 view 的布局。在转屏、分屏后这些方法都会被触发。

需要注意的是,iPad 上不仅有旋转屏幕的操作,还有分屏的操作,系统也提供了进入分屏的系统通知,如果需要可以进行监听。比如大多数拍照 app 会在进入分屏后为用户弹一个分屏无法拍照的 alert