Hotfixing iOS appswith Javascript
Sergio Padrino Recio
About me…
• Sergio Padrino (@sergiou87)
• Started working on iOS on 2010
• Worked at Tuenti as iOS engineer (2012-2013)
• Worked at Fever as iOS Lead engineer (2014)
• Working at Plex since July 2014 – Current iOS Team Lead
About Plex
About Plex
Example case
Example case
• Submit to Apple.
• Wait for review: 7 days.
• In review: 3 hours.
• Release!
Example caseWTF??
8 days later…
We are all Peter
Example case• Extreme case, workflow full of flaws:
• Coder failed.
• Code reviewer failed.
• Testers failed.
• Apple… didn’t say anything.
Plex: The Monster
• Too many moving parts:
• Plex Media Server version
• User network setup
• Interoperability with other Plex players
• Audio/Video/Subtitle format
Fixes are usually quick, but…
Apple App Review process• Used to be 1 week.
• Now reduced to 1-3 days.
• Still not reliable…
• Reviewer testing the wrong binary.
• App rejected because… Apple 😒
• Christmas holidays.
• Sometimes just gets longer.
appreviewtimes.com
Can we improve this?
Can we improve this?
• Android and Web apps can be updated at any time.
• Native iOS apps need to bypass Apple review:
• Use a remote configuration (GroundControl).
• Embedded web pages can be updated.
• Or…
rollout.io• Service to hotfix apps without Apple review:
• Surround method with try…catch
• Replace method argument or return value
• No-op a method
• Basic scripting
rollout.io• Main problems (for us):
• Their platform has the ability to change our app remotely. If they’re compromised… 😱
• “Expensive” post-build script to upload dSym.
• Our dSym is massive and they had trouble processing it 😆
DIY• Can we do it ourselves… better?
• Recent pain working on Apple TV app:
• Too much Javascript 😖
• Objective-C magic
• My own solution… SPHotPatch
plex.tv
DIY
Configuration file
(hotfixes)
Plex for iOS
MAGIC✨
But… how??
But… how??
Method Swizzling!
Method SwizzlingAn Example
- (void)doSomethingWrong { self.array[self.array.count];}
Method SwizzlingAn Example
- (void)doSomethingWrong { self.array[self.array.count];} - (void)safe_doSomethingWrong { @try { [self safe_doSomethingWrong]; } @catch(…) { } }
Method SwizzlingAn Example
- (void)doSomethingWrong { self.array[self.array.count];} - (void)safe_doSomethingWrong { @try { [self safe_doSomethingWrong]; } @catch(…) { } }
INFINITERECURSION?!
Method SwizzlingAn Example
- (void)doSomethingWrong { self.array[self.array.count];} - (void)safe_doSomethingWrong { @try { [self safe_doSomethingWrong]; } @catch(…) { } }
INFINITERECURSION?!
doSomethingWrong
Method SwizzlingAn Example
array[array.count]
safe_doSomethingWrong
@try { [self safe_doSomethingWrong];} @catch(…) {}
doSomethingWrong
Method SwizzlingAn Example
array[array.count]
safe_doSomethingWrong
@try { [self safe_doSomethingWrong];} @catch(…) {}
safe_doSomethingWrongdoSomethingWrong
@try { [self safe_doSomethingWrong];} @catch(…) {}
Method SwizzlingAn Example
array[array.count]
doSomethingWrong
@try { [self safe_doSomethingWrong];} @catch(…) {}
Method SwizzlingAn Example
array[array.count]
safe_doSomethingWrong
Where is my Javascript??
• JavascriptCore since iOS 7.
• Run JS scripts from Objective-C.
• Bridging: call Objective-C code from JS.
• More Objective-C runtime magic.
Javascript Core
• Run Javascript scripts from Objective-C:
Bridging
• Invoke Objective-C code from Javascript:
MyCrashyClass
doSomethingWrong
Combine all that…
array[array.count]
MyCrashyClass
safe_doSomethingWrongdoSomethingWrong
JSContext *context = …[context evaluate:hotfixString]
Combine all that…
array[array.count]
doSomethingWrong
MyCrashyClass
JSContext *context = …[context evaluate:hotfixString]
…fixed!
array[array.count]
safe_doSomethingWrong
More Objective-C runtime?
• “Proxy” object that gives access to Obj-C stuff from JS.
More Objective-C runtime?
• Box method parameters to JSValue.
• Unbox return value from JSValue ⚠
• A lot of boilerplate to support as many types as possible.
• Took some “inspiration” from OCMockito.
Unboxing return value…IMP newImp = imp_implementationWithBlock(^(id self, ...) { va_list args; // Box parameters from args and prepare script NSString *script = ...; JSValue *result = [context evaluateScript:script]; if ([result isString]) return [result toString]; else if ([result isNumber]) return [result toInt32];}
Unboxing return value…
• Method parameters are easy: variadic arguments.
• There is no “wild card” for return types.
• The only option… override forwardInvocation:
• NSInvocation is the key!
Unboxing return value…
IMP newImp = imp_implementationWithBlock(^(id self, NSInvocation *inv) { // Box parameters from invocation and prepare script NSString *script = ...; JSValue *result = [context evaluateScript:script]; if (inv.methodSignature.methodReturnType == ‘@’) [inv setReturnValue:[result toObject]]; else if (inv.methodSignature.methodReturnType == ‘i’) [inv setReturnValue:[result toInt32]];}
MyCrashyClass
doSomethingWrong
Real Life™
array[array.count]
forwardInvocation:
original implementation
MyCrashyClass
ORIGdoSomethingWrongdoSomethingWrong
_objc_msgForward
Real Life™
forwardInvocation:
original implementation
array[array.count]
MyCrashyClass
ORIGdoSomethingWrongdoSomethingWrong
_objc_msgForward
Real Life™
forwardInvocation:
original implementation
array[array.count]
MyCrashyClass
ORIGdoSomethingWrongdoSomethingWrong
_objc_msgForward
Real Life™
forwardInvocation:
original implementation
array[array.count]
ORIGforwardInvocation:
JSContext *context = …[context evaluate:hotfixString]
MyCrashyClass
ORIGdoSomethingWrongdoSomethingWrong
_objc_msgForward
Real Life™
array[array.count]
ORIGforwardInvocation:forwardInvocation:
JSContext *context = …[context evaluate:hotfixString] original implementation
DEMO
SPHotPatch• Share it with friends to show off…
• …until someone tells me about JSPatch
• Open Source
• Better JS syntax (pre-processing)
• Extensions to use C stuff (CoreGraphics…)
JSPatch
JSPatch in Plex• Remote configuration file declares available patches.
• Patches belong to a:
• App version.
• App build.
• Patch channel (default: production).
JSPatch in Plex
• Multiple “channels” allow:
• Testing hotfixes before “releasing” them.
• Create custom patches for users if needed.
JSPatch in Plex
JSPatch in Plex• More features:
• Safe patching: if the app crashes before the patch can be downloaded and applied, next time the whole patching process will be synchronous.
• Skip patching in the next run.
• Clear last patch.
Things you can do
Hotfixingof course
Gather data
• For bugs hard/impossible to reproduce.
• Create specific patch channel for affected users.
• Deploy patches for those users.
• Ask them for feedback in the forums.
Gather data
• Example 1:
• Video stalls and stuttering.
• Patches to log more info.
• Patches to change different settings of the video player.
Gather data
• Example 2:
• Weird AutoLayout crash on old devices.
• Crash stacktrace impossible to read: all Apple code.
• Patches to change different bits of broken layout.
Rewrite the whole app in JS
Rewrite the whole app in JS
Things you CAN’T fix
Things you CAN’T fixseriously…
• Virtually nothing?
• You REALLY can write your whole app with JSPatch!
• Create extensions for C stuff you need.
• Apply patches as soon as you can.
• At Plex, we leave out +load methods.
What about Swift?
• Depends on Objective-C runtime so…
• Only works with NSObject.
• No structs or primitive types.
• No classes not inheriting from NSObject.
What about Apple?3.3.2 Except as set forth in the next paragraph, an Application may not download or install executable code. Interpreted code may only be used in an Application if all scripts, code and interpreters are packaged in the Application and not downloaded. The only exceptions to the foregoing are scripts and code downloaded and run by Apple's built-in WebKit framework or JavascriptCore, provided that such scripts and code do not change the primary purpose of the Application by providing features or functionality that are inconsistent with the intended and advertised purpose of the Application as submitted to the App Store
What about Apple?
• Just Javascript code that runs in JavascriptCore.
• Small fixes, not changing the whole app.
Security
• Avoid downloading patches from unknown sources.
• Don’t run JS scripts without a valid signature.
• Only allow to sign patches to a handful of people.
Good practices
• Don’t abuse JS patching. It’s just your safe net.
• Establish a proper workflow to catch bugs before the release.
• Test, test, test. Automate as much as you can.
Good practices• QA should find nothing. If they do, it should be a big
thing.
• If all the above fails and the bug has a huge impact, hotfix it.
• JS hotfixes should be reviewed and tested too.
• Immediately after, submit another build. Never rely on those hotfixes.
Questions?
Thank you!
Top Related