After the public launch of iOS 15, we started receiving feedback from clients that they were inexplicably logged off to the login page repeatedly when opening our application (Cookpad). Surprisingly, this is not a problem we found when we tested the beta version of iOS 15.

If you’re looking for a fix, scroll straight down to the conclusion, but if you want to learn more about how we debugged this particular problem, get started.

Reproduce the feedback problem

The details in user reports are limited, but the only thing we do know is that starting with iOS 15, users will open the app and find themselves logged out.

We had no video and no concrete steps to recreate the problem, so I tried to launch the app in various ways in hopes of seeing it for myself. I tried to reinstall the app, I tried to launch with and without an Internet connection, I tried to force out, and after 30 minutes of trying, I gave up and started replying that I couldn’t find the problem.

It wasn’t until I unlocked the phone again and started the Cookpad without doing anything, that I found the APP had just logged out of the login screen, as our users had reported!

I couldn’t exactly reproduce the problem after that, but it seemed to be related to taking a break from using the phone for a while and then using it again.

Narrow down the problem

I’m concerned that reinstalling the application from Xcode might affect the recurrence of the problem, so before doing so, it’s time to look at the code and try to narrow down the problem. Based on our implementation, I came up with three potential reasons.

  • 1.UserDefaultsThe data in the.
  • 2. An unexpected API call returns HTTP 401 and triggers logging out.
  • 3,KeychainAn error was thrown.

I was able to rule out the first two potential causes, thanks to some subtle behaviors I observed after recreating the problem myself.

  • The login screen didn’t ask me to select a region — that suggestsUserDefaultsThere is no problem with the data in, because our “shown locale selection” preference is still in effect.
  • The main user interface is not shown, even briefly — indicating that no network request has been attempted, so it may be premature for the API to be the cause of the problem.

That leaves us with the Keychain, which leads me to the next question. What changed and why is it so hard to replicate?

What changed and why is it so hard to replicate?

After a cursory look at the release notes and a quick Google search, I couldn’t find anything, so I had to keep digging to get a better understanding of the issue.

Access to Keychain data is provided through the Security framework, which is notoriously tricky. While there are a number of third-party libraries that wrap the framework to make things easier, we maintain our own simple wrapper based on some of Apple’s sample code.

Looking at this code, we call the SecItemCopyMatching method to load our access token, which returns the data along with the OSStatus code that describes the result. Unfortunately, however, while our wrapper throws unsuccessful results along with the status code for debugging, we discard this information in the next layer and simply treat errors as nil.

We implemented a weekly release schedule, thanks to a lot of automation. At this point, our next release deadline (code freeze) is the next day. Since we don’t fully understand how common this problem is, and we’re not sure if we’ll be able to issue a fix before the code freezes, I took the opportunity to address the lack of observability by adding some additional non-lethal logs using Crashlytics(crash logging tool).

This result gives us some good observation points that we can then look at over the next few weeks.

At this point, I was able to capture the exact error code returned. The culprit is errSecInteractionNotAllowed:

Interaction with the Security Server is not allowed.

This error tells us that we are trying to read data from the Keychain at a point in time when data is not available. This usually happens to you try to read the stored data, and set its accessibility to kSecAttrAccessibleWhenUnlocked, the device is locked.

Now this makes perfect sense, but the only problem is that in Cookpad we only read information from the Keychain when the app is launched, and my assumption is that the user must have clicked the app icon to launch the app, so the device should always be unlocked at that point, right?

So what has changed? Even if I could recreate the problem, I’m 100% sure my phone unlocked when I clicked the app icon, so I don’t understand why the Keychain error occurred.

Determined to find out why, I replaced our application’s implementation with a debugging tool that would try and log Keychain reads at different nodes in its life cycle.

In the scenario where the problem was replicated, I observed the following results:

  • main.swift– failure (errSecInteractionNotAllowed)
  • AppDelegate.init()– failure (errSecInteractionNotAllowed)
  • AppDelegate.applicationProtectedDataDidBecomeAvailable(_:)

Success –

  • AppDelegate.application(_:didFinishLaunchingWithOptions:)Success –
  • ViewController.viewDidAppear(_:)Success –

So that explains it (half). To avoid holding some implicitly unpacked optional properties on our AppDelegate, we make some Settings in the init() method, part of which involves reading the access token from the Keychain. This is why reads fail, and eventually why some users find themselves logged out.

I learned an important lesson here, that I shouldn’t assume protected data is available when the AppDelegate initializes, but to be honest, I’m still not happy because I don’t understand why it’s not available. After all, we haven’t changed this part of the code in years, and it works fine on iOS 12, 13, and 14, so why?

Look for root causes

My debugging interface was useful, but it was missing something important to help answer all the questions: time.

I know the AppDelegate. Application (_ : before didFinishLaunchingWithOptions:), “protected data” is not available, but it still doesn’t make sense, because in order to reproduce this problem, I am doing the following:

3. Forcibly Exit the app. 4. Lock my device and leave it for 30 minutes

Every time I start the application again in Step 6, I’m 100% sure the device is unlocked, so I’m confident THAT I should be able to read from the Keychain in Appdelegate.init ().

It wasn’t until I looked at all these steps that things started to make a little bit of sense.

Take a closer look at the timestamp again:

  • main.swift– 11:38:47
  • AppDelegate.init()– 11:38:47
  • AppDelegate.application(_:didFinishLaunchingWithOptions:)– 12:03:04
  • ViewController.viewDidAppear(_:)– 12:03:04

The app itself launched 25 minutes before I actually unlocked the phone and clicked the app icon!

Now, I actually never thought there was such a big delay, it was actually @_Saagarjha who suggested I check the timestamp, and after that, he pointed me to this tweet.

Interesting iOS 15 optimizations. Duet is now trying to pre-empt third-party applications by running them through dyLD and pre-primary static initializers a few minutes before you click on an app icon. The application is then paused and the subsequent “boot” appears to be faster.

It all makes sense now. We didn’t test it initially because we probably didn’t give the iOS 15 beta enough time to “learn” our usage habits, so the problem only reappeared in a real-world scenario where the device thought I was going to launch the app soon. I still don’t know how this prediction comes about, but I’ll just chalk it up to “Siri intelligence “and leave it at that.

conclusion

Starting with iOS 15, the system may decide to “warm up” your app before the user actually tries to open it, which can increase the probability that protected data will be accessed when you think it should be unusable.

By waiting for the application (_ : didFinishLaunchingWithOptions:) entrust a callback to protect themselves, if possible, Refer to the UIApplication. IsProtectedDataAvailable (or entrusted by the corresponding callback/notification) and corresponding processing.

We still found very fewer non-fatal problem, in the application (_ : didFinishLaunchingWithOptions isProtectedDataAvailable:) in the report is false, from key string reading we can postpone the access token, This would be a large-scale mission and it is not worth investigating further at this time.

It’s a fairly difficult bug to debug, and changes in behavior seem to go completely unrecorded, which really doesn’t help me. If you are also troubled by this problem, please consider copying FB9780579.

I learned a lot from it, and I hope you do too!

Update: Since publishing this post, many people have actually pointed me out to Apple’s relatively good documentation on warm-up behavior. However, others have also told me that they still observe behaviors that differ from those recorded in some scenarios, so proceed with caution.

Solving Mysterious Logout Issues on iOS 15

About us

Swift community is a public welfare organization jointly maintained by Swift enthusiasts. We mainly operate wechat public accounts in China. We will share technical content based on Swift practice, SwiftUl and Swift foundation, and also collect excellent learning materials.

Welcome to pay attention to the public number :Swift community, backstage click into the group, you can enter our community’s various exchanges and discussion groups. I hope our Swift community is another common place to belong in cyberspace.

Special thanks to every editor in Swift Community editorial department for their hard work, providing quality content to Swift community and contributing to the development of Swift language, in no particular order:

  • Yu Zhang @Microsoft
  • NiYao @ Trip.com
  • Daiming @ Kuaishou
  • Show @ ESP
  • Du Xinyao @sina
  • Wei string @ Gwell