因為原生這邊有個換膚功能,用戶下載對應的皮膚包,app會重新渲染一些頁面元素色值,以及替換相關圖片。某一天產品跑來說,RN的頁面也要支持換膚哦。雖然我默默的從抽屜里拿出來一把caidao。但還是要去想辦法把它搞掉。
思路如下:
1.先去看看原生那邊換膚是怎么玩的。
- 點開app下載一個皮膚包。會從服務器請求一個zip壓縮包。
- 解壓縮打開,一份色值表配置json文件,若干替換圖片。
看代碼。大致實現是換膚庫提供了很多的UIKit組件的分類,提供了相關設置色值,圖片的方法。并且會去注冊監聽換膚通知。一般我們寫業務代碼的時候,都會使用那些分類所提供的支持換膚的方法。并且部分頁面需要主動監聽換膚通知的從而實現主動渲染。當觸發換膚的時候,頁面重新渲染,就會從對應的皮膚包里獲取對應的圖片,以及色值。從而實現的換膚。
2.RN這邊既然要支持換膚,不就是JS中設置圖片,色值的地方改成通過橋接從原生那邊獲取對應的色值和圖片嗎?
- 色值
首先原生寫好了一個橋接方法,傳入不同的key去色值表中查找對應的16進制色值,返回給rn。rn頁面在render()函數之前,調用該橋接獲取色值,并且通過state保存該色值。之后在render()函數渲染的時候設置到對應的節點上就行了。好吧。是可以。后來又發現了個問題。RN中原生和JS橋接是異步執行,那么就不能保證在render()函數執行之前獲取到的是真正的色值。很尷尬。這種方式好像行不通。換個思路。 - 圖片
其實RN加載圖片,是通過RCTImageLoader這個類。通過斷點發現主要調用下面的那個方法。
-(RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest *)request
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate))completionBlock {
}
主要的一個參數就是request。如果是網絡圖片就是正常的http:// 協議的鏈接。如果是本地圖片,不管是項目image.xcassets里的圖片還是mainBundle里的圖片,又或者是沙盒里的圖片,都會是一個file://協議的圖片本地地址。好吧。替換下圖片路徑試試?結果可行。那么RN圖片換膚就可以通過修改源碼來實現。
3.圖片搞定了,還有色值呢?既然圖片可以通過修改源碼的形式搞定,那么色值能不能也通過修改源碼的方式去搞?找到原生這邊生成UIColor的方法?之前RN那部分不僅支持16進制的色值,而且還支持字符串類型的色值,例如設置'red',RN最終會設置成真正的色值。RN是不是通過這個字符串key,去它自己的色值表去查找對應色值的呢?那么就要找到將‘red’解析成真正色值的方法。
- 原生部分
找了半天,發現了RCTConvert中有個方法是用來轉換顏色的。斷點發現RN中的設置顏色最終都會來到該方法里轉換成UIColor的對象。
+ (UIColor *)UIColor:(id)json
{
if (!json) {
return nil;
}
if ([json isKindOfClass:[NSString class]]) {
//去皮膚包查找色值
if([json isEqualToString:@"ck_black"]) {
return [UIColor whiteColor];
}
}
if ([json isKindOfClass:[NSArray class]]) {
NSArray *components = [self NSNumberArray:json];
CGFloat alpha = components.count > 3 ? [self CGFloat:components[3]] : 1.0;
return [UIColor colorWithRed:[self CGFloat:components[0]]
green:[self CGFloat:components[1]]
blue:[self CGFloat:components[2]]
alpha:alpha];
} else if ([json isKindOfClass:[NSNumber class]]) {
NSUInteger argb = [self NSUInteger:json];
CGFloat a = ((argb >> 24) & 0xFF) / 255.0;
CGFloat r = ((argb >> 16) & 0xFF) / 255.0;
CGFloat g = ((argb >> 8) & 0xFF) / 255.0;
CGFloat b = (argb & 0xFF) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:a];
} else {
RCTLogConvertError(json, @"a UIColor. Did you forget to call processColor() on the JS side?");
return nil;
}
}
好吧。看起來該方法里只支持從RN那邊傳遞過來的NSArray類型和NSNumber類型的。也不支持NSString類型的啊。所以RN中通過'red'這種字符串設置顏色的解析并不是在原生這部分。所以在原生部分支持字符串類型。通過該字符串key去對應皮膚包的色值表中去查找真正的色值。
- RN部分
發現node_modules/react-native/Libraries/StyleSheet/processColor.js該文件中processColor()函數是用來解析色值的。
var Platform = require('Platform');
var normalizeColor = require('normalizeColor');
/* eslint no-bitwise: 0 */
function processColor(color) {
if (color === undefined || color === null) {
return color;
}
var int32Color = normalizeColor(color);
if (int32Color === null) {
return undefined;
}
if(typeof int32Color === 'string') {
return int32Color;
}
// Converts 0xrrggbbaa into 0xaarrggbb
int32Color = (int32Color << 24 | int32Color >>> 8) >>> 0;
if (Platform.OS === 'android') {
// Android use 32 bit *signed* integer to represent the color
// We utilize the fact that bitwise operations in JS also operates on
// signed 32 bit integers, so that we can use those to convert from
// *unsigned* to *signed* 32bit int that way.
int32Color = int32Color | 0x0;
}
return int32Color;
}
在該方法中,RN會去查找類似'red'這樣的色值,并且返回為一個32位int類型。如果沒找到對應色值,它會強轉為一個0x0的色值。所以我們在它查找完之后,強轉之前,如果沒找到,直接將字符串類型的變量返回出去,這樣原生也就能接受到一個字符串類型的key了。
最后
最后RN換膚實踐成功實現了產品的需求。就是找代碼的過程很藍瘦,尤其是JS的那部分。如果RN版本升級了,動過的源碼部分也要同步過去。