Skip to content
Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
389 lines (264 sloc) 21.4 KB

2017.12

iOS中的mach_continuous_time()方法

作者: 高老师很忙

mach_absolute_time() 这个函数大家应该比较了解:返回的是CPU已经运行的时钟周期数,可以转化为秒数,这个是不会受本地时间影响的,不过当手机重启会重新开始计数,锁屏休眠后会暂停计数。今天看文档惊喜的发现了 iOS10 之后提供了 mach_continuous_time() 方法:

注释写的是:

like mach_absolute_time, but advances during sleep

突然感觉好惊喜,查看了一下源码:

通过休眠测试(使用 iPhone8+ , iOS11 ,删除所有 APP, 关掉网络和蓝牙等,先运行 demo 获取一个时间,锁屏过了几个小时,再次获取时间),结果果然可喜:

不过重启后仍然会重置。顺便再介绍一下 mach_approximate_time(),是获取一个大约时间,暂时没有用过,可以简单看下源码:

比较好理解。希望能帮助到大家!

objc_getClass和object_getClass

作者: 南峰子_老驴

昨天打开好久没开的公众号后台,看到一个小伙伴 @阳光下的小泡沫星人002 给我留言,指正我之前写的关于Runtime的文章 中一个错误之处,并为此写了一篇文章 在此感谢。

这个错误是关于获取 NSObject 的元类的 isa 指针的问题。在我的文章中,错误的用 objc_getClass((__bridge void *)[NSObject class]) 这种方式来获取对象的指针。代码如下:

void TestMetaClass(id self, SEL _cmd) {
    NSLog(@"This objcet is %p", self);
    NSLog(@"Class is %@, super class is %@", [self class], [self superclass]);
    Class currentClass = [self class];
    for (int i = 0; i < 4; i++) {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = objc_getClass((__bridge void *)currentClass);
    }
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", objc_getClass((__bridge void *)[NSObject class]));  // 0x0
}

实际上 objc_getClass 的参数是类名的字符串,获取指定类的类对象。上述代码中 (__bridge void *)[NSObject class]) 的结果并不是类名的字符串,而是一个对象的指针,所以 objc_getClass 函数返回值的相当于是一个 nil,打印指针的值就是 0x0。所以正确使用 objc_getClass 的方式应该是objc_getClass("NSObject"),其效果与 [NSObject class] 是一样的。

要想获取 NSObject 类对象的元类,可以使用 object_getClass 函数。这个函数参数是 id 类型,即一个对象,返回对象的 Class 信息,即对象的 isa 指针;如果传入的是一个类对象,获取的就是元类信息。

所以正确的代码如下,for 循环中的输出结果也印证了 NSObject 元类的isa指向的是其本身。

void TestMetaClass(id self, SEL _cmd) {
    NSLog(@"This objcet is %p", self);
    NSLog(@"Class is %@, super class is %@", [self class], [self superclass]);
    Class currentClass = [self class];
    for (int i = 0; i < 4; i++) {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = object_getClass(currentClass);
    }
        
    // Following the isa pointer 0 times gives 0x10048e1f0
    // Following the isa pointer 1 times gives 0x10048e220
    // Following the isa pointer 2 times gives 0x7fff8c8e50f0
    // Following the isa pointer 3 times gives 0x7fff8c8e50f0
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));      
        
    // NSObject's class is 0x7fff8c8e5140
    // NSObject's meta class is 0x7fff8c8e50f0
}

由于博客已停更,错误之处可以在微博上私信。还请阅读的时候不要盲从。

Gerrit 是阻挡不了你使用 SourceTree

作者: Lefe_x

当我们提交的代码需要 Review 的时候,需要用到 Gerrit,具体关于 Gerrit 的介绍可以看 [这里] (https://gerrit-review.googlesource.com/Documentation/) 。使用 Gerrit 后,执行 push 操作的时候,不能直接使用 git push 命令,也就说你不能使用 SourceTreePush 功能,只能在终端乖乖的输入 git push origin HEAD:refs/for/dev 。有些同学可能会问,我特别想使用 SourceTree ,不想使用终端命令,有没有好的方法?其实,SourceTree 提供了一个功能:【自定义操作】(SourceTree -- 偏好设置 -- 自定义操作 -- 添加),导入事先写好的脚本。还可以设置一个快捷键(这里设置了 cmd+p)。脚本如下:

#!/bin/bash
cd /Users/wangsuyan/Desktop/iOS
git push origin HEAD:refs/for/Dev0.0.1

备注:记得给脚本执行权限。 如果你没有给自定义的操作设置快捷键,可以通过【动作 -- 自定义操作】选择执行你的 Action。

这样,当你 push 的时候直接 cmd+p 即可提交你的代码,是不是很爽。当然你可以写一些其它的脚本,并自定义为 Action ,来提高你的工作效率。你可以看我以前写的 [脚本教程] (http://www.jianshu.com/p/8a975f358de8) 。

是谁调了我的底层库

作者: Lefe_x

调试的时候,往往底层库会埋一些 NSLog 来调试,使用下面这种方式打印出来的函数名称 __PRETTY_FUNCTION__ 是底层库的函数名。

#   define LEFLog(fmt, ...) NSLog((@"%s (%d) => " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)

打印是这样的:+[Network post] 中打印了 I am a log,而不清楚是谁调用了。

+[Network post] (22) => I am a log

但是,我想要的是我在最顶层调用时的函数名,这样我可以很容易的看到是那个方法调用了底层库。

不太理解?举个例子吧: 每个 APP 都会有一个网络层,业务层会直接与网络层进行交互。调试的时候,我想知道 A 请求是在哪个页面中的哪个函数被调用了,咋么办?前提是 NSLog 在底层库。我们可以这样实现:

@implementation LEFLog
+ (NSString *)lastCallMethod
{
    NSArray *symbols = [NSThread callStackSymbols];
    NSInteger maxCount = symbols.count;
    NSString *secondSymbol = maxCount > 2 ? symbols[2] : (maxCount > 1 ? symbols[1] : [symbols firstObject]);
    if (secondSymbol.length == 0) {
        return @"";
    }
    
    NSString *pattern = @"[+-]\\[.{0,}\\]";
    NSError *error;
    NSRegularExpression *express = [NSRegularExpression regularExpressionWithPattern:pattern options:kNilOptions error:&error];
    if (error) {
        NSLog(@"Error: %@", error);
        return @"";
    }
    
    NSTextCheckingResult *checkResult = [[express matchesInString:secondSymbol options:NSMatchingReportCompletion range:NSMakeRange(0, secondSymbol.length)] lastObject];
    NSString *findStr = [secondSymbol substringWithRange:checkResult.range];
    return findStr ?: @"";
}

@end

然后定义一个宏:

#   define LEFLog(fmt, ...) NSLog((@"%@, %s (%d) => " fmt), [LEFLog lastCallMethod], __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__

打印结果是这样的:在 LefexViewController 中的 viewDidLoad 调用了 Network post 方法,并打印 I am a log.

-[LefexViewController viewDidLoad], +[Network post] (22) => I am a log

指定构造器在 UITableViewController 中的坑

作者: Vong_HUST

我们都知道,如果我们想要自定义指定构造器时,应该要遵循以下3个原则(图1): 1、子类指定构造器必须调用父类指定构造器 2、便捷构造器只能通过调用自身指定构造器来完成初始化 3、指定构造器必须要用 NS_DESIGNATED_INITIALIZER 标示 但是如果你继承了 UITableViewController,并且自定义了指定构造器,而你的项目刚好要支持 iOS8 的话,在 iOS8 下就会出现一个必崩的 bug。示例代码及简单解释见图2。 有人提了对应的 radar, stackoverflow 上也有对应的详尽解释 更多内容可查看 目前唯一的解决方案就是不继承 UITableViewController,而是继承自 UIViewController 然后持有一个 UITableView😂

为断点加条件和简单命令

作者: 高老师很忙

今天主要给大家介绍一个如何给断点加条件和命令来提高我们的调试效率。例如:一个for循环,在第n次循环的时候有一个bug,这个时候条件断点就有了用武之地,通过右击断点,选择编辑断点;

我们可以设置Condition:i= n,直接断到我们想要调试的那次循环;

或者也可以用Ignore功能来设置忽略前面不关心的循环;

通过设置Action功能还可以在断点处执行一些简单的命令、打印日志、播放声音、执行脚本等,

让调试变的欢乐起来吧!

SourceTree 搭配 Kaleidoscope 进行 Code Review

平时在 GitlabGitHubSourceTree 上进行 CodeReview 的时候,只能看到发生改动的地方,想要查看改动点对应的上下文时非常麻烦(网页上要不断的点展开)。今天给大家介绍一个我们小组内 CodeReview 时用到的工具及其配置,如果你有其它方式,欢迎一起交流探讨。确保配置前已安装好 SourceTree 以及 Kaleidoscope,配置方式如下

  • 点击 Kaleidoscope 菜单 –> 点击 Integration –> 下载 ksdiff(点击 Read More

  • 安装完成之后,删除 ~/.gitconfig 文件中 difftoolmergetool 相关配置(删之前最好备份一下原文件)

  • 打开 SourceTree 的偏好设置,按下图的方式配置。两处 Command 都填 /usr/local/bin/ksdiffArgument 分别为 --partial-changeset --relative-path "$MERGED" -- "$LOCAL" "$REMOTE"--merge --output "$MERGED" --base "$BASE" -- "$LOCAL" --snapshot "$REMOTE" --snapshot。这两个参数也可以按自己的需求来配置。

  • 以上设置完成之后可以给 SourceTree 加一个自定义动作,快捷键按自己的喜好设置,参数项填 difftool -y -t sourcetree $SHA HEAD 即可。

以上配置完成之后就大功告成了,使用方式就是在 SourceTree 中选中一个非 HEADcommit,然后按下快捷键,就会在 Kaleidoscope 打开所有改动过的文件。

Swift 4.0 中 Dictionary 编码成类数组结构的字符串

在Swift 4.0中,使用Codable来编码一个Dictionary时,某些情况下得到的可能并不是类似于字典/对象结构的字符串,而可能是一个类似数组结构的字符串,如下代码所示:

let dict: [Float : String] = [
    18.0: "ff0000",
    20.0: "00ff00",
    21.0: "0000ff"
]

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let encoded = try! encoder.encode(dict)
let jsonText = String(decoding: encoded, as: UTF8.self)
print(jsonText)

//[
//  21,
//  "0000ff",
//  18,
//  "ff0000",
//  20,
//  "00ff00"
//]

这里的dictFloat类型为key,结果是输出一个类似数组的字符串。查看源码中DictionaryEncodable协议的扩展实现,如下代码所示:

extension Dictionary : Encodable /* where Key : Encodable, Value : Encodable */ {
    public func encode(to encoder: Encoder) throws {
        assertTypeIsEncodable(Key.self, in: type(of: self))
        assertTypeIsEncodable(Value.self, in: type(of: self))

        if Key.self == String.self {
            // Since the keys are already Strings, we can use them as keys directly.
            var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
            for (key, value) in self {
                let codingKey = _DictionaryCodingKey(stringValue: key as! String)!
                try (value as! Encodable).__encode(to: &container, forKey: codingKey)
            }
        } else if Key.self == Int.self {
            // Since the keys are already Ints, we can use them as keys directly.
            var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
            for (key, value) in self {
                let codingKey = _DictionaryCodingKey(intValue: key as! Int)!
                try (value as! Encodable).__encode(to: &container, forKey: codingKey)
            }
        } else {
            // Keys are Encodable but not Strings or Ints, so we cannot arbitrarily convert to keys.
            // We can encode as an array of alternating key-value pairs, though.
            var container = encoder.unkeyedContainer()
            for (key, value) in self {
                try (key as! Encodable).__encode(to: &container)
                try (value as! Encodable).__encode(to: &container)
            }
        }
    }
}

可以看到当 key 的类型为 StringInt 时,使用 _DictionaryCodingKey 来编码 key,进而编码 key-value 对,最后以类似字典的结构输出(依赖于 keyed container ),这是因为在 Codable 系统中,这两个类型是有效的可编码 key 类型;而其它类型则不是,由于 Dictionary 无法告诉其它类型如何编码自身,所以将这些值 key 存储在一个 unkeyed container 中,最终处理成一个数组。

参考:

  1. UnkeyedEncodingContainer
  2. Why Dictionary sometimes encodes itself as an array

CoreAnimation 与 pop 的对比

最近在 Medium 上看到一篇《Should you use POP?》的文章,文章主要通过以下几个方面来对比了 popCore Animation

  • Core Animation 工作原理:每次添加动画时 QuartzCore 会打包其参数,然后通过进程间通信的方式传递给一个名为 backboardd 的后台进程。然后该进程通过 OpenGL 渲染和处理 layer 的层级以及 layer 上的动画。最重要的一点就是该进程完全独立于你的应用,应用只会拿到动画开始和结束的回调(CAAnimationDelegate),不负责动画的渲染(显式动画除外)。也就是主线程和 CoreAnimation 不会互相影响,也就是即使主线程阻塞了,CoreAnimation 依旧在执行。

  • pop 的工作原理:使用 CADisplayLink 来开启一个 fps=60 的渲染工作。每当 CADisplayLink 回调触发时,更新一下动画的进度。也就是每一帧发生改变时都需要通知 backboardd 来渲染,因为它对于 layer 的变化并不知情。

  • 由于 pop 必须在主线程上处理动画,所以 pop 动画很有可能发生卡顿。作者写了一个 Demo 来演示对应效果,效果如下图,左边为 Core Animation 的方式,右边为 pop 的方式。

  • 作者对比了 Core Animationpop 的性能。同样动画效果情况下(运行10s),使用 Time Profilerbackboardd (CoreAnimation) 和应用(pop) CPU 消耗,iPhone4 iOS7.1.2iPhone6 iOS8.1.1 的对比结果下图所示(左边 iPhone4,右边 iPhone6)。可以看出两者 backboardd 进程的 CPU 耗时差别在 100ms 左右,但是应用的 CPU 耗时差距十分明显,Core Animation 应用 CPU 耗时接近于0。

  • 结论如下图所示。Core Animation 优点为:①单独进程运行 ②不会阻塞主线程。缺点为:①复杂动画效果要写冗长的代码 ②手势驱动动画比较复杂。 pop 的优点为:①丰富的 API ②内置很多的动画 缺点为:①在主线程上执行 ②动画过程可能卡顿 ③消耗更高的 CPU

宏中的 ## 的含义

作者: Lefe_x

在宏的定义中,我们也许会遇到过 ##,比如下面是一些第三方库中 ## 使用场景:

微信 WCDB 中的宏定义: #define __WCDB_BINDING(className) _s_##className##_binding

唱吧 KTVHTTPCache 定义不同类中是否可以打印的例子: #define KTVHCLogEnableValueConsoleLog(target) KTVHCLog_##target##_ConsoleLogEnable

## 有什么用呢? ## 在宏中的作用就是先分隔,然后进行强制连接。我们可能会定义不同的函数名或变量时就可以使用这样的宏定义。

## 是如何工作的呢?

  1. __WCDB_BINDING(className) ,首先 _s_##className##_binding 会拆分成 _s,className ,_binding 。__WCDB_BINDING(ViewController) 将会被替换成 _s_ViewController_binding

  2. KTVHCLogEnableValueConsoleLog(target),首先 KTVHCLog_##target##_ConsoleLogEnable 会被拆分为 KTVHCLog_, target _ConsoleLogEnable。KTVHCLogEnableValueConsoleLog(Lefex) 会被替换成 KTVHCLog_ Lefex_ConsoleLogEnable

3.当使用 KTVHCLogEnable(HTTPServer, YES) ,将会定义一个名为 KTVHCLog_ HTTPServer_ConsoleLogEnable 静态常量,初始值为 YES。

#define KTVHCLogEnable(target, console_log_enable)               \
static BOOL const KTVHCLog_##target##_ConsoleLogEnable = console_log_enable;        \

比如我们使用不同的 View 名字创建不同的 View:

#define Name(target) weibo_##target##_name
#define View(target) view##target##Label

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString * Name(lefex) = @"Lefe_x";
    // 打印:You weibo name is: Lefe_x
    NSLog(@"You weibo name is: %@", weibo_lefex_name);
    
    UILabel *View(1) = [UILabel new];
    view1Label.backgroundColor = [UIColor redColor];
    
    UIView *View(2) = [UIView new];
    view2Label.backgroundColor = [UIColor yellowColor];
}

iOS快速解析崩溃日志

作者: Vong_HUST

平时我们拿到一份崩溃日志,需要解析,一般操作是取到对应的 dSYM 和对应的二进制文件,然后拿到相应的崩溃日志 uuid 、二进制的 uuid、dSYM 的 uuid 通过命令行来解析。这些操作感觉比较耗时。下面介绍几种情况下的快速解析的方法:

  • 1、自己设备上 Xcode 编译的包发生闪退:连上手机打开 Xcode,cmd+shift+2 呼出 Device 的 Window,如图1所示,然后点击 View Device Logs,然后选中对应时间段自己 app 的崩溃日志。如果此时对应的调用栈还没有符号化,可以选中日志后右键如图2所示 Re-Symbolicate Log 即可。

  • 2、如果是打包服务器或者 Appstore 的包发生闪退:拷贝对应的包和 dSYM 到任意文件夹下,注意将 dSYM 解压以及 .ipa 里面的 .app 取出。然后按照情况1的方式处理即可,Xcode 会自动索引二进制及 dSYM。

  • 3、如果拿到别的设备导出的未符号化的崩溃日志,可以将日志拖至图2所示的列表中,注意此时上面 tab 记得选 All Logs 而不是 This Device,然后参考情况2,找到崩溃日志对应的二进制包和 dSYM 文件,按照情况2处理即可。可能会遇到系统库的一些方法无法符号化的问题,只需要找到对应的设备连上电脑,让 Xcode 读取一遍该设备(同机型和系统版本的也可以)的符号表,然后再 Re-Symbolicate 一遍就行。

  • 4、遇到线上用户崩溃,无法拿到完整崩溃日志,可以让用户到【设置->分析->分析数据】里面找到对应时间点的崩溃日志,然后截图,根据一个开源工具 dSYMTools,把崩溃栈的关键地址输入到文本框中即可解析出崩溃的那个方法,具体使用方法参考 ReadMe

You can’t perform that action at this time.