I get w weird crash related to observing AVPlayer.timeControlStatus in iOS 13. It's not happening in iOS 12.
Here's the code for setting the observer up:
// stored in View Controller
private var playerStateObservation: NSKeyValueObservation?
#objc var player : AVPlayer?
// setting KVO after initialising AVPLayer
playerStateObservation = observe(\.player?.timeControlStatus) { [weak self] (object, change) in
let playing = self?.player?.timeControlStatus == .playing
self?.showPlayIcon(playing)
}
And here the function to stop observation. It's called in ViewController deinit.
func cleanUpObserver() {
playerStateObservation?.invalidate()
playerStateObservation = nil
}
The crash occurs in following situation:
Open ViewController with AVPlayer and start observation.
Go back to previous ViewController.
Dismiss the app to the background.
Bring back the app to the foreground.
Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x2b1bc593c)
Here's the callstack of the crash.
Looks like the AVPlayer is trying to send notification to a observer that should already be invalidated and released from memory. Did anyone have a similar issue?
viewWillDisappear will be better place to remove observations dude.
Related
I have an app, that is for many years in the AppStore and runs without any crashes (last deployment target iOS 12.4). I have some code for playing a sound on certain events in the app.
Now I tried to upgrade my app for iOS 13 and without changing any code related to that “playSound” thing, I always get this runtime error, when testing on a real device. Does not happen on simulator.
Thread 1: EXC_BAD_ACCESS (code=1, address=0x48)
PLEASE: Before you mark that question as “duplicate”, consider that this must have something to do with the release of iOS13, because before it didn’t happen and the code is just "usual".
Here is my code, also on gitHub.
I have a property in my ViewController to prevent ARC deallocating my AVAudioPlayer:
private var mySoundPlayer: AVAudioPlayer = AVAudioPlayer()
I have a routine, where the “play sound” should be performed (here happens the error, when assigning a new instance of AVAudioPlayer. The resourceURL is not the problem, the RE-ASSIGNING is the problem, I tested it with a new instance not being assigned and i did not crash.
// -------------------------------------------------
// MARK: Private Methods
// -------------------------------------------------
private func makeSoundEvent(_ soundEvent : SoundEvent) {
guard Settings().getSound() == .soundON else { return }
guard let urlToRessource : URL = soundEvent.getFileURLToSoundRessource() else { return }
do {
mySoundPlayer = try AVAudioPlayer(contentsOf: urlToRessource)
try? AVAudioSession.sharedInstance().setActive(true)
mySoundPlayer.play()
}
catch { print("Could not create AVPlayer for ressource \(urlToRessource)") }
}
And here I have the call of that subroutine, somewhere in my viewDidLoad() with a delay of 2 seconds.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.makeSoundEvent(.startFanfare)
}
I think it somehow does not work because of the async thread. But since it is on the main thread, I thought this should work.
Just remove the initialisation and it will work
private var mySoundPlayer: AVAudioPlayer!
Cheers!!!
private var mySoundPlayer: AVAudioPlayer = AVAudioPlayer()
AVAudioPlayer doesn't have init() listed as a valid initializer. The reference page shows four separate initializers, all of which take at least one parameter. If you were using the line above in code prior to iOS 13.1 without crashing, you were initializing the audio player incorrectly and probably just getting lucky that it wasn't a problem.
I don't know what changed specifically with AVAudioPlayer in iOS 13.1, but the release notes show quite a few audio-related changes, and it seems likely that some change to that class introduced a dependency on something that happens at initialization time.
I get random crashes (which I can't reproduce on devices I own) in my app with exception:
Cannot remove an observer Foundation.NSKeyValueObservation 0xaddress for the key path "readyForDisplay" from AVPlayerLayer 0xaddress because it is not registered as an observer.
This happens when I deallocate a UIView which contains AVPlayerLayer.
My init:
private var playerLayer : AVPlayerLayer { return self.layer as! AVPlayerLayer }
init(withURL url : URL) {
...
self.asset = AVURLAsset(url: url)
self.playerItem = AVPlayerItem(asset: self.asset)
self.avPlayer = AVPlayer(playerItem: self.playerItem)
super.init(frame: .zero)
...
let avPlayerLayerIsReadyForDisplayObs = self.playerLayer.observe(\AVPlayerLayer.isReadyForDisplay, options: [.new]) { [weak self] (plLayer, change) in ... }
self.kvoPlayerObservers = [..., avPlayerLayerIsReadyForDisplayObs, ...]
...
}
My deinit where exception is thrown:
deinit {
self.kvoPlayerObservers.forEach { $0.invalidate() }
...
NotificationCenter.default.removeObserver(self)
}
According to Crashlytics it happens on iOS 11.4.1 on different iPhones.
The code leading to deinit is pretty simple:
// Some UIViewController context.
self.viewWithAVLayer?.removeFromSuperview()
self.viewWithAVLayer = nil
I would appreciate any thoughts on why this happens.
I have seen this bug but it doesn't seem to be the cause for me.
EDIT 1:
Additional info for posterity. On iOS 10 if I don't invalidate I get reproducible crash on deinit. On iOS 11 it works without invalidation (not checked yet if crash disappears if I don't invalidate and let observers to be deinited with my class).
EDIT 2:
Additional info for posterity: I have also found this Swift bug which might be related - SR-6795.
After
self.kvoPlayerObservers.forEach { $0.invalidate() }
Add
self.kvoPlayerObservers.removeAll()
Also I don’t like this line:
self.kvoPlayerObservers = [..., avPlayerLayerIsReadyForDisplayObs, ...]
kvoPlayerObservers should be a Set and you should insert observers one by one as you receive them.
I have accepted matt's answer but I want to provide more info on how I actually tackled my problem.
My deinit that doesn't crash looks like this:
if let exception = tryBlock({ // tryBlock is Obj-C exception catcher.
self.kvoPlayerObservers.forEach { $0.invalidate() };
self.kvoPlayerObservers.removeAll()
}) {
remoteLoggingSolution.write(exception.description)
}
... // do other unrelated stuff
Basically I try to catch Obj-C exception if it occurs and try to log it remotely.
I have this code in production for past 2 weeks and since then I didn't receive neither crashes nor exception logs so I assume matt's proposal to add kvoPlayerObservers.removeAll() was correct (at least for my particular case).
I have a watchOS 4 app which displays SpriteKit animations (SKActions) on top of the UI. Everything works fine in simulator and also on device first couple of times, then after some time when app is in background, and it is started, animations just freeze and completion block for the most long-lasting animation is not called. Any idea what might be the issue?
This is how I run my actions, caller is waiting for completion closure in order to hide the spritekit scene:
private func runActions(with icon: SKShapeNode?, completion: #escaping () -> Void) {
if let icon = icon, let scaleAction = scaleAction, let bg = background {
self.label?.run(fadeInOutAction)
icon.run(scaleAction)
icon.run(fadeInOutAction)
bg.run(backgroundAction, completion: completion)
} else {
completion()
}
}
And yes, I am aware that SKScene is paused when app moves to background. I am doing this in willActivate of my InterfaceController:
if scene.scene?.isPaused == true {
scene.scene?.isPaused = false
}
I want to emphasize that this works first always. It begins to fail after the app has been backgrounded for some time. Especially if I start the app from complication and try to immediately fire these animations, then this freezing happens.
Can I answer my own question? I guess I can? Here goes:
I finally solved this. It turns out that the WKInterfaceScene in WatchKit has ALSO an isPaused property that you need to turn false sometimes. So now in willActivate of my InterfaceController I will also check that and turn it false if it is true. Since I made this change, I haven't seen a single hiccup, freeze or anything weird anymore.
Case closed, I guess. I leave this here for future generations who might face this issue.
Hey so the title gives some information, but let me expand further. In my iOS application, I am receiving this EXC_BAD_ACCESS message when I am joining and leaving chat rooms quickly (by selecting a thread in a UITableView and view is getting pushed to a new UIViewController and then popping back to the UITableViewController). I am trying to fix this problem because it is more prevalent with slower network connections, and I want to avoid crashes. I use the Quickblox Chat service based on the ChatService.h/.m files that are in the sample-chat xcode project. Here is the only modified code:
- (void)chatRoomDidEnter:(QBChatRoom *)room{
if (self.joinRoomCompletionBlock) {
self.joinRoomCompletionBlock(room);
self.joinRoomCompletionBlock = nil;
}
[[NSNotificationCenter defaultCenter] postNotificationName:#"kNotificationChatRoomDidEnter"
object:nil userInfo:nil];
}
The crash happens at the end of this method and when I turned on NSZombies, I got this error message:
*** -[QBChatRoom retain]: message sent to deallocated instance 0x15e6bbe0
I am actually calling these methods from that ChatService in my UIViewController's viewWillAppear/Disappear methods (written in Swift):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.chatRoom = self.dialog.chatRoom
ChatService.instance().joinRoom(self.chatRoom, completionBlock: { (joinedChatRoom : QBChatRoom!) -> Void in
//joined
})
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.removeObserver(self);
if (self.chatRoom != nil) {
ChatService.instance().leaveRoom(self.chatRoom);
self.chatRoom = nil
}
}
Thanks in advance for any insight with this issue.
I have no experience with Swift yet, but I would guess that
ChatService.instance().joinRoom(self.chatRoom, completionBlock:{...})
is asynchronous (thus the completion block) and is not retaining self.chatRoom.
When things are quick as you describe, viewWillDisappear is called first and self.chatRoom becomes nil. Later, the completion block is called and "room" (or self.chatRoom) attempts to retain, but it is too late.
self.chatRoom is released before the completion block is called and thus you have your zombie.
Is there an abort call of some kind in ChatService?
Does any body know what I need to check if app freezes after some time? I mean, I can see the app in the iPhone screen but no view responds.
I did some google and i found that, i've blocked the main thread somehow.
But my question is how to identify which method causes blocking of main thread? is there any way to identify?
Launch your app and wait for it to freeze. Then press the "pause" button in Xcode. The left pane should show you what method is currently running.
Generally, it is highly recommended to perform on the main thread all animations method and interface manipulation, and to put in background tasks like download data from your server, etc...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//here everything you want to perform in background
dispatch_async(dispatch_get_main_queue(), ^{
//call back to main queue to update user interface
});
});
Source : http://www.raywenderlich.com/31166/25-ios-app-performance-tips-tricks
Set a break point from where the freeze occurs and find which line cause that.
Chances may be,Loading of large data,disable the controls,overload in main thread,Just find out where that occurs using breakpoints and rectify based on that.
I believe it should be possible to periodically check to see if the main thread is blocked or frozen. You could create an object to do this like so:
final class FreezeObserver {
private let frequencySeconds: Double = 10
private let acceptableFreezeLength: Double = 0.5
func start() {
DispatchQueue.global(qos: .background).async {
let timer = Timer(timeInterval: self.frequencySeconds, repeats: true) { _ in
var isFrozen = true
DispatchQueue.main.async {
isFrozen = false
}
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + self.acceptableFreezeLength) {
guard isFrozen else { return }
print("your app is frozen, so crash or whatever")
}
}
let runLoop = RunLoop.current
runLoop.add(timer, forMode: .default)
runLoop.run()
}
}
}
Update October 2021:
Sentry now offers freeze observation, if you don't wanna roll this yourself.
I reached an error similar to this, but it was for different reasons. I had a button that performed a segue to another ViewController that contained a TableView, but it looked like the application froze whenever the segue was performed.
My issue was that I was infinitely calling reloadData() due to a couple of didSet observers in one of my variables. Once I relocated this call elsewhere, the issue was fixed.
Most Of the Time this happened to me when a design change is being called for INFINITE time. Which function can do that? well it is this one:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
Solution is to add condition where the function inside of viewDidLayoutSubviews get calls only 1 time.
It could be that another view is not properly dismissed and it's blocking user interaction! Check the UI Debugger, and look at the top layer, to see if there is any strange thing there.