CoreHaptics [__NSDictionaryI dealloc] crashes EXC_BAD_ACCESS

47 Views Asked by At

I am receiving rare crash reports in the Objective-C plugin written for Unity iOS builds to play haptics via AHAP json format.

Relevant parts of the plugin code is below. "playWithDictionaryFromJsonPattern" is the method used:

#import "CoreHapticUnityObjC.h"

@interface CoreHapticUnityObjC()
@property (nonatomic, strong) CHHapticEngine* engine;
@property (nonatomic, strong) id<CHHapticPatternPlayer> patternPlayer;
@property (nonatomic) BOOL isEngineStarted;
@property (nonatomic) BOOL isSupportHaptic;
@end

@implementation CoreHapticUnityObjC

static CoreHapticUnityObjC * _shared;

+ (CoreHapticUnityObjC*) shared {
    @synchronized (self) {
        if(_shared == nil) {
            _shared = [[self alloc] init];
        }
    }
    return _shared;
}

- (id) init {
    if (self == [super init]) {

        self.isSupportHaptic = @available(iOS 13, *) && CHHapticEngine.capabilitiesForHardware.supportsHaptics;
        #if DEBUG
          NSLog(@"[CoreHapticUnityObjC] isSupportHaptic -> %d", self.isSupportHaptic);
        #endif

        [self createEngine];
    }
    return self;
}

- (void) dealloc {
  #if DEBUG
      NSLog(@"[CoreHapticUnityObjC] dealloc");
  #endif

  if (self.isSupportHaptic) {

    self.engine = NULL;
    self.continuousPlayer = NULL;
  }
}

- (void) playWithDictionaryPattern: (NSDictionary*) hapticDict {
    if (self.isSupportHaptic) {

        if (self.engine == NULL) {
            [self createEngine];
        }
        [self startEngine];

        if(!_isEngineStarted) return;

        NSError* error = nil;
        CHHapticPattern* pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error];

        if (error == nil) {
            _patternPlayer = [_engine createPlayerWithPattern:pattern error:&error];

            [_engine notifyWhenPlayersFinished:^CHHapticEngineFinishedAction(NSError * _Nullable error) {
                if (error == NULL || error == nil) {
                     if (onHapticPatternFinished != NULL) {
                         onHapticPatternFinished(0);
                     }
                    return CHHapticEngineFinishedActionLeaveEngineRunning;
                } else {
                     if (onHapticPatternFinished != NULL) {
                         onHapticPatternFinished((int)error.code);
                     }
                    return CHHapticEngineFinishedActionStopEngine;
                }
            }];

            if (error == nil) {
                [_patternPlayer startAtTime:0 error:&error];
            } else {
                NSLog(@"[CoreHapticUnityObjC] Create dictionary player error --> %@", error);
            }
        } else {
            NSLog(@"[CoreHapticUnityObjC] Create dictionary pattern error --> %@", error);
        }
    }
}

- (void) playWithDictionaryFromJsonPattern: (NSString*) jsonDict {
    if (jsonDict != nil) {
        #if DEBUG
            NSLog(@"[CoreHapticUnityObjC] playWithDictionaryFromJsonPattern --> json: %@", jsonDict);
        #endif

        NSError* error = nil;
        NSData* data = [jsonDict dataUsingEncoding:NSUTF8StringEncoding];
        NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];

        if (error == nil) {
            [self playWithDictionaryPattern:dict];
        } else {
            NSLog(@"[CoreHapticUnityObjC] Create dictionary from json error --> %@", error);
        }
    } else {
        NSLog(@"[CoreHapticUnityObjC] Json dictionary string is nil");
    }
}

- (void) createEngine {
    if (self.isSupportHaptic) {
        NSError* error = nil;
        _engine = [[CHHapticEngine alloc] initAndReturnError:&error];

        if (error == nil) {

            _engine.playsHapticsOnly = true;
            __weak CoreHapticUnityObjC *weakSelf = self;

            _engine.stoppedHandler = ^(CHHapticEngineStoppedReason reason) {
                NSLog(@"[CoreHapticUnityObjC] The engine stopped for reason: %ld", (long)reason);
                switch (reason) {
                    case CHHapticEngineStoppedReasonAudioSessionInterrupt:
                        NSLog(@"[CoreHapticUnityObjC] Audio session interrupt");
                        break;
                    case CHHapticEngineStoppedReasonApplicationSuspended:
                        NSLog(@"[CoreHapticUnityObjC] Application suspended");
                        break;
                    case CHHapticEngineStoppedReasonIdleTimeout:
                        NSLog(@"[CoreHapticUnityObjC] Idle timeout");
                        break;
                    case CHHapticEngineStoppedReasonSystemError:
                        NSLog(@"[CoreHapticUnityObjC] System error");
                        break;
                    case CHHapticEngineStoppedReasonNotifyWhenFinished:
                        NSLog(@"[CoreHapticUnityObjC] Playback finished");
                        break;

                    default:
                        NSLog(@"[CoreHapticUnityObjC] Unknown error");
                        break;
                }

                weakSelf.isEngineStarted = false;
            };

            _engine.resetHandler = ^{
                [weakSelf startEngine];
            };
        } else {
            NSLog(@"[CoreHapticUnityObjC] Engine init error --> %@", error);
        }
    }
}

- (void) startEngine {
    if (self.engine == NULL) {
        [self createEngine];
    }

    if (!_isEngineStarted) {
        NSError* error = nil;
        [_engine startAndReturnError:&error];

        if (error != nil) {
            NSLog(@"[CoreHapticUnityObjC] Engine start error --> %@", error);
        } else {
            _isEngineStarted = true;
        }
    }
}

- (NSString*) createNSString: (const char*) string {
  if (string)
      return [[NSString alloc] initWithUTF8String:string];
  else
      return [NSString stringWithUTF8String: ""];
}

@end


#pragma mark - Bridge

extern "C" {
    void _coreHapticsUnityplayWithDictionaryPattern(const char* jsonDict) {
        [[CoreHapticUnityObjC shared] playWithDictionaryFromJsonPattern:[[CoreHapticUnityObjC shared] createNSString:jsonDict]];
    }
}

The crash report from Crashlytics:

EXC_BAD_ACCESS 0x3f7fffea3ad378d1
Crashed: CHMetricsDispatchQueue
0  libobjc.A.dylib                0x3a9c objc_release + 8
1  libobjc.A.dylib                0x3a9c objc_release_x0 + 8
2  CoreFoundation                 0x6e394 -[__NSDictionaryI dealloc] + 152
3  CoreHaptics                    0x21a1c -[CHMetrics sendDetailedMetricsForPlayerID:endTime:] + 1724
4  CoreHaptics                    0x7520 __44-[CHMetrics handleFinishedForPlayersAtTime:]_block_invoke + 172
5  CoreHaptics                    0x75d0 __29-[CHMetrics dispatchOnLocal:]_block_invoke + 44
6  libdispatch.dylib              0x637a8 _dispatch_call_block_and_release + 24
7  libdispatch.dylib              0x64780 _dispatch_client_callout + 16
8  libdispatch.dylib              0x3f6fc _dispatch_lane_serial_drain$VARIANT$armv81 + 600
9  libdispatch.dylib              0x401b0 _dispatch_lane_invoke$VARIANT$armv81 + 380
10 libdispatch.dylib              0x49f14 _dispatch_workloop_worker_thread + 608
11 libsystem_pthread.dylib        0x1bd0 _pthread_wqthread + 284
12 libsystem_pthread.dylib        0x1720 start_wqthread + 8

Tried to reproduce the crash locally but failed to do so. I'm not experienced in Objective-C, ARC and iOS development in general, so not sure what to look for here. Looking at the stack trace I thought that a duplicate deallocation is the cause for the crash, but I don't know why that would happen and how to prevent it. The only place I play haptic feedback in the app is this plugin. It's always called on the main thread on the Unity side and I just pass a string that contains the json content. Seeing the crash is related to CoreHaptics, this is the only place I'm focusing on. Any ideas?

0

There are 0 best solutions below