记录下自己在 RN 开发中遇到的一些问题。
RN 组件相关
TextInput
-
Android 文字对齐问题
当 TextInput 高度超过一行文本时,发现文字显示在 iOS 上是顶端对齐,而在 Android 上则是垂直居中,如下图:
解决方法是通过为 TextInput 设置 textAlignVertical: "top"
属性,相关 issue 见:Render Multiline Text at start instead of center
-
Android 上 TextInput 接收获得焦点之后键盘无法自动收起
这时候我们可以给根布局设置接收触摸事件:
onStartShouldSetResponder={() => true}
onResponderRelease={() => Keyboard.dismiss()}
这样当输入焦点之外区域触摸后,通过调用 Keyboard.dismiss() 方法强制收起键盘,曲线救国。
-
iOS 上无法清除文字
这个问题只出现在 RN 版本 0.55.x,如果不升级的话只能用下面比较不优雅的方式来解决:
clear() {
if (Platform.OS === 'ios') {
this.input.setNativeProps({ text: ' ' });
}
setTimeout(() => {
this.input.setNativeProps({ text: '' });
});
}
相关 issue 见:issues/18272
KeyboardAvoidingView
-
behavior
相关
该组件在 Android 和 iOS 上的表现有区别,所以我们会区分平台使用不同的 behavior,比如下面这样:
...
padding
模式下,当键盘弹起的时候,你的 view 会向上弹起并被压缩。使用 padding
作为 behavior 的时候,在 iOS 上表现比较好,而在 Android 上则不设置 behavior 比较好。
position
模式下,view 整体会向上滑动。这种模式 Android 和 iOS 上表现一致,但是前提是此时 KeyboardAvoidingView
是根 view。同时这也会造成一个问题,那就是键盘弹出后,输入组件会一直占有焦点,这在安卓上还好,可以通过返回键关闭键盘,而在 iOS 设备上就会造成键盘无法被关闭的尴尬。解决这一问题的方法是通过在 KeyboardAvoidingView
设置接收触摸事件,当在输入焦点之外获得点击时收起键盘:
true}
onResponderRelease={() => Keyboard.dismiss()}
/>
ImageBackground
-
imageStyle
属性
有没有发现给 ImageBackground 设置 style
的时候,其中某些属性似乎不起作用?比如设置 border
似乎没有效果。其实看下源码就可以发现,原来 style
属性里面还有个 imageStyle
属性,类似 border
和 borderRadius
这样的属性要设置到 imageStyle
上才有效。
FlatList/SectionList
-
contentContainerStyle
属性
与 ImageBackground 类似,在给 FlatList 设置 paddingBottom 的时候,发现不起作用,后来在这个 issue 下找到了解决办法:FlastList/SectionList 中有个 contentContainerStyle
属性,代表 list 中的 content 容器的 style 属性。所以如果想要为 list 设置 paddingBottom,在这个属性上设置才能起作用。
-
加载时的性能问题
我们知道在 Android 中加载大量列表数据时,RecyclerView 的性能是比较好的,因为它可以复用 view,而在 RN 中如果你用 FlatList 直接加载成百上千的数据的时候,你会发现整个界面会变得非常卡,所以这种情况下我们就需要懒加载。FlatList 本身是支持增量加载的,只不过需要一些额外的处理。
首先,FlatList 中有一个 initialNumToRender
属性,用于指定初始加载的数据,我们可以设置为 10,这个看你的需求了,一般根据 item 的高度来定。然后 FlatList 还有一个 onEndReached
属性,我们可以在这里定义一个方法,用于指定当列表滑动到底部的时候触发的事件。有了这两个属性,我们就可以对 FlatList 中的数据进行懒加载了。
}
initialNumToRender={10}
numColumns={2}
onEndReached={() => this.lazyLoadProducts()}
onEndReachedThreshold={0.5}
ListHeaderComponent={this.props.ListHeaderComponent}
ListFooterComponent={(this.state.productList.length !== this.props.productList.length) ? : undefined}
/>
可以看到,FlatList 中数据源来自 this.state.productList
,然后在 onEndReached
中调用了一个 lazyLoadProducts
方法:
lazyLoadProducts() {
if (this.state.productList.length === this.props.productList.length) {
return;
}
this.setState(state => ({
productList: state.productList.concat(
this.props.productList.slice(state.productList.length, this.state.productList.length + 10)
.map(item => ({...item, key: item.id})))
}));
}
我们首先将完整的数据保存在 props 中,然后在 onEndReached
中每次多加载 10 条新数据。
可以看到上面的 FlatList
中还定义了一个 onEndReachedThreshold
属性,表示 FlatList
可见部分离底部多远的时候会触发 onEndReached
方法。比如我们定义为 0.5,则如果可见部分为 10 条数据,那么当我们向下滑动 5 条数据的时候,就会去加载另外 5 条新数据。
PanResponder
-
onPanResponderMove
和Animated.event()
的结合使用
利用 PanResponder 做了一个拖动调节图标位置的功能,网上找的方法是在 onPanResponderMove
中使用 Animated.event()
来对 View 进行移动。实现效果不错,但是发现一旦在 onPanResponderMove
中使用了 lambda 表达式后,就不起作用了。后来网上找到这个 issue,发现原来 Animated.event()
会返回一个方法,并且接收 event 和 gestureState 作为参数,所以我们只要去调用一下这个方法即可:
onPanResponderMove: (evt, gestureState) => {
return Animated.event([null, {
dx: this.state.pan.x,
dy: this.state.pan.y,
}])(evt, gestureState)
}
UIManager
我们可以使用 UIManager 来测量某个 view 的位置,这个在一些特殊的场合非常有用。
测量某个 view 的位置前,我们首先需要获得该 view 的引用:
this.myView = component}/>
获得 view 的引用后,就可以通过 view 获得 nodeHandle
去测量 view 的位置了:
measurePosition() {
let nodeHandle = findNodeHandle(this.myView);
if (nodeHandle) {
UIManager.measure(nodeHandle, (x, y, width, height, pageX, pageY) => {
// measure success, do something with the data
});
}
}
从示例代码中可以看到,在测量方法中,我们定义了一个测量成功的回调,我们可以在这里获得测量到的当前 view 的中心点坐标,高度,宽度,距离页面顶端的 x 坐标,y 坐标。
其他 Tips
-
diplay: none
在 Android 上失效的问题
这个一般是由于和 position
混用造成的,只要在需要使用 display: none
的组件外添加一个 absolute
位置即可:
更进一步,为了防止你隐藏掉的 View 阻挡被覆盖的其他 View 的点击事件,以及在需要隐藏的 View 里存在可点击的组件,则还需要使用到 View 的 pointerEvents 属性做以下设置:
当展示出来的时候才可点击,如果隐藏则不接收点击事件。
- 组件循环更新的问题
有一种常见的场景是,当有一个组件 A 使用外部组件 B 的 state 作为 prop,并且组件 A 通过回调方法来传送数据(setState)给外部组件时,此时如果外部组件 B 在回调方法中也调用了 setState 方法,那么就会造成内部组件的 state 循环更新的问题。
一种解决方法是在组件 A 中的每一个需要传送数据到外部组件中的方法(setState)中添加一个标记,然后在 componentDidUpdate
中根据该标记来判断是否应该对回调方法进行调用。如下:
// region 内部组件 InnerComonent
componentDidUpdate(prevProps, prevState) {
// flag 为 true 时才进行回调
if (this.state.stateUpdateFlag) {
// 通过回调传递数据给外部组件
this.props.callback(_.omit(this.state, ['stateUpdateFlag']));
this.setState({stateUpdateFlag: false});
}
}
_updateContent(data) {
// ... 其它代码
this.setState({
data,
stateUpdateFlag: true
});
// 外部组件的回调方法中如果调用了 setState 方法,则用会造成内部组件 state 循环更新
// this.props.callback(data);
}
// endregion
// region 外部组件
render() {
return this.setState({formData: data})}/>
}
// endregion
- 屏幕闪动的问题
在 RN 0.62 以上的版本,如果为图片添加 flex: 1
的 style 就会出现页面闪动的情况,去除之后就能解决。
三方库相关
常见问题
-
三方库无法下载,卡在
checking installable status
这种情况大概率是你之前下载过,导致机子本地 npm 缓存与三方库冲突。此种情况,我一般通过全局安装,然后使用
npm link
来解决。 -
依赖未正确 link 导致的报错
很多时候安装三方库之后都会遇到各种各样的报错,这种时候不要慌,先对照三方库 README 检查下是不是缺了某些步骤,如果还是无法解决就去翻翻 issue,然后再使用谷歌搜索关键字。不要急着求助,因为 99.99% 的情况下,你遇到的问题别人早就已经遇到过了。
实在不行就把依赖删了然后重新安装并一步步检查 link 步骤有没有漏掉。
重启电脑可以解决大部分令人原地爆炸的奇怪问题。
@ant-design/react-native
- 3.x 版本一些组件 bug 很多,比如 Modal 等,还有使用的 ViewPager 版本过旧,如果造成 "register two views with the same name RNCViewPager" 的问题可以尝试删除 @ant-design/react-native/node-module 下的 @react-native-community/viewpager。
react-redux
- action 名字尽量不要和 reducer 的名字一样,这样会导致调用 action 的时候报 "xxx is not a function 的错误"。
关于如何使用可以看我之前写的文章:React Native - Redux 入门 和 React Native - 从 Redux 进阶谈起
react-native-update
-
iOS 上传报错
iOS 上传 ipa 包时,报错 TypeError [ERR_INVALID_CALLBACK]:
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
at maybeCallback (fs.js:133:9)
at Object.exists (fs.js:201:3)
at CertDownloader.pem (/Users/AwesomeProject/node_modules/cert-downloader/index.js:96:18)
at CertDownloader.verify (/Users/AwesomeProject/node_modules/cert-downloader/index.js:131:11)
at module.exports (/Users/AwesomeProject/node_modules/provisioning/index.js:7:10)
at /Users/AwesomeProject/node_modules/ipa-metadata/node_modules/async/lib/async.js:731:23
at /Users/AwesomeProject/node_modules/ipa-metadata/node_modules/async/lib/async.js:673:13
at /Users/AwesomeProject/node_modules/ipa-metadata/node_modules/async/lib/async.js:230:13
at _arrayEach (/Users/AwesomeProject/node_modules/ipa-metadata/node_modules/async/lib/async.js:81:9)
at _each (/Users/AwesomeProject/node_modules/ipa-metadata/node_modules/async/lib/async.js:72:13)
发现是由于 node 版本过高,相关 issue: pushy uploadIpa TypeError #209,可以通过下载 n
切换到较低版本来解决:
npm install -g n
sudo n 8.11.1
比如这里我把 node 从 10.7.0 降到 8.11.1 问题就解决了,参考:React Native 问题记录
react-native-scrollable-tab-view
在 Android 中,如果在该库的核心组件 ScrollableTabView
外嵌套使用了 ScrollView
的话,就会出现在 ScrollableTabView
中的内容无法显示的问题,在 iOS 上却又没有这个问题。
找了很多解决方案,但是都不太可用,比如为 ScrollView
的 contentContainerStyle
设置 {flex: 1}
。但是这样一来整个 ScrollView
就无法滚动了,所以摸索出一个可行的解决方案是为 contentContainerStyle
设置高度:
这种方法只能算是一个 hack 吧,即使是通过测量 view 的高度也不是很好,因为如果 tab 是很长的一个 list,会造成比较大的性能开销,而如果动态增加 ScrollView 的高度的话,要是 tab 里面包含长图也会需要做额外的处理。所以还是不推荐嵌套 ScrollView 使用。
react-native-puti-pay
这是一个微信和支付宝支付库,如果集成该库的同时也集成了 react-native-wechat,则会造成冲突:
duplicate symbol _OBJC_CLASS_$_WechatAuthSDK.xx
duplicate symbol _OBJC_IVAR_$_WechatAuthSDK.xx
...
上面只列出了两种冲突项,全部的冲突项可能达几十个,主要是因为这两个库同时引用了 libWeChatSDK.a
,所以只要删除其中一个库中的该引用即可。
react-native-debugger
配合 redux 使用:
import {applyMiddleware, combineReducers, compose, createStore} from 'redux';
const allReducers = combineReducers({...});
// 开启 REDUX DEVTOOLS 支持
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
// 通过 reducer生成 store (封装中间件)
let store = createStore(allReducers, composeEnhancers(applyMiddleware(thunkMiddleware)));
export default store;
在 console 中获取 AsyncStorage 中的数据:
showAsyncStorageContentInDev()
react-native-router-flux
-
实现 Android 上连续点击两次返回键退出
这个在安卓上是比较常见的操作,但是在 RN 中结合 react-native-router-flux 使用却折腾了好长时间,这里记录下自己的实现方式。
首先,react-native-router-flux 原生就支持,我们不需要通过自己去添加 BackHandler 监听器来实现。当我们使用 react-native-router-flux 时,我们一般用 Router
作为根节点,所以我们通过 Router
为其设置 backAndroidHandler
属性即可。
let lastBackPressed = Date.now();
const onExitApp = () => {
if (Actions.currentScene !== 'home') {
Actions.pop();
return true
}
if (lastBackPressed && Date.now() < lastBackPressed + 2000) {
BackHandler.exitApp();
return false;
}
ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
lastBackPressed = Date.now();
return true;
};
class AppRouter extends Component {
render() {
return
{/* other code */}
}
}
可以看到,这里定义了一个 onExitApp
方法并且设置到了 backAndroidHandler
属性上,该属性会根据返回值来决定是否退出应用(false 时退出应用)。
在 onExitApp
中,我们首先判断当前应用是否处于根页面(可以通过 react-native-router-flux 中的 Actions.currentScene
获取当前 Scene),如果不是根页面则作为普通的返回键处理(弹出一页),否则判断是否在指定时间内连续点击,连续点击才退出应用,否则弹出 Toast 提醒。
-
在 Android 上点击返回键时使 WebView 后退一页
假如有这样一个需求,某个页面下有一个 WebView 组件,我们需要控制当在该页面按下返回键时后退一页(相当于从网页的历史记录中后退),而如果没有历史记录时则直接退出。这个要怎么实现呢?
注意到,WebView 中有一个 onNavigationStateChange
方法,当新的页面加载或退出时该方法会被调用。因此一种可行的方法是,在该方法中监听页面变化并读取页面加载后的数据。以下是该方法中的部分数据:
canGoBack: true
canGoForward: true
loading: false
navigationType: "other"
target: 187
title: ""
url: ""
因此,可以通过事件中传回的 canGoBack
值判断此时 WebView 是否可以返回,如果可以则使用 WebView 的 ref 去调用 goBack()
返回上一页,否则使用 Actions.pop()
退出当前页面。
基本代码如下:
goBack = () => {
if (this.state.canGoBack) {
this.refs.webView.goBack();
} else {
Actions.pop();
}
};
当然由于返回按钮可能不在当前组件下,如果你使用的是 react-native-router-flux
和 react-redux
,则可以定义一个返回按钮的组件,该组件通过全局 state 树接收点击事件的 function,然后设置到 Scene
的 renderLeftButton
属性中,最后,在需要处理 WebView 的地方设置返回事件到返回按钮中即可(注意:react-redux
是可以接收 function
作为属性的,不然就没法设置事件了)。
react-native-amap3d
这是一个高德 3D 地图库。
-
安装失败以及冲突
该库 iOS 部分只能使用 CocoaPods 安装,花了很多时间在安装 Specs 依赖上,只要一 pod install
就会卡在 cloning into /Users/xxx/.cocoapods/repo/master
。
尝试了很多国内镜像,发现大多数都已经不能用了,然后也尝试了国内镜像 clone Specs 到 ~/.cocoapods/repos/master
里,发现也没啥用,因为 install 的时候还是会识别不了(因为太久没更新)。
后来发现原来这东西的确只能从官方的 Github 上 clone,只不过由于体积比较大(几个G,被限速了),只要耐心等待它 clone 完毕就行了。反正每台电脑只要操作一次就够了,后面的更新基本都很快的。
花了一晚上 clone 完,然后 install,结果编译还是不通过,老是报依赖找不到(ld: library not found for -lDoubleConversion)。尝试各种方案无果,后来看到一个 stackoverflow 上的回答才明白,原来使用 cocoapods 的项目要通过 .xcworkspace
打开项目而不是 .xcodeproj
。发现真相的我差点眼泪掉下来。
然后重新打开项目跑了之后,依旧继续报错(duplicate symbol _aes_encrypt_key128 in ios/Pods/AMap3DMap/MAMapKit.framework/MAMapKit)。
清理了缓存(npm cache clear --force ; watchman watch-del-all ; rm -rf $TMPDIR/react-* ; rm -rf ~/.rncache)重新安装依赖(rm -rf node_modules && npm install)后,问题依旧存在。
你以为我要崩溃了吗?不,作为一个程序员怎么能这么容易就崩溃?当然是去玩几把 FIFA 然后回来继续解决问题啊。
仔细看了下报错信息,发现是 react-native-amap3d 和 react-native-update 中的 libRCTHotUpdate 存在重复 symbol 所以一直编译不过。
确定问题后解决起来就简单了,搜到了这篇文章。原理很简单,把 react-native-amap3d 中会造成 symbol 重复的内容去掉就可以了(因为只是 x86_64 这一个平台中的重复 symbol,所以影响不大,如果是其他平台,操作也类似),具体步骤如下:
首先定位到 MAMapKit.framework,
project_name/ios/pods/AMap3DMap/MAMapKit.framework
,将其中的MAMapKit
复制出来将 MAMapKit 中 x86_64 平台的部分提取出来并命名为 MAMapKit.x86_64:
lipo -thin x86_64 MAMapKit -output MAMapKit.x86_64
- 将其中包含重复 symbols 的部分查找出来并保存的 symbols 文件中:
nm -j MAMapKit.x86_64 | grep aes > symbols
# symbols 文件内容如下
_aes_decrypt
_aes_decrypt_key128
_aes_encrypt
_aes_encrypt_key128
_dsasozkdgmaesfsvzyll
- 解压 MAMapKit.x86_64:
ar -x MAMapKit.x86_64
# 解压后获得三个新文件:
MAMapKit-x86_64-master.o # 存在重复 symbols 的文件
Pods-MAMapKit-dummy.o
__.SYMDEF SORTED # ar 命令生成的文件索引
- 将 symbols 文件中的符号列表从 MAMapKit-x86_64-master.o 文件中全部删除掉,并生成一个新的文件 MAMapKit-x86_64-master.o.strip
ld -x -r -unexported_symbols_list symbols MAMapKit-x86_64-master.o -o MAMapKit-x86_64-master.o.strip
- 将去除了重复 symbols 的文件重新打包并命名为 MAMapKit.x86_64_solved:
ar -r MAMapKit.x86_64_solved MAMapKit-x86_64-master.o.strip Pods-MAMapKit-dummy.o
- 使用无重复的 x86_64 平台包替换旧的,然后合成新的 MAMapKit:
lipo MAMapKit -replace x86_64 MAMapKit.x86_64_solved -output MAMapKitNew
复制 MAMapKitNew 到 MAMapKit.framework 中,删除原来的 MAMapKit 然后重命名 MAMapKitNew 为 MAMapKit,大功告成!
Xcode 重新编译,编译通过,项目成功运行!
-
在安卓上存在无法获取地图定位的问题。
尤其是当处于室内的时候,这种情况出现得比较频繁(定位后的回调中数据均为 0)。该库作者目前也没有较好的解决方案,所以最后决定在安卓上自己来定时刷新定位,如下:
{
if (Platform.OS === 'ios') {
this.setState({
centerLocation: {
latitude: nativeEvent.latitude,
longitude: nativeEvent.longitude
}
});
}
}}>
{
// 安卓上使用 marker 作为用户位置的标记
Platform.OS === 'android' ?
(
)}
coordinate={{
latitude: this.state.userLocation.latitude,
longitude: this.state.userLocation.longitude,
}}
/> : null
}
我们只在 iOS 上使用地图自带的定位功能,而在安卓上添加一个 marker 作为用户位置,并且自己控制定位频率:
componentDidMount() {
// 进入地图后刷新定位
this.getLocation();
// 安卓上定时刷新定位
if (Platform.OS === 'android') {
this.refershInterval = setInterval(() => this.getLocation(), 60 * 1000);
}
}
getLocation(refresh) {
Platform.select({
android: async () => {
let hasAccess = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
if (hasAccess) {
if (refresh) {
ToastAndroid.show('刷新定位...', ToastAndroid.SHORT);
}
navigator.geolocation.getCurrentPosition(
(position) => this.geo_success(position),
(e) => this.geo_error(e), {timeout: 5000});
} else {
this.requestLocationPermission();
}
}
})();
}
geo_success(position) {
// 将 GPS 坐标转换为国测局坐标
let gcj02Location = wgs84togcj02(position.coords.longitude, position.coords.latitude);
this.setState({
userLocation: {
latitude: gcj02Location[1],
longitude: gcj02Location[0]
}
})
}
geo_error() {
ToastAndroid.show('获取定位失败,请稍候再试!', ToastAndroid.SHORT);
}
async requestLocationPermission() {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
this.getLocation();
} else {
ToastAndroid.show('未获得授权,无法显示您的位置!', ToastAndroid.SHORT);
}
}
componentWillUnmount() {
// 清除定时器
this.refershInterval && clearInterval(this.refershInterval);
}
这种方式唯一的不足就是定位的图标不是动态的了,比如精度范围和方向都无法显示出来。但是为了获取到定位,这也算是一种 trade off 了吧。
其他
代码规范
-
一个花括号
{}
引发的惨案。
你有没有在测试安卓的时候,遇到过这个报错:Cannot add a child that doesn't have a YogaNode to a parent without a measure function! (Trying to add a 'RCTRawText [text: }]' to a 'RCTView')
这么一点点信息,完全让人摸不着头脑对不对?最后折腾半天,发现原来是因为某个角落里躺着一个 {
或者 }
,砸电脑的心都有了。
所以,一定要注意代码规范啊, 不要在代码里乱嵌套代码或者乱写三目运算来判断某个 View 的显示或隐藏等等。
iOS 相关
模拟器调试
大部分情况下,如果在模拟器上跑不起来,只要关掉 Metro Bundler,然后使用 Xcode Clean 再重新运行就可以解决,有时候在执行了 Clean 之后可能会遇到 React 相关的 Target 构建报错,一般是依赖树没有构建完整,只要多执行几次 Run 等待依赖全部加载完毕就能解决。
如果还不行可以尝试卸载模拟器上的应用然后重新构建,也能解决很多奇怪的运行不起来的问题。
实在不行再尝试『全清』大法:
# 清除依赖,重新安装
rm -rf node_modules && npm install
# 清除缓存
react-native start --reset-cache
# 清除 npm 和 watchman 缓存
npm cache clear --force && watchman watch-del-all
相关 issue:issues/1924,issues/4968
真机调试
大部分问题由于项目依赖的运行环境不一样,所以也没有什么统一的解决方案,只能靠自己谷歌慢慢摸索解决,使用上面模拟器中同样的方式有时候也能解决绝大部分问题。
但是有些问题比较特殊,而且出现次数较多,比如:RN 中 third-party
相关的问题。所以这里顺便记录下。
Build input file double-conversion cannot be found
出现这个错误一般是没有安装 double-conversion 依赖,只要运行下面的命令就能解决:
cd node_modules/react-native/scripts && ./ios-install-third-party.sh && cd ../../../
ios-configure-glog.sh: line 15: ./configure: No such file or directory
和 1 类似,也是比较常见的错误,解决方案:
cd ./node_modules/react-native/third-party/glog-0.3.4 && ../../scripts/ios-install-third-party.sh && cd ../../../../
这里出现报错的是 glog-0.3.4,如果你是其他 third-party 依赖或者其他版本也都可以用这种方法解决。
'config.h' file not found
一般出现在 1 之后,手动运行一下 configure 脚本就能解决:
cd node_modules/react-native/third-party/glog-0.3.4/; ./configure
No member named '__rip' in '__darwin_arm_thread_state64
这个问题一般比较少见,但是我遇到过多次,只出现升级到 Xcode 10 之后而且使用的 RN 版本低于 0.57 的情况下。
造成这种错误一个可能的原因是,由于手动构建 glog,在真机上因为缺乏 arm 平台的相关配置导致出现错误。
我目前参考的是这个已经被关闭的 issue 中的 workaround 来解决这个问题的:
// 在 node_modules/react-native/third-party/glog-0.3.4/src/config.h 下搜索:
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__rip
// 替换为:
#if defined(__arm__) || defined(__arm64__)
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__pc
#else
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__rip
#endif
如作者所说,RN 0.57 之后的 glog-0.3.5 脚本已经兼容了 Xcode 10(不能在 RN<0.57 中直接使用这个脚本),这个问题也就不存在了,所以推荐升级来解决,如果不想升级也可以用他提供的方法来解决。相关的 issue:issues/20774
另外,如果是 No member named '__rip' in '__darwin_i386_thread_state64
,则可以添加
#elif defined(__i386__)
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__eip
修改后的内容为:
/* How to access the PC from a struct ucontext */
#if defined(__arm__) || defined(__arm64__)
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__pc
#elif defined(__i386__)
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__eip
#else
#define PC_FROM_UCONTEXT uc_mcontext->__ss.__rip
#endif
Unknown argument type '**attribute**' in method -[RCTAppState getCurrentappState:error:]
同样只有 rn 0.55.4 或更老的版本才会出现这个问题,相关 issue 见 issues/25138,解决方案在这里:
// open at: node_modules/react-native/React/Base/RCTModuleMethod.mm
static BOOL RCTParseUnused(const char **input)
{
return RCTReadString(input, "__unused") ||
RCTReadString(input, "__attribute__((__unused__))") || // add this line
RCTReadString(input, "__attribute__((unused))");
}
Undefined symbols for architecture x86_6: _OBJC_CLASS_xxxxx
这个问题一般是由 CocoaPods 缓存引起的,解决方法:
# 清除 Xcode 缓存
rm -rf ~/Library/Developer/Xcode/DerivedData
# 删除依赖
cd ios
pod deintegrate && pod cache clean --all
# 重新安装依赖
pod update && pod install
参考:相关 issue
Android 相关
Android 不推荐使用真机调试,原因是 hot reload 常常失效(RN 0.60 以上版本似乎体验改善很多了)。
真机上会遇到的问题和 iOS 差不多,这里就记录一些比较常用的命令吧。
# 推荐添加 ~/Library/Android/sdk/emulator/emulator 到 PATH 中
# 查看所有可用的安卓模拟器
emulator -list-avds
# 启动对应的模拟器
emulator -avd 'avd_name'
# 在指定设备上运行
react-native run-android --deviceId=DEVICE_ID
# 运行并配置端口号(默认为8081,避免占
react-native run-android --port=8088
# 运行 release 版本,必须配置 signingConfigs
react-native run-android --variant=release
# 唤醒开发者菜单,或者在 Metro 窗口中按 d
adb shell input keyevent KEYCODE_MENU
# 查看 RN 相关的日志
adb shell logcat *:S ReactNative:V ReactNativeJS:V
另外提供 iOS 相关的常用命令:
# 查看已安装的 iOS 模拟器列表
xcrun simctl list --json devices
# 启动指定的模拟器
xcrun instruments -w 'iPhone 8 Plus'
# 在指定的模拟器上运行
react-native run-ios --simulator="iPhone 8 Plus"
# iOS 模拟器录屏,保存位置为当前路径
xcrun simctl io booted recordVideo video.mov
# 删除所有的 Provisioning Profile
rm ~/Library/MobileDevice/Provisioning\ Profiles/*
# 删除 DerivedData,有时候可以解决一些奇怪的问题
rm -rf ~/Library/Developer/Xcode/DerivedData
其它问题
- 无法连接到 packager server,相关 issue:issue-15388,issue-23380
通常是由于在 MainApplication
中导入了 BuildConfig
造成的,删除即可,见:Can't open developer menu on react-native debug build
- Attempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)' on a null object reference
算是 Android 上比较令人头疼的一个问题了,目前除了重启 packager 没有更好的解决办法了,相关 issue 见 issue-17530,该 issue 目前仍然处于 open 状态中。