[iOS] 如何获取一个更为靠谱的当前时间

闲话扯淡

好久没更新文章(估算有小半年了),之前上半年有段时间处于换工作空档期,比较闲,于是更新了好多篇文章,后来一忙起来,全身心投入到业务需求开发中,基本就没太多时间研究技术的东西了。

背景

这次发新版本的时候,改一个bug时遇到一个时间戳的问题,想着好久没动笔了,就顺道把这东西记下来吧。
顺道立个flag:争取在17年到来之前,从这次开始,能多总结积累一些技术点,多出几篇文章。

需求

这次的需求非常简单,对于大部分童鞋估计扫一眼题目就知道123了,也不用再往后看了,我这里只当是个小技术点的记录吧。

言归正传,作为移动的App,平时向服务端发的所有request请求,作为基础数据,所有的request都应该带requestId和请求的时间戳以及设备信息,经纬度信息等等,这些应该作为默认选项。
关于时间戳,因为客户端的时间不是很靠谱(用户随时可能自己修改时间),所以原则上,客户端的时间仅仅作为参考。真正涉及DBA或是DB部门数据统计或计算的时候,必然已server端为准。
这里想说明的是,虽然条件有限,如何在有限的条件下,我们取一个尽量可以准确点的时间呢,这样当用户处于无网络或信号差的场景时,App需要执行打点记录或者某些进行一些需要时间的行为动作时,可以更加准确点。
前提是,用户可以随意修改时间,但是我们有木有办法绕过这个尴尬的现实,想个更巧妙的实现方式呢?

实现

1. 思路

首先,我们平时取时间的函数如下:

//ObjC
NSDate *date = [NSDate date];
// swift      
var date = NSDate()

那么这个方法不靠谱的话,只能再往深入一点想。
那就是直接获取底层的时间,想到的就是内核kernel的时间了。每次系统重启后,会一直记录启动至今的时间。

这个时间称之为upTime,我们获取server时间和uptime的差值timerOffset ,将这个值保存下来,后续每次发request请求的时候,基于timerOffset将upTime换算为上传的真正时间值即可,然后定时或者按照一定周期和服务端同步,将timerOffset进行修正。
其实简单点就是数学的a-b=c,a=b+c的问题,代码如下:

self.timerOffset = serverTimer - upTime;
NSTimeInterval uploadTime = upTime + self.timerOffset;

Okey,结构上来说还是比较清晰也比较简单的,唯一的难点在于获取启动时间Uptime,我们一步一步往下走。

2. 获取启动时间

2.1 ObjC/swift的方式获取

系统进程可以通过NSProcessInfo获取,然后他很好的提供给我们了一个Uptime接口,直接就可以获取到启动时间,代码如下:

//ObjC
NSTimeInterval systemUptime =[[NSProcessInfo processInfo] systemUptime];
//swift
var systemUptime = ProcessInfo().systemUptime;

这个获取到的时间但是是秒,打印一下,大概是这个样子:

(lldb) po [[NSProcessInfo processInfo] systemUptime]
11473.798027249

然后,然后,就完事了? 好像我们的需求就这么达到了?
嗯,至少看上去是这样,然后,直接写代码,git push了...
当然,理论上是这样的,
但是作为一个有想法的coder,怎么能就此打住呢,还是需要继续探索啊...

2.2 通过C的方式获取kernel_task的启动时间

2.2.1

time这个东西,不同于其他,原则上越精准越好,当然,对于大部分App来说,是基本没需求的,但是对于学习和既能提升来说,还是可以再往深里考虑考虑的。
首先OC或者swift来说,毕竟隔了一层编译,效率上毕竟和C或者Java还是有那么点差距,所以需要考虑有没有通过C的方式实现这个问题。

2.2.2

既然我这么问了,答案自然是有的。

"就是获取kernel_task的启动时间"


kernel_task是个系统级的task,用mac的人可能比较熟,很多人当电脑比较烫或者风扇转的比较响的时候,看一下活动监视器,就会看到kernel_task,如下截图:


[iOS] 如何获取一个更为靠谱的当前时间_第1张图片
屏幕快照 2016-10-28 下午5.24.03.png

kernel_task包括多线程调度管理、虚拟内存、系统IO、线程之间通信等等。所以系统已启动,kernel_task就会跑起来,kernel_task运行的时间,就可以作为启动时间来使用。

2.2.3

获取kernel_task的启动时间,我们需要用到一个函数sysctl, 首先看一下sysctl函数的定义:

int sysctl(int *, u_int, void *, size_t *, void *, size_t);
  • sysctl的第一参数是个数组,按照顺序第一个元素指定本请求定向到内核的哪个子系统,第二个及其后元素依次细化指定该系统的某个部分,类推...
  • 之后是数组的长度u_int.
  • 后面的size_t *需要注意,当sysctl被调用时,size指向的值指定该缓冲区的大小;函数返回时,该值给出内核存放在该缓冲区中的数据量,如果这个缓冲不够大,函数就返回ENOMEM错误
  • sysctl函数的结果:成功:返回0; 失败:返回-1

2.2.4

一言不合show me the code:

- (time_t)uptime
{
    struct timeval boottime;
    int mib[2] = {CTL_KERN, KERN_BOOTTIME};
    size_t size = sizeof(boottime);
    time_t now;
    time_t uptime = -1;

    (void)time(&now);

    if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
    {
        uptime = now - boottime.tv_sec;
    }
    return uptime;
}

具体sysctl的代码,可以看/bsd/kern/kern_sysctl.c
其中的sysctl_boottime函数可以获取boottime,sysctl_boottime代码如下:

[iOS] 如何获取一个更为靠谱的当前时间_第2张图片
屏幕快照 2016-10-29 上午12.44.54.png

里面再往里看能看到tv_sec是通过boottime_sec获取的,
boottime_sec的代码在/bsd/kern/kern_time.c中。

[iOS] 如何获取一个更为靠谱的当前时间_第3张图片
屏幕快照 2016-10-29 上午12.53.32.png

里面的clock_get_boottime_nanotime函数是在 /osfmk/kern/clock.c中实现的。
[iOS] 如何获取一个更为靠谱的当前时间_第4张图片
屏幕快照 2016-10-29 上午12.55.20.png

这里插一句,如果大家去查sysctl,会查到sysctl在iOS9以后不可用的一些信息,这里我自己没直接找到相关的文档,但是我们这个时间的接口是可用的,iOS10亲测正常。唯一查到的相关信息是通过sysctl获取进程信息的那个路子被堵死了。
那个代码如下,大家不要搞混淆了.

    int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL ,0};
    size_t miblen = 4;
    size_t size;
    int st = sysctl(mib, miblen, NULL, &size, NULL, 0);

想深入了解,看这里https://nabla...


2.3 还遗漏了一种方式mach_absolute_time

2.3.1

mach_absolute_time这个也是在查询sysctl相关信息的时候,意外发现的.
它不会直接告诉你一个时间,你可以理解为CPU进程依赖的一个函数,它会返回系统重启到现在的一个时钟“滴答”数,这个值自然是不能直接用的,毕竟是硬件层概念使用的一个参数,但是我们经过简单处理,就可以获取所需要的启动时间。

2.3.2

show me the code:

#include 

int getUptimeInMilliseconds()
{
    const int64_t kOneMillion = 1000 * 1000;
    static mach_timebase_info_data_t s_timebase_info;

    if (s_timebase_info.denom == 0) {
        (void) mach_timebase_info(&s_timebase_info);
    }

    // mach_absolute_time() returns billionth of seconds,
    // so divide by one million to get milliseconds
    return (int)((mach_absolute_time() * s_timebase_info.numer) / (kOneMillion * s_timebase_info.denom));
}

2.3.3

但是这个方法使用的人不多,原因在于,如果系统处于休眠状态,那么这个值也是停止的,所以它本质是进程运行的时钟计数器(run()),而非整个系统的(run() + sleep())时钟计数器。这样严格来说,是完全不准确的数据,不可用。
但具体相关我也没深入研究过,有了解过的童鞋,可以评论或私信给我,指教我一下。

这个较为多的使用场景是作为耗时的测量函数,比如在某个时间获取mach_absolute_time作为startTime,间隔一定时间之后再次获取mach_absolute_time作为endTime,然后差值可以作为这个阶段的耗时,这样的使用场景较多。

2.3.4

查到的相关资料:

  • stackoverflow:有人问这个 mach_absolute_time的真正含义,因为他发现自己的设备并没有重启,但是基于mach_absolute_time算出来的时间,自己的设备在某一时刻重启过,所以对这个东西"一脸懵逼"...
    http://stackoverflow.com/questions/1450737/...

  • Apple 官方对某个开发者的提问What time units does it use?进行的回复.
    https://developer.apple.com/library/content/qa/qa1398/...

  • 某位大牛的博客文章: mach_absolute_time使用
    http://www.devdiv.com/mach_absolute_time...

你可能感兴趣的:([iOS] 如何获取一个更为靠谱的当前时间)