美文网首页技术重塑iOS Swift && Objective-CiOS
[iOS] 如何获取一个更为靠谱的当前时间

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

作者: pingpong_龘 | 来源:发表于2016-10-29 01:27 被阅读2826次

    闲话扯淡

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

    背景

    这次发新版本的时候,改一个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的启动时间"

    </br>

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


    屏幕快照 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代码如下:

    屏幕快照 2016-10-29 上午12.44.54.png

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

    屏幕快照 2016-10-29 上午12.53.32.png
    里面的clock_get_boottime_nanotime函数是在/osfmk/kern/clock.c中实现的。
    屏幕快照 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...

    </br>

    2.3 还遗漏了一种方式mach_absolute_time

    2.3.1

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

    2.3.2

    show me the code:

    #include <mach/mach_time.h>
    
    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

    查到的相关资料:

    相关文章

      网友评论

      • 落枫猿:servetime 是怎么获取的?
        pingpong_龘:@落枫猿 专门的API请求到server时间,持久化在本地
      • Locke:您好,我想问一下,通过 sysctl 获取系统内核的启动时间,影不影响 app 的上架。
        远航的移动开发历程:大神,其实我们可以,在获取到服务端时间的时候同时获取一下uptime一同保存下来, 之后每次启动APP的时候 获取一次最新的uptime,与之前记录保存的uptime比较,如果小于之前的uptime,说明用户重启过手机,我们需要去更新timeroffset. 这样就不用在定时去更新本地保存的服务端时间同步问题吧。
        Locke:@pingpong_龘 好的,谢谢。:+1:
        pingpong_龘:@Locke 不影响,我们自己也在用。
      • 大_K:如果改了本地时间就需要,和服务器进行时间校验了。
      • 当年轻遇见梦想:好文章,解决了我的问题。

      本文标题:[iOS] 如何获取一个更为靠谱的当前时间

      本文链接:https://www.haomeiwen.com/subject/wdjeuttx.html