React Native Android 项目搭建
·
基于真实环境编写:Apple Silicon Mac + Android Studio / SDK 37 / JDK 17(brew formula 版)+ RN 0.88.1,2026-10 在 Android 16 (API 37) 模拟器上端到端实测跑通。
目录
2、★把 Gradle 下载源换成腾讯镜像(Android 侧第一必做)
5、★手动启动 Metro(Android 侧必踩的静默坑)
6、★★ 授权本地网络访问(Android 16 / API 36+ 模拟器专属,最大暗坑)
3、列表页(网络请求 + 三态 + FlatList + 下拉刷新)
4、详情页(参数传递 + ★物理返回键拦截 BackHandler)
5、我的页(★运行时权限申请 PermissionsAndroid + 持久化)
一、环境检验
1、检验 JDK 17
java -version
# 期待 openjdk version "17.x.x"
echo $JAVA_HOME
# 期待 /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
- 原因:Android Gradle Plugin 8+ 强制要求 JDK 17,高了低了都不行。
JAVA_HOME是 Gradle 找 JDK 的方式(JAVA在 PATH 里但 JAVA_HOME 空着,命令行能跑、Gradle 构建照样崩)。 - 问题:
command not found: JAVA或 JAVA_HOME 为空 →source ~/.zshrc;还没配就按「Android 环境篇」补 brewopenjdk@17+ 环境变量。- 显示 21/24 等其他版本 → 用
/usr/libexec/java_home -V看装了哪些,确认 JAVA_HOME 指向 17。
2、检验 Android SDK 与命令行工具
echo $ANDROID_HOME
# 期待 /Users/你/Library/Android/sdk
adb version
# 期待有输出(Android Debug Bridge version 1.0.41)
ls $ANDROID_HOME
# 期待看到 platforms / build-tools / platform-tools / emulator
- 原因:
ANDROID_HOME是 RN CLI 定位 SDK 的唯一途径;adb负责"找到设备→装 APK→转发日志",是 Android 开发的瑞士军刀。 - 问题:
- 都为空 → 环境变量没配/没生效:确认
~/.zshrc里有 ANDROID_HOME 和 PATH 两段,然后source ~/.zshrc重开终端。 adb version报 version 为installed as 'android-adb'→正常,不影响。
- 都为空 → 环境变量没配/没生效:确认
3、检验模拟器与 AVD
emulator -list-avds
# 期待列出至少一个(如 Medium_Phone_API_37.0)
ls $ANDROID_HOME/platforms
# 看有 android-36/37
ls $ANDROID_HOME/build-tools
# 看有 36/37
- 原因:AVD(Android Virtual Device)= 一台虚拟手机的完整配置;platforms/build-tools 是编译目标。Apple Silicon 的系统镜像必须是 arm64(x86 镜像会卡成 PPT 或起不来)。
- 问题:
- 列表为空 → Android Studio → Device Manager → Create Device(镜像选带 arm64 标签的)。
- 有 AVD 但
emulator命令找不到 → PATH 里少了$ANDROID_HOME/emulator。
4、总检(可选)
cd 任意 RN 项目目录 && npx react-native doctor
- 原因:一键扫 Android + iOS 全部依赖项。
- 问题:个别项误报(尤其 JDK),以步骤 1~3 原始命令输出为准。
二、项目搭建
1、创建项目并安装依赖
mkdir -p ~/rn-playground && cd ~/rn-playground
npx @react-native-community/cli@latest init RnAndroidDemo --skip-install
cd RnAndroidDemo && npm install
# npmmirror 源下约 1 分钟
- 方式:同 iOS 篇——CLI 生成模板(自带
android/完整 Gradle 工程),--skip-install分离依赖安装。 - 原因:
android/目录含 gradle 配置、AndroidManifest.xml(权限声明处)、原生入口 MainActivity —— Android 侧"原生半身"由 Gradle 管理(对应 iOS 的 CocoaPods)。 - 问题:
- 模板/依赖拉取慢 → 确认
npm config get registry为 npmmirror。 - 项目名必须大驼峰字母开头
- 模板/依赖拉取慢 → 确认
2、★把 Gradle 下载源换成腾讯镜像(Android 侧第一必做)
cd android
sed -i '' 's|services.gradle.org/distributions|mirrors.cloud.tencent.com/gradle|' \
gradle/wrapper/gradle-wrapper.properties
grep distributionUrl gradle/wrapper/gradle-wrapper.properties # 确认已指向腾讯镜像
cd ..
- 原因:首次构建要下载 Gradle 发行版(一百多 MB),默认源
services.gradle.org国内极慢。每个新项目都要改一次(这是项目内文件,不随环境走)。 - 问题:
- 忘了改 →
npm run android长时间卡在Downloading gradle-x.x.x,Ctrl+C 后改好重跑即可(已下载部分有缓存)。 - 改错了 URL(版本号对不上)→ wrapper 报 404,核对镜像路径里的版本号与原文件完全一致。
- 忘了改 →
3、启动模拟器
# 方式 A(小白推荐):Android Studio → Device Manager → ▶
# 方式 B:命令行
emulator -avd 你的AVD名
- 原因:
npm run android不会自动帮你开模拟器(iOS 的 run-ios 会),Android 侧必须先有在线设备。 - 问题:
adb devices列表空 → 模拟器没开完(冷启动 10~60 秒),等它出现锁屏界面再执行adb devices,显示emulator-xxxx device(注意状态必须是device,offline还没就绪)。- 模拟器莫名自己关了 → 用命令行后台方式启动(防终端会话结束连坐):
-
nohup emulator -avd 你的AVD名 > /tmp/emulator.log 2>&1 &
4、首次构建与安装
cd ~/rn-playground/RnAndroidDemo
npm run android
- 方式:一条命令完成「编译 Gradle 插件 → 构建 APK → adb 安装 → 拉起 App」。
- 原因:Android 构建链是 Gradle(对比 iOS 是 Xcodebuild),理解这条命令分四步走,出问题才能定位在哪一段。
- 问题(重要,都是实测):
- 首次构建约 20 分钟完全正常:过程中会自动下载 NDK(约 600MB)、CMake、新版 Build-Tools,还要编译四套 CPU 架构的 C++ 代码。判断死活:
ps aux | grep -i gradle | grep -v grep,CPU 不为 0 就是在干活。第二次起就是秒级~分钟级增量构建。 - 构建报 JDK 相关错(
Unsupported class file major version等)→ 回步骤 1,JAVA_HOME 指向了非 17 的 JDK。 - 构建报 SDK location not found → ANDROID_HOME 没生效。
- 成功标志:
BUILD SUCCESSFUL+Installed on 1 device。
- 首次构建约 20 分钟完全正常:过程中会自动下载 NDK(约 600MB)、CMake、新版 Build-Tools,还要编译四套 CPU 架构的 C++ 代码。判断死活:
5、★手动启动 Metro(Android 侧必踩的静默坑)
# 新开一个终端
cd ~/rn-playground/RnAndroidDemo
npm start
- 原因:
npm run android正常会顺手开新终端窗口跑 Metro,但在很多终端环境下这一步静默失败(日志里只有一行小字error Cannot start server in new windows because no terminal app was specified),结果是装了个"没有粮食的空壳 App"——必红屏。养成习惯:Metro 永远自己开专属终端跑,全程别关。 - 验收:
curl http://localhost:8081/status→packager-status:running。 - 问题:
Port 8081 already in use→ 上次的 Metro 没退:lsof -ti:8081 | xargs kill -9。
6、★★ 授权本地网络访问(Android 16 / API 36+ 模拟器专属,最大暗坑)
如果你的模拟器系统是 Android 16(API 36+),App 会红屏报 Unable to load script / Make sure you're running Metro,但检查一切"正常"——执行这条:
adb shell appops set com.rnandroiddemo ACCESS_LOCAL_NETWORK allow
然后重启 App:
adb shell am force-stop com.rnandroiddemo
adb shell am start -n com.rnandroiddemo/.MainActivity
- 原因:Android 16 新增「本地网络访问」权限(ACCESS_LOCAL_NETWORK)。RN Debug 包要从
10.0.2.2(模拟器访问宿主 Mac 的专用别名)加载 JS,这个地址被系统划为本地网络——未授权时访问被静默丢弃,表现为超时,无任何弹窗和报错。这是 2026 年新建模拟器默认 API 36+ 后出现的新坑,网上资料极少。 - 判断你是否中招(可选,学排查思路):
# App 视角探测 Metro 是否在线:6 秒后返回 false 且 shell 手测网络是通的 = 中招
adb logcat -d | grep isMetroRunning | tail -1
# 模拟器 shell 手测(返回 HTTP/1.1 200 OK 说明网络本身没问题):
adb shell 'printf "GET /status HTTP/1.1\r\nHost: 10.0.2.2:8081\r\n\r\n" | nc -w 5 10.0.2.2 8081'
# 查权限授予状态:
adb shell dumpsys package com.rnandroiddemo | grep ACCESS_LOCAL_NETWORK
- 问题:
- 每装一个新 App / 重装 App 都要重新执行一次(按包名生效);Android 15 及以下模拟器无此坑。
- 执行后没变化 → 确认包名写对(在
android/app/build.gradle的applicationId里看),并确认 Metro 真的在跑(步骤 二、5 验收)。
7、处理「附近设备」权限弹窗
adb shell pm grant com.rnandroiddemo android.permission.BLUETOOTH_SCAN
- 原因:RN 新模板依赖带的蓝牙权限在 Android 12+ 是运行时权限,App 每次启动都弹「允许查找并连接到附近设备?」,弹窗期间 App 不继续加载 bundle,极易误判成"App 坏了"。
- 问题:就算点过 Don't allow 下次还弹(模板每次启动都请求),
pm grant一次永久清净。开发用不上蓝牙,放心授。
✅ 阶段验收:模拟器显示 RN 欢迎页(Welcome to React Native);改 App.tsx 保存秒级热更新;adb logcat -d | grep ReactNativeJS | tail -1 能看到 Running "RnAndroidDemo"。
三、Demo 功能页面搭建
1、安装导航与存储依赖
npm install @react-navigation/native @react-navigation/native-stack \
@react-navigation/bottom-tabs \
react-native-screens \
react-native-safe-area-context \
@react-native-async-storage/async-storage
# 这些含原生代码,但 Android 侧 autolinking 自动处理,无需像 iOS 那样手动 pod install!
- 原因:React Navigation 全家桶 + AsyncStorage(同 iOS 篇)。Android 与 iOS 的关键差异:RN 在 Android 用 autolinking(Gradle 自动扫描 node_modules 里的原生库),不需要手动执行任何"原生依赖安装"命令——这是 Android 侧比 iOS 省心的一点。
- 问题:
- 新装原生库后构建报
Could not resolve ...或运行红屏缺模块 → 先彻底重建: -
cd android && ./gradlew clean && cd .. npm run android - Gradle 偶发缓存抽风(报错与新增依赖相关的诡异错)→
cd android && ./gradlew --stop && rm -rf .gradle build app/build && cd ..再跑。
- 新装原生库后构建报
2、导航骨架(与 iOS 同构,贴 Android 配色)
// src/navigation.tsx
import { NavigationContainer, DarkTheme } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { Text } from 'react-native'
import HomeScreen from './screens/HomeScreen'
import DetailScreen from './screens/DetailScreen'
import ProfileScreen from './screens/ProfileScreen'
import type { User } from './types'
export type RootStackParamList = {
Home: undefined
Detail: { user: User }
}
export type MainTabParamList = { 首页: undefined; 我的: undefined }
const Stack = createNativeStackNavigator<RootStackParamList>()
const Tab = createBottomTabNavigator<MainTabParamList>()
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} options={{ title: '用户列表' }} />
<Stack.Screen name="Detail" component={DetailScreen} options={{ title: '用户详情' }} />
</Stack.Navigator>
)
}
export default function AppNav() {
return (
<NavigationContainer theme={DarkTheme}>
<Tab.Navigator screenOptions={{ tabBarActiveTintColor: '#4F9CF9', headerShown: false }}>
<Tab.Screen name="首页" component={HomeStack}
options={{ tabBarIcon: () => <Text style={{ fontSize: 18 }}>👥</Text> }} />
<Tab.Screen name="我的" component={ProfileScreen}
options={{ tabBarIcon: () => <Text style={{ fontSize: 18 }}>👤</Text>, headerShown: true, headerTitle: '我的' }} />
</Tab.Navigator>
</NavigationContainer>
)
}
// App.tsx
import AppNav from './src/navigation'
export default function App() {
return <AppNav />
}
// src/types.ts
export interface User {
id: number
name: string
email: string
phone: string
website: string
company: { name: string }
}
- 原因:结构说明同 iOS 篇(Tab 套 Stack、泛型 ParamList 类型安全)。Android 细节:底部 Tab 的样式由系统 Material 规范渲染,按压有原生 ripple 水波纹——这是 Android 平台默认行为,无需额外配置。
- 问题:红屏缺原生模块 → 步骤 12 的 clean 重建。
3、列表页(网络请求 + 三态 + FlatList + 下拉刷新)
// src/screens/HomeScreen.tsx —— 与 iOS 版完全一致(这正体现 RN 跨端价值)
import { useState, useCallback } from 'react'
import { View, Text, FlatList, TouchableOpacity, RefreshControl, ActivityIndicator, StyleSheet } from 'react-native'
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
import type { RootStackParamList } from '../navigation'
import type { User } from '../types'
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>
export default function HomeScreen({ navigation }: Props) {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const load = useCallback(async () => {
try {
setError(null)
if (users.length === 0) setLoading(true)
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
setUsers(await res.json())
} catch (e) {
setError(e instanceof Error ? e.message : '网络异常')
} finally {
setLoading(false)
}
}, [users.length])
if (loading)
return <View style={s.center}><ActivityIndicator size="large" /><Text>加载中…</Text></View>
if (error)
return (
<View style={s.center}>
<Text style={s.err}>{error}</Text>
<TouchableOpacity style={s.btn} onPress={load}><Text>重试</Text></TouchableOpacity>
</View>
)
return (
<FlatList
data={users}
keyExtractor={(item) => String(item.id)}
refreshControl={<RefreshControl refreshing={false} onRefresh={load} />}
renderItem={({ item }) => (
<TouchableOpacity style={s.card} onPress={() => navigation.navigate('Detail', { user: item })}>
<Text style={s.name}>{item.name}</Text>
<Text style={s.sub}>{item.company.name}</Text>
</TouchableOpacity>
)}
/>
)
}
const s = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 },
err: { color: '#ff6b6b' },
btn: { padding: 10, backgroundColor: '#4F9CF9', borderRadius: 8 },
card: { padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#333' },
name: { fontSize: 16, fontWeight: '600', color: '#fff' },
sub: { fontSize: 13, color: '#888', marginTop: 4 },
})
- 原因:这份代码与 iOS 版零差异可直接复用——RN "Learn once, write anywhere" 的实证;面试可强调:FlatList 视口回收(对应 Android RecyclerView)、三态设计、useCallback 防重渲染。
- 问题(Android 特有):
Network request failed→ 模拟器网络继承 Mac,但 Android 侧多一层坑:若宿主 Metro 不可达(步骤 9/10 的坑),fetch 请求也会失败——Android 上"网络请求失败"有时根因是本地网络权限而不是网络。- 列表底部被导航栏遮挡 →
FlatList加contentContainerStyle={{ paddingBottom: 16 }}。
4、详情页(参数传递 + ★物理返回键拦截 BackHandler)
// src/screens/DetailScreen.tsx
import { useEffect } from 'react'
import { View, Text, ScrollView, TouchableOpacity, BackHandler, Linking, Alert, StyleSheet } from 'react-native'
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
import type { RootStackParamList } from '../navigation'
type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>
export default function DetailScreen({ route, navigation }: Props) {
const { user } = route.params
// ★ Android 特色:拦截物理返回键
useEffect(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
Alert.alert('确认离开', '确定要返回列表页吗?', [
{ text: '留下', style: 'cancel' },
{ text: '返回', style: 'destructive', onPress: () => navigation.goBack() },
])
return true // ★ 返回 true 表示"已消费",阻止默认行为;false/不返回则执行默认返回
})
return () => sub.remove() // ★ 卸载时必须移除,否则回到列表页返回键仍被拦截(高频内存泄漏点)
}, [navigation])
const Row = ({ label, value }: { label: string; value: string }) => (
<View style={s.row}>
<Text style={s.label}>{label}</Text>
<Text style={s.value}>{value}</Text>
</View>
)
return (
<ScrollView contentContainerStyle={s.wrap}>
<View style={s.avatar}><Text style={s.avatarText}>{user.name[0]}</Text></View>
<Text style={s.name}>{user.name}</Text>
<Row label="公司" value={user.company.name} />
<Row label="邮箱" value={user.email} />
<Row label="电话" value={user.phone} />
<Row label="网站" value={user.website} />
<TouchableOpacity style={s.btn} onPress={() => Linking.openURL(`mailto:${user.email}`)}>
<Text style={s.btnText}>发邮件</Text>
</TouchableOpacity>
</ScrollView>
)
}
const s = StyleSheet.create({
wrap: { padding: 20 },
avatar: { width: 72, height: 72, borderRadius: 36, backgroundColor: '#4F9CF9', alignSelf: 'center', justifyContent: 'center' },
avatarText: { fontSize: 30, color: '#fff', textAlign: 'center' },
name: { fontSize: 22, fontWeight: '700', textAlign: 'center', marginVertical: 16, color: '#fff' },
row: { flexDirection: 'row', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#333' },
label: { width: 60, color: '#888' },
value: { flex: 1, color: '#fff' },
btn: { marginTop: 24, padding: 14, backgroundColor: '#4F9CF9', borderRadius: 10, alignItems: 'center' },
btnText: { color: '#fff', fontWeight: '600' },
})
- 原因(面试高频):
BackHandler是 Android 独有 API(iOS 没有物理返回键,手势返回由 native-stack 原生处理)。返回true= 拦截、false= 放行默认行为,经典场景就是"表单未保存拦截返回"。return () => sub.remove():不清理的话,离开页面后监听器仍挂着(闭包持有 navigation),列表页按返回键会再次弹确认框——事件监听泄漏是 RN Android 面试必问。
- 问题:
- 真机上返回键直接退出没弹窗 → 监听器返回了
false或undefined(默认放行),确认写的是return true。 - iOS 上跑这份代码 →
hardwareBackPress静默不触发(平台专属事件),无需做平台判断,非目标平台自动失效。
- 真机上返回键直接退出没弹窗 → 监听器返回了
5、我的页(★运行时权限申请 PermissionsAndroid + 持久化)
// src/screens/ProfileScreen.tsx
import { useState, useEffect } from 'react'
import { View, Text, TextInput, Switch, Button, StyleSheet, Alert, PermissionsAndroid, Platform } from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
const KEY = 'profile'
// ★ Android 特色:运行时权限申请(危险权限需弹窗征求用户同意)
async function requestNotifyPermission(): Promise<boolean> {
// Android 13+ 通知属于运行时权限;低版本自动授权
if (Platform.OS !== 'android' || Platform.Version < 33) return true
const res = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
{
title: '通知权限',
message: '开启后才能收到消息提醒',
buttonPositive: '开启',
buttonNegative: '取消',
},
)
return res === PermissionsAndroid.RESULTS.GRANTED
}
export default function ProfileScreen() {
const [nickname, setNickname] = useState('')
const [notify, setNotify] = useState(true)
const [loading, setLoading] = useState(true)
useEffect(() => {
AsyncStorage.getItem(KEY)
.then((raw) => { if (raw) { const d = JSON.parse(raw); setNickname(d.nickname); setNotify(d.notify) } })
.finally(() => setLoading(false))
}, [])
const toggleNotify = async (v: boolean) => {
if (v) {
const granted = await requestNotifyPermission()
if (!granted) { Alert.alert('未授权', '没有通知权限,无法开启'); return }
}
setNotify(v)
}
const save = async () => {
await AsyncStorage.setItem(KEY, JSON.stringify({ nickname, notify }))
Alert.alert('已保存', `昵称:${nickname || '(空)'},通知:${notify ? '开' : '关'}`)
}
if (loading) return null
return (
<View style={s.wrap}>
<Text style={s.title}>偏好设置</Text>
<Text style={s.label}>昵称</Text>
<TextInput style={s.input} value={nickname} onChangeText={setNickname}
placeholder="输入昵称" placeholderTextColor="#666" />
<View style={s.row}>
<Text style={s.label}>接收通知</Text>
<Switch value={notify} onValueChange={toggleNotify} />
</View>
<Button title="保存" onPress={save} color="#4F9CF9" />
<Text style={s.hint}>首次开启通知会弹系统权限框;数据存本地(AsyncStorage),杀 App 重开仍在</Text>
</View>
)
}
const s = StyleSheet.create({
wrap: { flex: 1, padding: 20 },
title: { fontSize: 22, fontWeight: '700', marginBottom: 20, color: '#fff' },
label: { color: '#aaa', marginBottom: 6 },
input: { borderWidth: 1, borderColor: '#444', borderRadius: 8, padding: 12, color: '#fff', marginBottom: 20 },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 },
hint: { marginTop: 16, color: '#666', fontSize: 12 },
})
- 原因(面试高频):
- Android 权限模型:普通权限(如 INTERNET)装时自动授予;危险权限(相机/定位/通知等)必须运行时弹窗征求同意。
PermissionsAndroid.request是 RN 官方申请入口——这是与 iOS(Info.plist 声明 + 系统首次自动弹)最大的流程差异。 Platform.OS/Platform.Version平台分支:跨端代码的标准写法,面试常问"怎么写平台差异化代码"。- AsyncStorage 持久化与受控组件说明同 iOS 篇。
- Android 权限模型:普通权限(如 INTERNET)装时自动授予;危险权限(相机/定位/通知等)必须运行时弹窗征求同意。
- 问题:
- 弹窗不出现直接失败 → 模拟器 API < 13 不适用(代码已用 Version 守卫);或该权限曾被用户永久拒绝(选了"不再询问")——
request直接返回 never_ask_again,此时应引导去系统设置:Linking.openSettings()。 - AndroidManifest 没声明权限会闪退:
PermissionsAndroid申请的权限必须在android/app/src/main/AndroidManifest.xml里有对应<uses-permission>(通知权限模板已内置;其他权限如相机要自己加)——清单声明是"资格",运行时申请是"使用",缺一不可。
- 弹窗不出现直接失败 → 模拟器 API < 13 不适用(代码已用 Version 守卫);或该权限曾被用户永久拒绝(选了"不再询问")——
6、运行验证
# 模拟器在跑、Metro 在跑的前提下
npm run android
✅ 终极验收清单:
- 底部双 Tab 正常切换,按压有原生水波纹
- 列表自动加载 10 个用户,下拉出现刷新指示器(Android 是圆形转圈)
- 进详情页 → 按模拟器侧边的返回键(或 Esc 键)→ 弹出"确认离开"框,选"返回"回列表
- 我的页:开通知开关 → 弹系统权限框 → 允许后开关生效
- 改昵称保存 → 模拟器上划杀掉 App 重开 → 数据还在
- 改代码保存秒级热更新
四、调试能力(Android 模拟器专属)
| 操作 | 方式 | 说明 |
| 打开 RN 调试菜单 | 模拟器获得焦点按 ⌘ + M(或 Ctrl+M) | Android 对应 iOS 的 ⌘D |
| 重新加载 JS | 调试菜单 → Reload(或终端 Metro 按 r) | |
| 查看 console.log | 终端 Metro 窗口 或 adb logcat -s ReactNativeJS | logcat 是 Android 日志总闸,-s ReactNativeJS 只看 RN JS 层日志——排查 JS 问题第一现场 |
| 调试 JS 断点 | 调试菜单 → Open Debugger | Hermes 原生调试器(Chrome DevTools 协议) |
| 检查 UI 布局 | 调试菜单 → Toggle Inspector | |
| 录屏/截图 | adb exec-out screencap -p > /tmp/x.png | 给同事/面试演示素材 |
- 问题:
- ⌘M 没反应 → 焦点不在模拟器窗口;或用
adb shell input keyevent 82(菜单键)代打。 - logcat 噪音巨大 → 必须
-s过滤标签,或adb logcat -d --pid=$(adb shell pidof com.rnandroiddemo)按进程过滤。 - 热更新失效 → Metro 死了(重跑
npm start)或 watchman 故障(watchman shutdown-server后重装)。
- ⌘M 没反应 → 焦点不在模拟器窗口;或用
附录 A:Android 真机调试(比 iOS 简单得多)
- 手机 → 设置 → 关于手机 → 连点「版本号」7 次开启开发者模式 → 开发者选项 → 打开 USB 调试
- 数据线连 Mac(弹"允许 USB 调试"选允许)→
adb devices显示设备 - ★关键一步(真机必需):adb reverse tcp:8081 tcp:8081,然后照常
npm run android(或直接在手机上打开已装的 App) - 无线调试(可选):开发者选项 → 无线调试 →
adb pair IP:端口配对后 connect
- 原因:真机的
localhost是手机自己,不是你的 Mac。adb reverse把"手机 8081 端口"反向代理到"电脑 8081",Metro 才够得着——不执行这条,真机上必红屏 Unable to load script(模拟器不需要,它走 10.0.2.2)。真机(Android 16)同样可能需要步骤 10 的 ACCESS_LOCAL_NETWORK 授权——访问的宿主机局域网 IP 也属本地网络。 - 问题:
adb devices显示unauthorized→ 手机上没点允许弹窗,重新插拔。- 显示设备但装不上 → 小米/华为等国产 ROM 有"USB 安装"独立开关,去开发者选项打开。
附录 B:Android 侧高频报错速查(实战浓缩)
| 报错/现象 | 根因 | 一条命令/操作 |
构建卡 Downloading gradle | gradle.org 慢 | wrapper 换腾讯镜像 |
| Unsupported class file major version | JDK 不是 17 | JAVA_HOME 指向 openjdk@17 |
| SDK location not found | ANDROID_HOME 未生效 | source ~/.zshrc 重开终端 |
Installed on 0 devices / 构建后没反应 | 模拟器没开/没就绪 | adb devices 确认状态为 device |
红屏 Unable to load script(Metro 明明在跑) | Android 16 本地网络权限(API 36+) | adb shell appops set 包名 ACCESS_LOCAL_NETWORK allow 后重启 App |
| Metro 根本没起 | CLI 静默失败 | 自己开终端 npm start,curl localhost:8081/status 验证 |
| 每次 App 启动弹"附近设备"权限 | BLUETOOTH_SCAN 运行时权限 | adb shell pm grant 包名 android.permission.BLUETOOTH_SCAN |
| 真机红屏连不上 Metro | 缺端口反向代理 | adb reverse tcp:8081 tcp:8081 |
| 新装原生库后构建/红屏异常 | Gradle 缓存旧 | cd android && ./gradlew clean 重建 |
| Port 8081 already in use | 旧 Metro 未退 | lsof -ti:8081 | xargs kill -9 |
| 红屏 Text strings must be rendered… | 裸文本没包 <Text> | 所有文本包 <Text>(Web 转 RN 第一坑) |
| 通知权限申请直接失败且不弹窗 | 用户曾选"不再询问" | 引导 Linking.openSettings() |
更多推荐

所有评论(0)