Demo Project: nsdictionary-nilsafe
Problem
People who believe in developing IOS apps with objective-c are not unfamiliar with the following crash:
-
* * *-[__nsplaceholderdictionary InitWithObjects:forKeys:count:]: Attempt to Insert Nil object from objects[1]
-
* * * Setobjectforkey:key cannot be nil
-
* * Setobjectforkey:object cannot be nil
" in >objective-c; nsdictionary
Span class= "Apple-converted-space" > is not supported   Nil
as key or value. But there will always be some places that occasionally go to   nsdictionary
insert   Nil
value. In our project development process, there are two very common scenarios:
- The Event log (button click or page impression), such as:
log:SOME_PAGE_IMPRESSION_EVENT eventData:@{ @"some_value": someObject.someValue,}];www.90168.org
- When sending an API request, such as:
NSDictionary *params = @{ @"some_key": someValue,};[[APIClient sharedClient] post:someURL params:params callback:callback];
Initially, many of the following fragments exist in our code:
log:SOME_PAGE_IMPRESSION_EVENT eventData:@{ @"some_value": someObject.someValue ?: @"",}];
NSDictionary *params = @{ @"some_key": someValue ?: @"",};
Or:
NSMutableDictionary *params = [NSMutableDictionary dictionary]; if (someValue) { params[@"some_key"] = someValue;}
There are several disadvantages to doing this:
- Too many redundant code
- Accidentally forget to check nil, some corner case only live crash will be found on the line
- Most of our APIs are in JSON format, so a
nil
value Whether it is an empty string or not, is semantically incorrect, and may even cause some strange server bugs
so we want   nsdictionary
is used like this:
-
nil
no crash when inserted.
-
nil
The key that it corresponds to after inserting does exist and can fetch the value (NSNull)
- is converted to NULL when it is serialize into JSON.
- Let's get
NSNull
closer nil
, can eat any way not crash
Test Cases
This task is well suited for test-driven development, so you can simply translate the requirements from the previous section into the following test cases:
- (void) Testliteral {ID nilval =NilID Nilkey =NilID Nonnilkey =@ "Non-nil-key";ID nonnilval =@ "Non-nil-val";Nsdictionary *dict = @{nonnilkey:nilval, Nilkey:nonnilval,}; Xctassertequalobjects ([Dict AllKeys], @[nonnilkey]); Xctassertnothrow ([dict objectforkey:nonnilkey]);ID val = Dict[nonnilkey]; Xctassertequalobjects (Val, [NSNull null]); Xctassertnothrow ([val length]); Xctassertnothrow ([Val Count]); Xctassertnothrow ([Val anyobject]); Xctassertnothrow ([Val intvalue]); Xctassertnothrow ([Val integervalue]);} - (void) Testkeyedsubscript {Nsmutabledictionary *dict = [Nsmutabledictionary dictionary];ID nilval =NilID Nilkey =NilID Nonnilkey =@ "Non-nil-key";ID nonnilval =@ "Non-nil-val"; Dict[nonnilkey] = Nilval; Dict[nilkey] = Nonnilval; Xctassertequalobjects ([Dict AllKeys], @[nonnilkey]); Xctassertnothrow ([dict objectforkey:nonnilkey]);} - (void) Testsetobject {Nsmutabledictionary *dict = [Nsmutabledictionary dictionary];ID nilval =NilID Nilkey =NilID Nonnilkey =@ "Non-nil-key";ID nonnilval =@ "Non-nil-val"; [Dict Setobject:nilval Forkey:nonnilkey]; [Dict Setobject:nonnilval Forkey:nilkey]; Xctassertequalobjects ([Dict AllKeys], @[nonnilkey]); Xctassertnothrow ([dict objectforkey:nonnilkey]);} - (void) Testarchive {ID nilval =NilID Nilkey =NilID Nonnilkey =@ "Non-nil-key";ID nonnilval =@ "Non-nil-val";Nsdictionary *dict = @{nonnilkey:nilval, Nilkey:nonnilval, }; NSData *data = [Nskeyedarchiver archiveddatawithrootobject:dict];Nsdictionary *dict2 = [Nskeyedunarchiver unarchiveobjectwithdata:data]; Xctassertequalobjects ([Dict2 AllKeys], @[nonnilkey]); Xctassertnothrow ([Dict2 Objectforkey:nonnilkey]);}- (void) Testjson {id nilval = NIL; id nilkey = NIL; id nonnilkey = @ "Non-nil-key"; id nonnilval = @ "Non-nil-val"; nsdictionary *dict = @{nonnilkey:nilval, Nilkey:nonnilval,}; nsdata *data = [nsjsonserialization datawithjsonobject:dict options:0 error:null"; nsstring *jsonstring = [[nsstring Alloc] InitWithData:data Encoding:nsutf8stringencoding]; nsstring *expectedstring = @ "{\" non-nil-key\ ": null}"; Xctassertequalobjects (jsonstring, expectedstring);}
The above code can be found in the demo project, before the transformation, all case should be fail, the purpose of the transformation is to let them all pass.
Method swizzling
According to crash Log,dictionary there are three main entrances to the nil object:
- when the literal Initializes a dictionary, the is invoked;
DictionaryWithObjects:forKeys:count:
- directly calls
Setobject:forkey
- when the value is assigned by subscript, is called;
setobject:forkeyedsubscript:
So you can pass the method swizzling, the four methods (also initWithObjects:forKeys:count:
, although not found where there is a call to it) to replace their own methods, when the key is nil, when the value is nil, replace with NSNull re-insert.
This setObject:forKey
method is actually replaced by the method because it is implemented through class cluster __NSDictionaryM
.
Take dictionaryWithObjects:forKeys:count:
For example:
+ (Instancetype) Gl_dictionarywithobjects: (const id []) objects Forkeys: ( Const id<nscopying> []) Keys count: (Nsuinteger) CNT {id SAFEOBJECTS[CNT]; id safekeys[cnt]; Nsuinteger j = 0; for (Nsuinteger i = 0; i < cnt; i++) {id key = Keys[i]; id obj = objects[i]; if (!key) {continue;} if (!obj) {obj = [NSNull null];} safekeys[j] = key; Safeobjects[j] = obj; j + +;} return [self gl_dictionarywithobjects:safeobjects ForKeys: Safekeys count:j];
See the GitHub source file for full code.
After this category is introduced, all test cases can be passed smoothly.
Safety of NSNull
As modified Nsdictionary, the odds of getting NSNull from dictionary are higher, so we want NSNull to accept all method calls and return nil/0, just like nil.
At first, we used the Extnil in LIBEXTOBJC as the placeholder to make null more secure. Later found that can actually refer to the implementation of Extnil directly swizzle NSNull itself, so that it can accept all method calls:
-(Nsmethodsignature *) Gl_methodsignatureforselector: (SEL) aselector {nsmethodsignature *sig = [Self Gl_methodSignatur Eforselector:aselector];if (SIG) { return sig; } return [nsmethodsignature signaturewithobjctypes: @encode (void)];} www.90168.org-(void) Gl_forwardinvocation: (nsinvocation *) aninvocation {nsuinteger returnlength = [[ Aninvocation Methodsignature] methodreturnlength]; if (! Returnlength) {//Nothing to does return;}// set return value to all zero bits char buffer[Returnle Ngth]; memset (buffer, 0, returnlength); [Aninvocation setreturnvalue:buffer];}
Summarize
At this point, we have solved all the problems mentioned in the first section, with a nil safe nsdictionary. This program in the actual project used for more than a year, the effect is good, the only encounter a pit is to write to the nsuserdefaults with NSNull dictionary time will crash: Attempt to insert non-property list object
. Of course this is not the problem of the solution itself, the solution is to put dictionary archive or serialize into JSON and then write to the user Defaults, but then again, the complex structure is considered to be removed from the user Defaults.
When Nsdictionary met Nil