一、引言

大数据分析最核心的是数据,我们不仅仅要做到采集数据,还需要把数据上传到指定的服务端。然后再经过服务端的存储、抽取、分析和展现,才能充分发挥数据真正的价值。

神策分析 iOS SDK 针对数据传输,从完整性、正确性以及高效性等多方面综合考虑,设计并实现了一套适用于数据采集的网络传输方案。

下面针对神策分析 iOS SDK 网络模块进行解析,希望能够给大家提供一些参考。

二、网络请求方案

iOS 中网络请求的实现有多种方式,例如:苹果官方提供的网络请求 API ;或者一些开源网络框架。下面分别介绍这两种方案的优缺点。

2.1 苹果官方 API

基于苹果官方 API 实现的网络请求一般使用 NSURLSession 或者 NSURLConnection 。但是,NSURLConnection 发送网络请求的相关接口在 iOS 9.0 开始已标记为过期。因此,开发者基本都加入了 NSURLSession 的阵营。下面我们看下 NSURLSession 有哪些优缺点。

基于 NSURLSession 实现网络请求方案具有以下优点[1]:

1. 基于连接的认证方案;

2. 支持 HTTP 请求配置;

3. 可私有化存储对象;

4. 支持后台上传下载。

但是,也存在以下缺点:

1. 没有一些开源网络框架的功能易用,例如:构建复杂的网络请求和响应的处理;

2. 部分细节没有封装,需要自己处理。

2.2 开源网络框架

基于开源网络框架实现网络请求方案具有以下优点:

1. 主流的开源网络框架功能丰富、相对稳定。例如:AFNetworking 等;

2. 可以方便快捷地实现网络请求的功能。

但是,也存在以下缺点:

1. 功能较多,代码逻辑复杂,学习成本较高;

2. 内部缺陷修复难度大甚至需要依赖作者来更新维护;

3. 包含很多可能使用不到的功能;

4. 引入开源网络框架后导致体积增大。

基于开源网络框架实现网络请求方案有利有弊,大家可以根据实际需求选择合适的方案。

三、SDK 网络模块

如果 SDK 网络模块基于开源网络框架实现,客户在集成 SDK 的同时也使用了同样的开源网络框架就会导致冲突。另外,维护开源网络框架会存在版本更新不及时、问题排查困难等风险。

由于基于开源网络框架存在这些弊端,SDK 网络模块采用 NSURLSession 的方式实现。

NSURLSession 是系统提供的网络访问 API,不仅可以满足 SDK 网络请求的需要,而且功能稳定、易拓展。

3.1 实现原理

NSURLSession 可以创建一个或者多个实例,每个实例协调一组相关的数据传输任务[1]。例如:在创建一个 Web 浏览器时,App 可能会为每个选项卡或者窗口创建一个会话。一个会话用于用户交互使用,另一个会话用于后台下载。在每个会话中,App 添加了一系列任务,每个任务都是表示对特定 URL 的请求。

3.2 具体实现

3.2.1. 网络相关配置

SDK 可以对数据发送进行一系列的配置,开发者可以根据具体需求设置相应的配置,从而达到高效的数据发送效果。

SDK 的相关配置在初始化时完成,可以配置的参数如下:

1. serverURL:数据发送地址,采集的数据会发送到该地址;

2. flushInterval:两次数据发送的最小时间间隔(单位毫秒),默认为 15000 毫秒;

3. flushBulkSize:两次数据发送的最小缓存条数,当本地缓存条数达到 flushBulkSize 时则会发送数据,默认为 100 条;

4. securityPolicy:SSL 证书和自签名证书的配置;

5. flushNetworkPolicy:数据发送时的网络策略。

3.2.2. 数据发送线程封装

SDK 数据发送是在子线程中完成的,当采集的数据满足发送策略时触发异步发送,上传任务会在 SAHTTPSession 类中完成。在初始化 SDK 时,创建 SAHTTPSession 实例,并实例化 NSOperationQueue 实现多线程发送事件。具体实现代码如下:

- (void)requestWithRecords:(NSArray<SAEventRecord *> *)records completion:(void (^)(BOOL success))completion {    [SAHTTPSession.sharedInstance.delegateQueue addOperationWithBlock:^{            ......        // 网络请求回调处理        SAURLSessionTaskCompletionHandler handler = ^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {            ......        };         // 转换成发送的 http 的 body        NSData *HTTPBody = [self buildBodyWithJSONString:jsonString isEncrypted:isEncrypted];        NSURLRequest *request = [self buildFlushRequestWithServerURL:self.serverURL HTTPBody:HTTPBody];        NSURLSessionDataTask *task = [SAHTTPSession.sharedInstance dataTaskWithRequest:request completionHandler:handler];        [task resume];    }];}

3.2.3. 数据发送策略

SDK 在采集到数据后会先储存到本地的 SQLite 数据库,在满足下面的发送策略时才会发送:

  • 客户端本地存储数据超过一定条数(默认 100 条)

在 SDK 初始化时,可配置 flushBulkSize 来限制条数(默认为 100 条)。如果用户设置的条数小于 50 条,则为 50 条。SDK 采集的数据较多,如果设置发送策略中的限制条数太小会导致频繁的网络请求,从而影响性能。如果用户设置的限制条数太大,会导致一次发送的数据过多。这样不仅会导致上传时间延长,还可能导致上传失败的概率增大。

  • 两次数据发送间隔一定时间(默认 15 秒)

在 SDK 初始化时,可配置 flushInterval 来控制两次数据发送的时间间隔(默认 15 秒)。如果用户设置的时间间隔小于 5 秒,则为 5 秒。SDK 会开启一个定时器,每隔 15 秒发送数据。

  • App 退到后台发送数据

SDK 监听了 UIApplicationDidEnterBackgroundNotification 通知,在 App 退到后台时发送数据。具体代码如下:​​​​​​​

- (void)applicationDidEnterBackground:(NSNotification *)notification {    ......    dispatch_async(self.serialQueue, ^{        [self.eventTracker flushAllEventRecords];        endBackgroundTask();    });}

除了上面提到的发送策略外,还会在触发以下事件时发送本地数据库中的数据:

1. 触发激活事件;

2. 触发 $SignUp 事件;

3. 触发 $AppRemoteConfigChanged 事件;

4. 通过远程配置禁用 SDK 时,会发送本地数据库中的全部数据。

3.2.4. 数据安全

3.2.4.1. 数据加密

SDK 发送的数据涉及到用户隐私,保护用户隐私是开发者应尽的责任和义务。SDK 提供数据加密功能,防止数据在传输过程中发生泄露。

目前 SDK 使用混合加密策略(RSA + AES)对数据进行加密,具体可分为两部分:

1. 使用生成的 AES 密钥对采集的数据进行 AES 对称加密;

2. 使用 RSA 公钥对 AES 密钥进行加密。

加密后的数据格式如下:​​​​​​​

[{    "pkv": 1,    "ekey": "e7lE4W67gUoER1al86Fg8CsMVhVpIDQReuONmwVyiIfZQA+U0J5J67UBnABFc6YKIYpWYPgAyQ5U+wPal17zOtyA7EeO2H+bxui1ESfKrh0pOaViElbHS7WTD9fBcAiOacNxukGlpjK70KWtSEFt+35XejWRw09AUIn8KeYSwnV7wetu4Ba783VvHsOd0vyWace3+I+T3tr7hiAnAxaeKtaYdeoKWCAydj8AM1jK+3z+kIc1aVTwDXKEw/Cw03EyO5wKF/0pHYBCkCnTRhXFIULVR6EDQWJh/fW0Bc5YpT2YDP9KRzXP6HfAML2/k7YkwuMRXhR4p12h0RPFxvmTRg==",    "flush_time": 1542625604461,    "payloads": ["Tg7E5sMghLePA3yW/1X6xO+MAPnncKvn9wYGk/T912JMljW0bK0hxXL14ttPY26uc1bksBHAqFW5xRb3LUYX+kcuM/N7shaw1/4XJcghw2JhexICA3Lf3Vsv37UtS0o8hW0LNq7kkSZt9wOa1Xb3agwtL7vhENtreqFBM+k+5ZH7MjVK8GalQdDauR7cZ1dtcprkFJiXuKrotp0DSeTCCtYiABlS3mDdVc1/NjbvvavbV4p4FC+R5VA8aDWszKC02gMyF4pohPKpYgJsFSwCKbNPoPY8TEop3HS6UnXDI6hlTPmzspaDfJHXThLhb83mnqWnrtQih2HllGUUthA2fKZj+QgqoGj7EHhjsGgaXxV0I7Op6NEe5EA1nOejOK8ibo7s0c67lkyb7AFcBfRNgJzhSkuZy3lqGXl61d1KIo3UT4+iBnRAgxZf", "Tg7E5sMghLePA3yW/1X6xO+MAPnncKvn9wYGk/T912JMljW0bK0hxXL14ttPY26uc1bksBHAqFW5xRb3LUYX+kcuM/N7shaw1/4XJcghw2JhexICA3Lf3Vsv37UtS0o8hW0LNq7kkSZt9wOa1Xb3agwtL7vhENtreqFBM+k+5ZH7MjVK8GalQdDauR7cZ1dtcprkFJiXuKrotp0DSeTCCtYiABlS3mDdVc1/NjbvvavbV4p4FC+R5VA8aDWszKC02gMyF4pohPKpYgJsFSwCKbNPoPY8TEop3HS6UnXDI6hlTPmzspaDfJHXThLhb83mnqWnrtQih2HllGUUthA2fKZj+QgqoGj7EHhjsGgaXxV0I7Op6NEe5EA1nOejOK8ibo7s0c67lkyb7AFcBfRNgJzhSkuZy3lqGXl61d1KIo3UT4+iBnRAgxZf", "Tg7E5sMghLePA3yW/1X6xO+MAPnncKvn9wYGk/T912JMljW0bK0hxXL14ttPY26uc1bksBHAqFW5xRb3LUYX+kcuM/N7shaw1/4XJcghw2JhexICA3Lf3Vsv37UtS0o8hW0LNq7kkSZt9wOa1Xb3agwtL7vhENtreqFBM+k+5ZH7MjVK8GalQdDauR7cZ1dtcprkFJiXuKrotp0DSeTCCtYiABlS3mDdVc1/NjbvvavbV4p4FC+R5VA8aDWszKC02gMyF4pohPKpYgJsFSwCKbNPoPY8TEop3HS6UnXDI6hlTPmzspaDfJHXThLhb83mnqWnrtQih2HllGUUthA2fKZj+QgqoGj7EHhjsGgaXxV0I7Op6NEe5EA1nOejOK8ibo7s0c67lkyb7AFcBfRNgJzhSkuZy3lqGXl61d1KIo3UT4+iBnRAgxZf"]}, {    "pkv": 1,    "ekey": "e7lE4W67gUoER1al86Fg8CsMVhVpIDQReuONmwVyiIfZQA+U0J5J67UBnABFc6YKIYpWYPgAyQ5U+wPal17zOtyA7EeO2H+bxui1ESfKrh0pOaViElbHS7WTD9fBcAiOacNxukGlpjK70KWtSEFt+35XejWRw09AUIn8KeYSwnV7wetu4Ba783VvHsOd0vyWace3+I+T3tr7hiAnAxaeKtaYdeoKWCAydj8AM1jK+3z+kIc1aVTwDXKEw/Cw03EyO5wKF/0pHYBCkCnTRhXFIULVR6EDQWJh/fW0Bc5YpT2YDP9KRzXP6HfAML2/k7YkwuMRXhR4p12h0RPFxvmTRg==",    "flush_time": 1542625604461,    "payloads": ["Tg7E5sMghLePA3yW/1X6xO+MAPnncKvn9wYGk/T912JMljW0bK0hxXL14ttPY26uc1bksBHAqFW5xRb3LUYX+kcuM/N7shaw1/4XJcghw2JhexICA3Lf3Vsv37UtS0o8hW0LNq7kkSZt9wOa1Xb3agwtL7vhENtreqFBM+k+5ZH7MjVK8GalQdDauR7cZ1dtcprkFJiXuKrotp0DSeTCCtYiABlS3mDdVc1/NjbvvavbV4p4FC+R5VA8aDWszKC02gMyF4pohPKpYgJsFSwCKbNPoPY8TEop3HS6UnXDI6hlTPmzspaDfJHXThLhb83mnqWnrtQih2HllGUUthA2fKZj+QgqoGj7EHhjsGgaXxV0I7Op6NEe5EA1nOejOK8ibo7s0c67lkyb7AFcBfRNgJzhSkuZy3lqGXl61d1KIo3UT4+iBnRAgxZf", "Tg7E5sMghLePA3yW/1X6xO+MAPnncKvn9wYGk/T912JMljW0bK0hxXL14ttPY26uc1bksBHAqFW5xRb3LUYX+kcuM/N7shaw1/4XJcghw2JhexICA3Lf3Vsv37UtS0o8hW0LNq7kkSZt9wOa1Xb3agwtL7vhENtreqFBM+k+5ZH7MjVK8GalQdDauR7cZ1dtcprkFJiXuKrotp0DSeTCCtYiABlS3mDdVc1/NjbvvavbV4p4FC+R5VA8aDWszKC02gMyF4pohPKpYgJsFSwCKbNPoPY8TEop3HS6UnXDI6hlTPmzspaDfJHXThLhb83mnqWnrtQih2HllGUUthA2fKZj+QgqoGj7EHhjsGgaXxV0I7Op6NEe5EA1nOejOK8ibo7s0c67lkyb7AFcBfRNgJzhSkuZy3lqGXl61d1KIo3UT4+iBnRAgxZf", "Tg7E5sMghLePA3yW/1X6xO+MAPnncKvn9wYGk/T912JMljW0bK0hxXL14ttPY26uc1bksBHAqFW5xRb3LUYX+kcuM/N7shaw1/4XJcghw2JhexICA3Lf3Vsv37UtS0o8hW0LNq7kkSZt9wOa1Xb3agwtL7vhENtreqFBM+k+5ZH7MjVK8GalQdDauR7cZ1dtcprkFJiXuKrotp0DSeTCCtYiABlS3mDdVc1/NjbvvavbV4p4FC+R5VA8aDWszKC02gMyF4pohPKpYgJsFSwCKbNPoPY8TEop3HS6UnXDI6hlTPmzspaDfJHXThLhb83mnqWnrtQih2HllGUUthA2fKZj+QgqoGj7EHhjsGgaXxV0I7Op6NEe5EA1nOejOK8ibo7s0c67lkyb7AFcBfRNgJzhSkuZy3lqGXl61d1KIo3UT4+iBnRAgxZf"]}]

为了提高加密的效率和减少密钥的长度,经过调研以及大量的测试,最终选择了椭圆曲线加密算法(Elliptic Curve Cryptography,缩写:ECC)。

ECC 相较于 RSA 的优势是:在同等级别的安全性下,ECC 的密钥长度会更小,加密速度也更快[2]。

因此,SDK 提供的另外一种混合加密策略就是 ECC + AES,具体也可以分为两部分:

1. 使用生成的 AES 密钥对采集的数据进行 AES 对称加密;

2. 使用 ECC 公钥对 AES 密钥进行加密。

目前此功能还在开发中,相信很快就可以投入使用。

3.2.4.2. 自签证书

SDK 中提供了 SASecurityPolicy 类(参考 AFNetworking 中的 AFSecurityPolicy 类实现,使用方法也类似[3]),主要是验证 HTTPS 证书是否正确。

下面我们看下证书校验的策略:

1. SASSLPinningModeNone:表示客户端无条件地信任服务端返回的证书;

2. SASSLPinningModeCertificate:表示客户端会将服务端返回的证书与本地保存证书中的 “所有内容” 进行校验;

3. SASSLPinningModePublicKey:表示客户端会将服务端返回的证书与本地保存证书中的 “PublicKey部分” 进行校验。

SDK 初始化后会默认创建一个 SASSLPinningModeNone 的 SASecurityPolicy 对象,可以使用如下代码支持自签证书:​​​​​​​

SASecurityPolicy *securityPolicy = [SensorsAnalyticsSDK sharedInstance].securityPolicy;  /* allowInvalidCertificates 是否需要验证自签证书,默认为 NO */securityPolicy.allowInvalidCertificates = YES;  /* validatesDomainName 是否需要验证域名,默认为 YES */securityPolicy.validatesDomainName = NO;  /* pinnedCertificates 存储本地证书 */securityPolicy.pinnedCertificates = [SASecurityPolicy certificatesInBundle:[NSBundle mainBundle]];

注意:iOS 只支持 DER 格式的证书,其他格式证书可以使用 OpenSSL 进行格式转换。

3.2.5. 数据发送流程

数据采集时,属性信息需要封装成神策需要的 json 格式,然后存储到数据库中。

当 SDK 触发一条数据时会检测当前是否处于 Debug 模式或者是否超过本地数据库最大缓存,如果达到上述条件会发送数据。

不满足时会进行如下判断:如果触发的事件为 “$SignUp” 、本地缓存的数据条数大于设置的 flushBulkSize 、距离上次发送时间间隔大于 flushBulkSize 等。满足任意一条都会发送数据,具体流程如图 3-1 所示:

图 3-1 数据发送流程图

数据发送时,以下情况不会发送数据:

  • serverURL 为空时不会发送数据;

  • 无网络时不会发送数据;

  • 不满足 SDK 设置的网络发送策略时不会发送数据。

满足发送条件时,SDK 会将本地的数据全部发送。如果一次性传输的数据较多,会增加发送失败的概率,同时对性能的影响也比较大。因此,SDK 一次最多读取 50 条数据,对读取的原始数据先采用 Gzip 压缩,然后对压缩的内容进行 Base64 编码,保证高效传输。

数据发送后,在本地标记该部分数据正在发送。然后根据网络请求状态码判断是否发送成功:状态码在 200 ~ 300 之间时,SDK 认为发送数据是成功的,会先标记本地发送的数据状态为成功发送,然后删除本地数据数据。发送失败时,本地数据不会删除,并把数据标记为未发送。

每次发送都会循环读取本地存储的数据,直到数据全部发送完成。

3.2.6. 数据发送校验

开发者在使用 SDK 的过程中,需要校验 SDK 采集的数据能否成功地发送到指定的服务端。因此,SDK 提供了查看日志以及使用动态调试模式等方式来校验数据是否成功发送。

3.2.6.1. 控制台日志校验

在 Xcode 控制台查看数据是否成功发送,需要在初始化时开启日志打印,SDK 在满足发送策略时会发送数据到指定的服务端。

控制台显示的日志具体分以下几种情况:

  • 埋点事件触发成功时,输出 “track event” 开头的事件数据;

  • 埋点事件触发失败时,输出相应的错误原因;

  • 事件数据发送成功时,输出 “valid message” 开头的事件数据;

  • 事件数据发送失败时,输出 “invalid message” 开头的事件数据并输出错误原因。

开发过程中,可以根据日志判断数据是否成功发送。

3.2.6.2. 动态调试模式

SDK 提供了动态调试发送数据的功能,方便开发者在使用 SDK 时校验数据。动态调试模式下 SDK 采集的数据会实时地发送到指定的服务端,同时提供了 DEBUG_ONLY 和 DEBUG_AND_TRACK 两种模式:

  • DEBUG_ONLY:实时发送数据但不会入库,避免在测试过程中产生的脏数据入库;

  • DEBUG_AND_TRACK:实时发送数据,同时也会入库。

那神策分析的服务端如何知道 SDK 开启了动态调试模式呢?答案是我们在发送请求时把 serverURL 中的 “sa” 替换成了 “debug”,这样服务端就会知道该数据是动态调试的数据。

那 DEBUG_ONLY 和 DEBUG_AND_TRACK 这两种模式,服务端又是如何区分的呢?答案是当我们在使用 DEBUG_ONLY 时会在请求头中添加属性 “Dry-Run”,用于区分 DEBUG_ONLY 和 DEBUG_AND_TRACK。

动态调试模式需要使用 Scheme,Scheme 正确配置后可以通过扫描 Debug 实时数据查看中的二维码拉起 App。具体的使用方法是:

1. 先使用调试设备扫描网页二维码,开启该设备的 “调试模式”;

2. 点击开始刷新后,操作 App 触发事件;

3. 事件上传成功就会在 Debug 实时数据查看中看到对应的事件。

四、总结

本文主要介绍了神策分析 iOS SDK 网络模块的具体实现。SDK 网络请求没有基于开源网络框架来实现,避免了一些潜在的风险。通过对系统类 NSURLSession 进行封装、采用完善的发送策略、以及对数据进行压缩、编码、校验等操作,实现了数据及时、准确、高效地发送。

最后,希望通过这篇文章,大家能够对神策分析 iOS SDK 的网络模块有一个系统的了解。

参考文献:

[1]https://developer.apple.com/videos/play/wwdc2013/204/

[2]https://wiki.openssl.org/index.php/Elliptic_Curve_Cryptography

[3]https://github.com/AFNetworking/AFNetworking

文章来源:公众号-神策技术社区

老板不让用 AFNetworking,我该怎么办?相关推荐

  1. 置顶带滚动效果_高端大气的滚动条图表,你学会了还怕老板们不喜欢,不升职吗?...

    今天,小默给大家带来一个简单又好玩的动态图表制作方法,而且还有一个滚动条控件,通过对滚动调的拖动查看图表的数据,效果非常棒!老板看了一目了然,说不定就在考虑你的升职加薪的事情了. 先来演示一下效果 拖 ...

  2. 对AFNetworking的简单封装

    #import "YGLoadDataManager.h" #import "AFNetworking.h" @implementation YGLoadDat ...

  3. 详解AFNetworking的HTTPS模块

    0.0 简述 文章内容包括: AFNetworking简介 ATS和HTTPS介绍 AF中的证书验证介绍 如何创建服务端和客户端自签名证书 如何创建简单的https服务器 对CA正式证书和自签名证书的 ...

  4. HTTPS网络加密双向验证-使用AFNetworking封装

    1.首先使用OC封装请求头 #import <Foundation/Foundation.h> #import "AFNetworking.h" @interface ...

  5. 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking

    该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...

  6. 老板来了:人脸识别 + 手机推送,老板来了你立刻知道!

    背景介绍 学生时代,老师站在窗外的阴影挥之不去.大家在玩手机,看漫画,看小说的时候,总是会找同桌帮忙看着班主任有没有来. 一转眼,曾经的翩翩少年毕业了,新的烦恼来了,在你刷知乎,看视频,玩手机的时候, ...

  7. 为什么有些老板要注册很多家公司

    1. 避税 避税的玩法非常多,最简单的就是开一大堆小规模纳税人做销售主体,每月收入低于10万免增值税,10万满了就换下一个公司开发票,达到节税的目的. 第二个避税玩法是互开发票或者转移成本.比如你有个 ...

  8. 使用 AFNetworking 进行 XML 和 JSON 数据请求

    (1)XML 数据请求 使用 AFNetworking 中的 AFHTTPRequestOperation 和 AFXMLParserResponseSerializer,另外结合第三方框架 XMLD ...

  9. 工程师文化:BAT 为什么不喊老板

    BAT员工之间不喊老板,也不喊真名,而是用同学.花名,这是虚情假意?还是弘扬武侠文化?还是另有隐情?为什么欧美公司不这么做?本文将带大家走进科学,探索真相. BAT 的称呼方式 腾讯:英文名,例如 P ...

最新文章

  1. 自学习 与 无监督特征学习
  2. Mongodb数据分片的实现
  3. 如何使用VAIO Care恢复和还原VAIO
  4. 12.oauth密码模式identity server4实现
  5. PHP基础语法的学习
  6. 寻找一条适合自己的写作晋级之路
  7. 阿里云数据库再添新成员,企业级MariaDB正式开卖!
  8. 【2013年04月18号】
  9. 我的编程学习日志(9)--交换A,B值得方法(相加,异或,swap函数)
  10. 简单免费内网穿透教程,外网快速访问内网群晖/nas/树莓派
  11. 汽车轮毂识别定位检测
  12. Codesys基础应用----ST语言实现经典冒泡排序
  13. Office ❀ PPT图形中的合并形状梳理
  14. wex5 checkbox
  15. Android 登录3D翻转动画效果
  16. 单片机中常用的串口通信协议帧
  17. windows下更改Apache以fastcgi方式运行php
  18. Charles联动Burp-新世界的大门
  19. Redis Desktop Manager连接失败/超时?
  20. sqlmap安装及简单使用实例

热门文章

  1. 做工精巧的腕上多面手,运动健康功能是亮点,Galaxy Watch4上手
  2. iPhone限制每天游戏时间,设置某些APP每天最长使用时间,未成年保护 - 《屏幕使用时间》密码设置
  3. 北漂职业生涯中心酸而又收获颇丰的一年
  4. 常见的JavaScript调试工具
  5. 零代码应用搭建规范建议
  6. CAD怎么查看图纸?查看CAD图纸有什么便捷的方法?
  7. 101-STM32+Air724UG基本控制篇(自建物联网平台)-基础搭建-购买云主机,安装MQTT服务器软件(.Windows系统)
  8. 散片便宜300元!但还是劝你买盒装CPU
  9. python可以制作网页吗_自己制作网页的网站(python制作网页)
  10. 博物馆智慧化建设创新实践