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?