苹果多开框架_苹果暴露通知框架的旅程以及如何使用它

苹果多开框架

In early March the nonprofit association Novid20 was founded aiming to find and implement solutions to fight against COVID-19 infection spreading. With our initiative and strong support from our company Dolphin Technologies GmbH, a small team of us joined the Novid20 team to volunteer with our expertise in finding a fast, anonymous, and practical solution. Back then, there weren’t any talks about special OS or API support for contact tracing frameworks yet. We had to work with what we have and overcome the public API limitations on our own.

3月初,非营利组织Novid20成立,旨在寻找并实施解决方案来对抗COVID-19感染扩散。 在我们公司Dolphin Technologies GmbH的大力支持下,我们的一个小团队加入了Novid20团队,以我们的专业知识自愿寻找快速,匿名和实用的解决方案。 那时,还没有关于联系人跟踪框架对特殊OS或API支持的讨论。 我们必须使用已有的资源,并独自克服公共API的限制。

Our solution is based on an SDK hosted by authority apps that can determine social interactions and their duration using Bluetooth and GPS Location. Interactions that have a certain intensity in terms of time and proximity are stored locally by both apps in an encrypted form. If someone tests positive for a contagious disease like COVID-19, he is requested to report and upload his Bluetooth detected contacts anonymous identifiers and optionally his location for the last few days. People who may have had contact with the infected person within the last few days will receive a warning with instructions to move into quarantine and contact the local governmental authority. Aiming for transparency and benefiting the public, we open-sourced the SDK on GitHub. We already implemented and released the solution for the Georgian Health Ministry in their StopCovid app.

我们的解决方案基于由权威应用托管的SDK,该SDK可以使用蓝牙和GPS位置确定社交互动及其持续时间。 在时间和接近度方面具有一定强度的交互由两个应用程序以加密形式本地存储。 如果某人的传染性疾病(如COVID-19)测试呈阳性,则要求他报告并上传其蓝牙检测到的联系人匿名标识符以及最近几天的位置(可选)。 在过去几天内与感染者接触过的人将收到警告,并带有进入隔离区并与当地政府当局联系的指示。 为了提高透明度并使公众受益,我们在GitHub上开源了该SDK。 我们已经在其StopCovid应用程序中为乔治亚州卫生部实施并发布了该解决方案。

Image for post
StopCovid App 风格的StopCovid应用程序的屏幕截图

This solution faced a lot of limitations from Apple’s public APIs, such as: • Advertisement packet customization and size. • Broadcasting and scanning continuously in the background. • Calculate accurate contact details using passive scanning. • Crossplatform communications limitations.

该解决方案面临Apple的公共API的诸多限制,例如:•广告包的自定义和大小。 •在后台连续广播和扫描。 •使用被动扫描计算准确的联系人详细信息。 •跨平台通信限制。

We had to overcome those challenges using innovative workarounds to provide a practical solution. But with some of those limitations like continuous broadcasting and monitoring in the background, while detecting accurate contact durations, it was just not possible with Apple public APIs. So we had to deal with this limitation in a practical way that could be adapted by the user. Hence, we had to come up with the “3-sec auto-lock” feature. This is simply a dark dimmed lock screen that overlays the app after 20 seconds of user inactivty, prevents it from sleeping, and is not dismissed except after a continuous 3 seconds touch from the user. The dark dimmed screen helped with saving brightness energy while the app is not sleeping. And the 3 seconds touch was to prevent continuous unlocking of the screen with unintentional touches when the app is in the user’s pocket.

我们必须使用创新的解决方法来克服这些挑战,以提供切实可行的解决方案。 但是,由于存在一些局限性,例如在后台进行连续广播和监视,同时又检测到准确的联系时间,因此使用Apple公共API完全不可能。 因此,我们必须以一种可以由用户适应的实用方式来解决此限制。 因此,我们不得不提出“ 3秒自动锁定”功能。 这只是一个暗灰色的锁定屏幕,在用户不活动20秒后便会覆盖该应用程序,防止其进入Hibernate状态,除非用户连续触摸3秒,否则该屏幕不会消失。 暗暗的屏幕有助于在应用程序不睡眠时节省亮度能量。 3秒钟的触摸是为了防止当应用程序放在用户的口袋中时,屏幕会因意外触摸而连续解锁。

Image for post
The ring progress represents user’s 3 sec touch
响铃进度代表用户3秒钟的触摸

宣布改变游戏规则的公告! (The announcement that changed the game!)

On the 10th of April, Apple and Google announced merging forces in fighting against Covid-19. Their proposed solution to be provided in two phases. The first phase is a contact tracing framework that can be hosted in governmental, public health authorities, and NGOs apps, with only one app per country. In the second phase, the solution will be provided at the operating system level without requiring an app to be installed, aiming for wider technology adoption. In this article, we will only focus on the first phase, the framework.

4月10日,苹果和Google宣布合并,共同对抗Covid-19。 他们提出的解决方案将分两个阶段提供。 第一阶段是一个联系人跟踪框架,该框架可以托管在政府,公共卫生当局和NGOs应用程序中,每个国家只有一个应用程序。 在第二阶段,该解决方案将在操作系统级别提供,而无需安装应用程序,旨在广泛采用技术。 在本文中,我们将仅关注框架的第一阶段。

Alongside its announcement, Apple released a preliminary framework specification document that demonstrates how the framework will work. That news was a very promising step because now all our challenges and limitations will be handled on the OS level with even more features to add. For example, we stopped working on continuously randomizing the user’s advertised identifier to prevent tracking, as this feature is already provided under Apple’s framework.

在发布公告的同时,Apple还发布了初步的框架规范文档 ,该文档演示了框架将如何工作。 这个消息是非常有希望的一步,因为现在我们所有的挑战和限制都将在操作系统级别上进行处理,并添加更多功能。 例如,由于Apple框架已提供此功能,因此我们停止继续对用户的广告标识符进行连续随机化以防止跟踪。

On the 29th of April, Apple released the beta Exposure Notification Framework, and right away all concerned parties started to learn and adapt to the new framework.

4月29日,Apple发布了beta版“ 暴露通知框架” ,所有有关方面立即开始学习并适应新框架。

Now let’s go through the Exposure Notification Framework and how to use it.

现在,让我们看一下“ 曝光通知框架”以及如何使用它。

框架概述(Framework Overview:)

The concept of the Exposure Notification Framework depends on Bluetooth technology. It could be easily explained in three parts:

曝光通知框架的概念取决于蓝牙技术。 它可以分为三个部分轻松地解释:

A. Contact Tracing: The Framework continuously broadcasts a privacy-preserving key identifier that changes every 10–20 minutes.On the other hand, it also scans for other broadcasting identifiers. When a broadcasted identifier is detected, it will be recorded and securely store on the device.

A. 联系人 跟踪 :框架连续广播每10-20分钟更改一次的隐私保护密钥标识符。另一方面,它还会扫描其他广播标识符。 当检测到广播的标识符时,它将被记录并安全地存储在设备上。

B. Report Infection: When a user reports in the hosting app that he is infected, the app is expected to fetch from the framework a list of all the identifiers that have been recently used by that device, and upload it to the server.

B. 报告 感染 :当用户在主机应用程序中报告被感染时,该应用程序将从框架中获取该设备最近使用的所有标识符的列表,并将其上传到服务器。

C. Exposure Detection: Parallel to that, the hosting app is expected to periodically fetch from the server the list of key identifiers that were used and upload by infected users to the server. The framework then checks the device’s identifiers list against the downloaded list of infected identifiers. If there is a collision match, the user might be notified based on some configured parameters.

C. 暴露检测 :与此同时,托管应用程序应定期从服务器获取被感染用户使用并上传到服务器的密钥标识符列表。 然后,框架根据下载的受感染标识符列表检查设备的标识符列表。 如果存在冲突匹配,则可能会基于一些已配置的参数通知用户。

框架用法(Framework Usage:)

Perquisites: Xcode 11.5+, iOS 13.5+

必备条件:Xcode 11.5 +,iOS 13.5+

Starting with the limitations, the framework requires the special entitlement “com.developer.exposure-notification” to work. And as announced by Apple, that this entitlement will be available for governments and public health authorities only. So far this limitation is even required in the debug environment as well. As the framework is still in beta, it is unclear if this will continue to be the case.

从限制开始,该框架要求使用特殊权利“ com.developer.exposure-notification”。 正如苹果公司宣布的那样,该权利仅适用于政府和公共卫生当局。 到目前为止,在调试环境中甚至也需要此限制。 由于该框架仍处于测试阶段,因此尚不清楚是否会继续如此。

Image for post
Apple Docs 苹果文件

So far without an entitlement, your ways to get around it are either using a jailbroken device or another great workaround which is a mock framework done by Quentin Zervaas called TracePrivately to simulate Apple’s access limited framework. Thanks to his efforts, initial testings are possible without extra hacks. Of course, the mock framework is based on assumptions and is not doing the actual exposure scoring that is done by Apple’s framework, but sufficient for testing.

到目前为止,没有授权,您可以使用越狱设备或另一种很好的解决方法来解决它,该方法是由Quentin Zervaas完成的名为TracePrivately的模拟框架,用于模拟Apple的访问受限框架。 由于他的努力,可以进行初步测试而无需额外的黑客。 当然,模拟框架是基于假设的,并不像苹果框架那样进行实际的曝光评分,但足以进行测试。

As explained earlier in the overview section, first thing the host app is expected to do is to start advertising its identifiers and monitoring the surrounding identifiers in its medium.

如概述部分前面所述,主机应用程序要做的第一件事是开始发布其标识符并监视其介质中的周围标识符。

因此,这是第一部分。 联系追踪! (又名曝光通知) (Hence, comes the first part. Contact Tracing! (aka. Exposure Notification))

A. Contact Tracing: To start the framework’s contact tracing feature, the host app is expected to call the following API:

A.联系人 跟踪 :要启动框架的联系人跟踪功能,主机应用程序应调用以下API:

func setExposureNotificationEnabled(_ enabled: Bool, 
completionHandler: @escaping ENErrorHandler)

Calling this API will handle:

调用此API将处理:

  1. Continuously broadcasting and scanning for random identifiers that aren’t tied to a user’s identity and that change every 10–20 minutes for protection. Check this document for more details about the rolling key identifiers.

    连续广播和扫描与用户身份无关的随机标识符,该标识符每10–20分钟更改一次,以提供保护。 检查此文档以获取有关滚动键标识符的更多详细信息。

  2. Securely present scanned identifiers and the contact essential details.

    安全地提供扫描的标识符和联系方式的基本信息。

To call this API, the code will look something like:

要调用此API,代码将类似于:

Contact Tracing Snippet
接触追踪片段
  1. Initialize an instance of ENManager. This will be one of our two interfaces to the framework. The second interface is ENExposureDetectionSession and will be explained in the third part.

    初始化ENManager的实例。 这将是我们与框架的两个接口之一。 第二个接口是ENExposureDetectionSession ,将在第三部分中进行说明。

  2. Activate the instance using activate(completionHandler: @escaping ENErrorHandler() API. This method must be first called and the handler must complete successfully before using the ENManager instance. Only then the features of the class can be used.

    使用Activate activate(completionHandler: @escaping ENErrorHandler () API激活实例。必须首先调用此方法,并且必须先成功完成处理程序,然后才能使用ENManager实例。仅可使用该类的功能。

  3. Check for activation errors before using the ENManager instance.

    在使用ENManager实例之前,请检查激活错误。

  4. Now the ENManager instance is successfully activated, we request enabling the contact tracing feature using setExposureNotificationEnabled(_ enabled: Bool, completionHandler: @escaping ENErrorHandler) API. Calling this API triggers a system alert for authorization.

    现在成功激活了ENManager实例,我们请求使用setExposureNotificationEnabled(_ enabled: Bool, completionHandler: @escaping ENErrorHandler) API setExposureNotificationEnabled(_ enabled: Bool, completionHandler: @escaping ENErrorHandler)联系人跟踪功能。 调用此API会触发系统警告以进行授权。

  5. Check for exposure notification enabling errors.

    检查曝光通知启用错误。
  6. In this case, the app could decide to terminate the activated ENManager instance using invalidate() API until errors are resolved. This API is also called when the app is done with its ENManager instance. To use the ENManager again, create and activate a new instance of the class.

    在这种情况下,应用程序可以决定使用invalidate () API终止已激活的ENManager实例,直到解决错误为止。 当应用程序使用其ENManager实例完成时,也会调用此API。 要再次使用ENManager ,请创建并激活该类的新实例。

  7. If no exposure notification enabling errors, then exposure notification is successfully enabled.

    如果没有启用曝光通知的错误,则成功启用曝光通知。
Image for post
Screenshot of enabling exposure detection system alert.
启用曝光检测系统警报的屏幕快照。

现在是框架的第二部分。 报告感染! (Now comes the second part of the framework. Reporting Infection!)

B. Report Infection: When the user reports that he is COVID-19 positive in the host app, the app should request fetching a list of all the identifiers that have been recently used by that device, and upload it to its server. Fetching the list of device identifiers is using the API:

B. 报告 感染 :当用户在主机应用程序中报告其COVID-19阳性时,该应用程序应请求获取该设备最近使用的所有标识符的列表,并将其上传到其服务器。 使用API​​获取设备标识符列表:

func getDiagnosisKeys(completionHandler: @escaping ENGetDiagnosisKeysHandler)

This instance method as all other ENManager methods needs an initialized and active instance of ENManager to be called on. In the following snippet, we will assume that we are using the same initialized and active enManager instance created in Contact Tracing code snippet.

这个实例的方法的所有其他ENManager方法需要的初始化和活动实例ENManager被称为上。 在以下代码段中,我们假设我们使用在“联系跟踪”代码段中创建的相同的初始化且活动的enManager实例。

To call this API, the code will look something like:

要调用此API,代码将类似于:

  1. Request fetching the list of most recent key identifiers used by the infected user’s device. On completion, ENGetDiagnosisKeysHandler is passed, containing potential Error? and [ENTemporaryExposureKey]? are retuned. Each time this API is called, a system alert is triggered for the user’s permission.

    请求获取受感染用户设备使用的最新密钥标识符的列表。 完成后,将传递ENGetDiagnosisKeysHandler ,其中包含潜在的Error ?[ ENTemporaryExposureKey ]? 重新调整。 每次调用此API时,都会触发系统警报以征得用户的许可。

  2. Check for potential errors.

    检查潜在的错误。
  3. After checking that a list of key identifiers is passed, send this list to your server to be stored. This list should be then fetched by other users to processed on their device to check potential exposures with infected users.

    检查是否已通过密钥标识符列表之后,将该列表发送到您的服务器进行存储。 然后,其他用户应获取此列表,以在其设备上进行处理,以检查感染用户的潜在风险。
Guilherme Rambo of system alert when requesting DignosesKeys Guilherme Rambo的屏幕截图

Apple expects that the host app will somehow handle verifying the reported infections with its corresponding authorities as that the mechanism of official verification would vary from one public health authority to another.

苹果公司希望主机应用程序将以某种方式与相应的主管部门核实所报告的感染情况,因为官方认证的机制会因一个公共卫生机构而异。

最后但并非最不重要的是, 曝光检测(Last but not least, Exposure Detection!)

C. Exposure Detection: For the framework to check for any exposure possibilities, the host will have to create an ENExposureDetectionSession. ENExposureDetectionSession is the framework’s interface for this feature. The app must first download the list of infected key identifiers that are uploaded by other infected users. As that is out of scope, we will assume that we already have this list of infected key identifiers infectedKeyIdentifiers.

C. 暴露检测:为了使框架检查是否存在暴露可能性,主机将必须创建一个ENExposureDetectionSessionENExposureDetectionSession是此功能的框架接口。 该应用必须首先下载由其他受感染用户上传的受感染密钥标识符列表。 因为这是范围了,我们将假设我们已经有感染的关键标识符的这个列表infectedKeyIdentifiers

As that this list could grow large, the framework expects to be passed this list in form of batches, using API:

由于此列表可能会变大,因此该框架希望使用API​​以批处理形式传递此列表:

func addDiagnosisKeys(_ keys: [ENTemporaryExposureKey], completionHandler: @escaping ENErrorHandler)

The batch sizes shouldn’t be greater than the maximumKeyCount defined under ENExposureDetectionSession. The second batch shouldn’t be passed except when the first completes successfully. Keep repeating this step until all keys are passed to the framework or an error occurs. When all keys are passed, the host app is expected to call:

批处理大小不应大于ENExposureDetectionSession下定义的ENExposureDetectionSession 。 除非第一批成功完成,否则不应通过第二批。 继续重复此步骤,直到所有键都传递到框架或发生错误。 传递所有密钥后,主机应用程序应调用:

func finishedDiagnosisKeys(completionHandler: @escaping ENExposureDetectionFinishCompletion)

On successful completion, a potential ENExposureDetectionSummary? object is passed in the ENExposureDetectionFinishCompletion handler. This object will contain a summarised overview of any exposures detected. For more detailed exposure info, use:

成功完成后,可能会出现ENExposureDetectionSummary? 对象在ENExposureDetectionFinishCompletion处理程序中传递。 该对象将包含检测到的所有暴露的摘要。 有关更多详细的曝光信息,请使用:

func getExposureInfo(withMaximumCount maximumCount: Int, completionHandler: @escaping ENGetExposureInfoCompletion)

Where maximumCount is the maximum number of detected exposures the app is ready to handle in a single call. On completion, ENGetExposureInfoCompletion is passed containing a list of ENExposureInfo and a boolean done. That boolean represents if there are more exposures to be fetched or not. To fetch all exposures, repeat this step till the done value is true or an error occurs.

其中, maximumCount是应用程序准备在单个调用中处理的检测到的最大曝光数。 完成后, ENGetExposureInfoCompletion传递包含列表ENExposureInfo和一个布尔值done 。 该布尔值表示是否要提取更多的风险。 要获取所有曝光,请重复此步骤,直到done值为true或发生错误。

To achieve this, the code will look something like:

为此,代码将类似于:

  1. Initialize an instance of ENExposureDetectionSession.

    初始化ENExposureDetectionSession的实例。

  2. Same asENManager instance, ENExposureDetectionSession must first be activated and completed successfully in order for its services to be used. Activate the instance using activate(completionHandler: @escaping ENErrorHandler() API.

    ENManager实例相同,必须先成功激活并成功完成ENExposureDetectionSessio n才能使用其服务。 使用activate(completionHandler: @escaping ENErrorHandler () API activate(completionHandler: @escaping ENErrorHandler ()激活实例。

  3. Check for activation errors before using the ENExposureDetectionSession.

    在使用ENExposureDetectionSessio n之前,请检查激活错误。

  4. Use a recursive method taken and modified from this gist, to recursively batch and pass the infectedKeyIdentifiers list that is fetched earlier from the server.

    使用从该摘要中获取并修改的递归方法,以递归方式批处理并传递先前从服务器获取的infectedKeyIdentifiers列表。

  5. Pass a batch of infectedKeyIdentifiers that is not greater than the maximumKeyCount defined under ENExposureDetectionSession.

    传递一批不大于maximumKeyCount下定义的maximumKeyCountinfectedKeyIdentifiers ENExposureDetectionSession

  6. Recursively repeat the process until all infectedKeyIdentifiers are passed.

    递归地重复该过程,直到所有infectedKeyIdentifiers的KeyIdentifiers通过。

  7. Check for error while batching the keys. Same as with the ENManager, you can decide to terminate the activated session using invalidate(). You also use this method when the app is done with its session.

    批处理密钥时检查错误。 与ENManager ,您可以使用invalidate ()决定终止激活的会话。 当应用完成会话后,您也可以使用此方法。

  8. When all keys are added and batchPositiveDiagnosisKeys() completes successfully, call finishedDiagnosisKeys() API. This will process all passed infectedKeyIdentifiers and returns ENExposureDetectionFinishCompletion on completion.

    添加所有密钥并且batchPositiveDiagnosisKeys()成功finishedDiagnosisKeys() ,请调用batchPositiveDiagnosisKeys() finishedDiagnosisKeys() API。 这将处理所有通过的infectedKeyIdentifiers ENExposureDetectionFinishCompletion ,并在完成时返回ENExposureDetectionFinishCompletion

  9. Check for any matches with infectedKeyIdentifiers, expressing exposure with other infected users.

    检查是否与infectedKeyIdentifiers匹配,以表明其他被感染的用户可以接触到它。

  10. In case of exposure, the app could request more details about those exposures using getExposureInfo(withMaximumCount maximumCount: Int, completionHandler: @escaping ENGetExposureInfoCompletion). Pass the maximum number of exposure infos the app could handle in a single call. Per the documentation, Apple recommends using a reasonable number as 100.

    如果发生暴露,则应用程序可以使用getExposureInfo(withMaximumCount maximumCount: Int, completionHandler: @escaping ENGetExposureInfoCompletion)请求有关这些暴露的更多详细信息。 传递一次应用可以处理的最大曝光信息数量。 根据文档,Apple建议使用合理的数字100。

  11. After checking for possible errors, now exposuresList contains a batch of maximum 100 exposure infos.

    在检查了可能的错误之后,现在exposuresList包含一批最多100个曝光信息。

  12. Optional fetch more exposure infos, till the done flag is true. A recursive technique like in batchPositiveDiagnosisKeys() could be used.

    可选获取更多曝光信息,直到done标志为true为止。 可以使用类似batchPositiveDiagnosisKeys()递归技术。

  13. Call invalidate() when the app is done with the session.

    应用程序完成会话后,请调用invalidate ()

To prevent abuse, the framework enforces a limit on the rate that your app can use exposure detection session operations.

为防止滥用,该框架对您的应用可以使用暴露检测会话操作的速率实施限制。

The host app can always configure the criteria to estimate risk using ENExposureConfiguration. More illustrations found in the sample app documentation.

主机应用程序始终可以使用ENExposureConfiguration配置标准以估计风险。 在示例应用程序文档中可以找到更多插图。

Image for post
Apple Docs 苹果文件

The host app is expected to periodically check for exposure in a nonabusive mater. The best way of doing that is BGTaskScheduler. Because as per sample app documentation, when declaring a background task in the app’s Info.plist, the app should declare the task identifier ending with exposure-notification. The BackgroundTask framework detects apps that contain this special Exposure Notification entitlement prefix and the operating system will guarantee to launch these apps automatically when they aren’t running and allow them more background time to ensure that the app can process and report results promptly.

主机应用程序应定期检查非滥用材料中的暴露情况。 最好的方法是BGTaskScheduler 。 因为根据示例应用程序文档 ,当在应用程序的Info.plist中声明后台任务时,应用程序应声明任务标识符,并以exposure-notification结尾。 BackgroundTask框架检测到包含此特殊的Exposure Notification权利前缀的应用程序,并且操作系统将保证在这些应用程序不运行时自动启动它们,并为它们提供更多的背景时间以确保该应用程序可以及时处理和报告结果。

The documentation sample app has a great example of doing that in a more optimized way using DispatchGroup.

该文档样本应用程序具有使用DispatchGroup以更优化的方式执行此操作的绝佳示例。

苹果在其示例应用程序中提到了进一步的准则。 (Further Guidelines mention by apple in their sample app.)

• Inform users that they can choose to enable Exposure Notifications, as well as that they can disable them at any time.

•通知用户他们可以选择启用“暴露通知”,也可以随时禁用它们。

• Explain that this only works with other devices that have Exposure Notifications turned on.

•说明这仅适用于已打开“曝光通知”的其他设备。

• Explain the use of Random IDs, including what they are, how they’re shared and collected, and the privacy protections associated with them.

•解释随机ID的用法,包括它们的含义,如何共享和收集它们以及与之相关的隐私保护。

• Explain how a user can be notified of an exposure event (including time), as well as the fact that exposure details (date, duration, and signal strength) will be shared with you.

•说明如何向用户通知曝光事件(包括时间),以及与您共享曝光详细信息(日期,持续时间和信号强度)的事实。

• Include an explanation of how a user may work with you to submit a positive diagnosis, with their consent.

•包括用户在您的同意下如何与您合作提交阳性诊断的说明。

Now we have a full overview about the Exposure Notification Framework.

现在,我们对曝光通知框架有了完整的概述。

Special thanks to community members like Quentin Zervaas for contributing with such efforts in a short while to provide his mock framework solution for the his community peers. I hope that article helped you in understanding the Exposure Notification Framework better. Thank you.

特别感谢Quentin Zervaas这样的社区成员在短时间内做出了这样的努力,为社区中的同龄人提供了他的模拟框架解决方案。 我希望这篇文章能帮助您更好地了解“曝光通知框架”。 谢谢。

You can always reach me on:• TwitterLinkedInEmail

您可以随时通过以下方式与我联系:• TwitterLinkedIn电子邮件

References:• Apple DocsTracePrivatelySnippetFramework Sample App

参考:• Apple文档TracePrivately摘录框架示例应用

Image for post
Dolphin Technologies GmbH 海豚技术有限公司

翻译自: https://medium.com/macoclock/a-journey-towards-apples-exposure-notification-framework-and-how-to-use-it-2a7a2c4d7fc4

苹果多开框架

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值