How to Stop Your Android App from Draining the Battery with Wake Locks
TL;DR · AI 摘要
How to Stop Your Android App from Draining the Battery with Wake Locks October 1, 2026 / Android Nikheel Vishwas Savant...
核心要点
- 主题聚焦:How to Stop Your Android App from Draining the B
- 来源:freeCodeCamp.org,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
如何通过唤醒锁防止Android应用耗尽电池
2026年10月1日
/
#Android
Nikheel Vishwas Savant
唤醒锁是Android中最简单的API之一,也是最容易被误用的API之一。获取一个唤醒锁只需要一行代码。如果忘记释放它,可能会让手机的CPU保持运行数小时,整夜耗尽电池,并导致你的应用在Google Play上收到警告标签。
大多数唤醒锁漏洞并非源于对API本身的误解,而是源于普通的控制流问题:在acquire()和release()之间抛出异常、提前返回、从未触发的回调,或引用计数失去平衡。
这些漏洞在代码审查中难以发现,在常规测试中也难以察觉,因为设备通常处于充电状态且保持唤醒。
本文将解释唤醒锁在系统层面的实际作用,逐步讲解应用最常见的唤醒锁泄漏方式,并展示具体的预防和检测模式及工具。同时还会介绍现代API,这些API在大多数情况下消除了手动管理唤醒锁的需求。
目录
- 先决条件
- Android设备休眠时会发生什么
- 唤醒锁的作用
- 唤醒锁的类型
- 如何获取和释放唤醒锁
- 引用计数的工作原理
- 应用泄漏唤醒锁的常见方式
- 防止泄漏的模式
- 完全不需要使用唤醒锁的场景
- 如何检测唤醒锁泄漏
- 如何测试唤醒锁行为
- 框架层以下的唤醒锁
- 总结
先决条件
你应该熟悉使用Kotlin编写Android应用,并了解服务、广播接收器和协程的基础知识。
调试部分还需要安装adb和物理设备。模拟器无法以现实方式模拟电源状态,因此文章后续部分的一些命令在模拟器上会产生误导性结果。
Android设备休眠时会发生什么
当屏幕关闭且CPU没有其他需求时,Android会将应用处理器置于低功耗挂起状态。在此状态下,CPU停止执行指令,大部分RAM保持自刷新状态,只有少数硬件模块(调制解调器、实时时钟、传感器中枢和少量中断控制器)保持活动。设备看起来处于空闲状态,但能在中断到达时快速唤醒。
Android使用一种称为autosleep的机制。内核会在没有保持唤醒的理由时尽快挂起系统。任何需要持续运行CPU的任务必须通过持有唤醒源(内核术语,对应Android中的唤醒锁)明确声明。只要至少有一个唤醒源处于活动状态,内核就会拒绝挂起。
屏幕关闭
|
v
有任何活动的唤醒源? == 是 ==> CPU保持运行
|
否
|
v
内核挂起CPU
|
v
中断(闹钟、网络数据包、按键按下)
|
v
CPU恢复,驱动程序或框架获取唤醒锁以处理事件上图展示了屏幕关闭时内核反复做出的决策。如果存在任何唤醒源,CPU会持续运行。如果没有唤醒源,系统会挂起直到硬件中断到达。当设备恢复运行时,处理中断的代码通常会获取一个短暂的唤醒锁,以便在内核再次尝试挂起前完成工作。
关键细节在于保持唤醒是需要主动申请的。当屏幕关闭时,除非有组件代为持有唤醒锁,否则你的代码无法获得CPU时间。
唤醒锁的作用机制
应用层级的唤醒锁是对PowerManagerService(运行在system_server中的系统服务)发出的请求。当你的应用调用acquire()时,该调用通过Binder传递到该服务,服务会记录你的UID、标签字符串和锁类型等信息。PowerManagerService会聚合所有应用请求,并代表所有请求向内核持有单个唤醒源。当最后一个应用层级的锁被释放时,系统会释放内核唤醒源,设备即可进入休眠状态。
这种设计带来了两个需要理解的后果。首先,系统能精确追踪每个锁的持有者及持续时间,这正是电池统计信息和Play Console健康度指标能够定位问题应用的原理。
其次,PowerManagerService通过Binder死亡通知将每个锁与你的进程关联。如果进程崩溃,系统会自动释放相关锁。因此锁的泄漏范围被限制在进程生命周期内,但那些运行前台服务或被系统主动保活的进程可能持续数小时。
唤醒锁的类型
PowerManager类定义了多个锁级别。其中大部分已被弃用,实际上对应用开发者而言只有一个是重要的。
| 等级 | 保持CPU运行 | 保持屏幕亮起 | 状态 | |------|-------------|---------------|------| | PARTIAL_WAKE_LOCK | 是 | 否 | 当前推荐,大多数应用应使用此类型 | | SCREEN_DIM_WAKE_LOCK | 是(降低亮度) | 是 | 自API 17起弃用 | | SCREEN_BRIGHT_WAKE_LOCK | 是(全亮度) | 是 | 自API 13起弃用 | | FULL_WAKE_LOCK | 是(包括键盘背光) | 是 | | | PROXIMITY_SCREEN_OFF_WAKE_LOCK | 关闭靠近传感器时的屏幕 | 否 | 当前推荐,用于通话界面 |
第一列是传递给newWakeLock()的常量,后两列描述锁保持供电的设备部分。屏幕相关的锁级别被弃用是因为应用在用户离开后仍持续持有,导致屏幕常亮和电池耗电。
替代方案是使用FLAG_KEEP_SCREEN_ON窗口标志(或布局文件中的android:keepScreenOn),窗口管理器会将该标志与窗口可见性绑定,因此不会发生泄漏。靠近传感器锁是通话界面的特殊用例。对于其他场景,现代Android中的"唤醒锁"特指PARTIAL_WAKE_LOCK,本文其余部分将重点讨论该类型。
如何获取和释放唤醒锁
使用唤醒锁需要在清单文件中声明WAKE_LOCK权限。这是一个普通权限,安装时会自动授予,无需用户确认。
<uses-permission android:name="android.permission.WAKE_LOCK" />此声明允许应用持有唤醒锁。缺少该声明时,acquire()会抛出SecurityException。由于权限是静默授予的,用户直到查看电池使用情况才会意识到应用使用了唤醒锁,这也是需要谨慎使用的原因。
以下是基本用法:
val powerManager = context.getSystemService(PowerManager::class.java)
val wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"myapp:upload"
)
wakeLock.acquire(10 * 60 * 1000L) // 10分钟
uploadPendingFiles()
wakeLock.release()代码获取PowerManager系统服务,并使用partial级别和标签创建WakeLock对象。创建对象本身不会产生任何效果。CPU仅在acquire()和release()之间保持唤醒状态。
标签"myapp:upload"会出现在dumpsys、Battery Historian和Play Console中,因此应能同时标识您的应用和具体任务。
Google推荐使用app:component格式,这种格式能在调试时节省真实时间。acquire()的参数是毫秒级超时时间。如果10分钟后锁仍然未释放,系统将为您自动释放。
这段代码看似正确但存在实际错误:如果uploadPendingFiles()抛出异常,release()将永远不会执行。超时机制将损害限制在10分钟内,但这仍然意味着每次失败都会消耗10分钟的电池电量。下一节将解释如何修复此问题以及还存在哪些其他陷阱。
引用计数的工作原理
WakeLock默认采用引用计数机制。每次调用acquire()会增加一个内部计数器,每次调用release()会减少计数器,只有当计数器归零时锁才会被释放。
wakeLock.acquire(60_000L) // count = 1, lock held
wakeLock.acquire(60_000L) // count = 2
wakeLock.release() // count = 1, still held
wakeLock.release() // count = 0, lock released
wakeLock.release() // throws RuntimeException: WakeLock under-locked前两次调用使计数器达到2。第一次release()仅将其减少到1,因此即使代码执行了"release()",锁仍然保持激活状态。
锁实际上在第二次release()时才会被释放。第三次release()时没有剩余的锁可释放,会抛出带有" WakeLock under-locked"消息的RuntimeException。这会导致生产环境崩溃,这也是为什么有些开发者会将release()包裹在try块中或先检查isHeld状态。
当多个独立代码段共享单个WakeLock对象时,引用计数非常有用。当获取和释放次数不匹配时(例如函数被调用两次但完成回调只触发一次),就会出现问题。
可以通过setReferenceCounted(false)关闭引用计数。设置后,任何单次release()都会释放锁,无论之前获取了多少次。当某个组件拥有锁时,这通常是更简单的模型。
应用程序泄露WakeLock的常见方式
WakeLock泄露几乎总是遵循少量模式。识别这些模式会使代码审查更加有效。
异常和提前返回
上面的基本示例是最常见的泄露方式。任何在acquire()和release()之间未执行release()就退出函数的代码路径都会导致泄露。这包括抛出的异常、未注意到锁而添加的return语句,以及循环中的break或continue。
fun syncContacts() {
wakeLock.acquire(5 * 60 * 1000L)
val account = accountStore.current() ?: return
api.pushContacts(account)
wakeLock.release()
}当没有当前账户时,这个函数会泄露锁,因为提前返回跳过了release()。当pushContacts()抛出网络异常时也会泄露。单独看这些路径似乎都无害。超时机制防止了永久泄露,但每次同步失败仍会消耗5分钟的唤醒时间。
超出调用者生命周期的异步工作
WakeLock经常在一处获取并在其他地方的回调中释放。如果回调从未执行,锁将永远不会被释放。
fun startDownload(url: String) {
wakeLock.acquire()
downloader.enqueue(url, object : Callback {
override fun onComplete() {
wakeLock.release()
}
override fun onError(e: Throwable) {
log("download failed", e)
}
})
}这里存在两个错误。错误回调忘记释放锁,导致每次下载失败时都会发生内存泄漏。而且锁的获取完全没有设置超时,因此泄漏会持续到进程终止。
还有一个更隐蔽的风险:如果 downloader 被取消或在未调用任何回调的情况下丢弃请求,锁同样会发生泄漏。任何依赖第三方回调来释放锁的设计都必须设置超时作为安全网。
不平衡的引用计数
当共享的、基于引用计数的锁被获取次数多于释放次数时,即使每条代码路径似乎都释放了锁,锁仍会保持占用状态。
class LocationTracker(private val wakeLock: PowerManager.WakeLock) {
fun onStart() {
wakeLock.acquire(30 * 60 * 1000L)
startUpdates()
}
fun onStop() {
stopUpdates()
wakeLock.release()
}
}这个类看起来是平衡的。当 onStart() 被调用两次(例如配置更改或重复意图后)但 onStop() 只被调用一次时,问题就会出现。计数最终为 1,锁会一直保持到其超时结束。关闭引用计数或通过 if (!wakeLock.isHeld) 保护 acquire 操作,可以修复这个特定的错误。
在广播接收器中启动后台工作
系统会在 onReceive() 运行期间为你持有唤醒锁。一旦 onReceive() 返回,该锁就会被释放,进程可能会被标记为闲置。
一个常见错误是在 onReceive() 中获取一个单独的唤醒锁,启动一个线程,并从线程中释放锁。如果进程被终止或线程卡住,释放操作将永远不会发生。
旧版的 WakefulBroadcastReceiver 辅助类是为这种模式设计的,但由于这些问题现已弃用。现代的替代方案将在本文后面部分介绍。
跨无界操作持有锁
没有超时的网络请求、没有截止时间的 CountDownLatch 等待、以及从蓝牙套接字进行的阻塞读取都是可能永远挂起的操作。如果唤醒锁在这些操作期间被持有,锁也会随之挂起。从技术上讲,代码在每条路径上都会释放锁,但由于操作永远不会完成,释放操作永远无法到达。
防止泄漏的模式
这些泄漏的修复方法归结为几个容易一致应用的习惯。
始终使用超时
每次调用 acquire() 时都应该传递一个超时。Android Lint 通过 WakelockTimeout 检查强制执行此操作,该检查会标记没有参数的 acquire() 调用。
选择一个比工作所需时间明显更长但又足够短的超时,以确保泄漏是可以恢复的。对于通常需要 20 秒完成的同步任务,几分钟是合理的。一小时则不合理。
在 finally 块中释放
结构化释放是最重要的修复方法。在 Kotlin 中,一个小型扩展函数可以有效避免错误。
inline fun <T> PowerManager.WakeLock.withLock(
timeoutMs: Long,
block: () -> T
): T {
acquire(timeoutMs)
try {
return block()
} finally {
if (isHeld) release()
}
}该函数以强制超时方式获取锁,执行调用者的代码块,并在 finally 子句中释放锁。由于 finally 的存在,释放操作会在正常完成、代码块提前返回或异常传播时都执行。
isHeld 检查至关重要,因为如果代码块运行时间过长,超时可能已经释放了锁。如果没有这个检查,release() 调用会抛出 "WakeLock under-locked" 异常,并覆盖代码块原本抛出的异常。将函数标记为 inline 允许调用者在代码块中使用 return 返回到外围函数,而 finally 子句仍然会执行。
使用该辅助函数后,之前示例中的联系人同步函数现在变得安全。
fun syncContacts() = wakeLock.withLock(timeoutMs = 5 * 60 * 1000L) {
val account = accountStore.current() ?: return
api.pushContacts(account)
}现在,提前返回和 pushContacts() 抛出的任何异常都会通过 withLock 中的 finally 块,因此在所有执行路径上都会释放锁。调用站点也更简洁,审查者只需一瞥即可确认正确性,无需追踪所有分支。
谨慎地将锁作用域限定在协程内
相同的模式在协程内部同样适用,因为 Kotlin 的取消操作会以 CancellationException 的形式传递,且当协程被取消时 finally 块会执行。
suspend fun uploadAll(files: List<File>) {
wakeLock.acquire(10 * 60 * 1000L)
try {
withTimeout(9 * 60 * 1000L) {
files.forEach { api.upload(it) }
}
} finally {
if (wakeLock.isHeld) wakeLock.release()
}
}锁在工作开始前获取,并在 finally 中释放,该子句会在完成、失败或外部作用域取消时执行。withTimeout 为上传操作设置了硬性截止时间,该时间略短于唤醒锁超时时间,确保协程在系统强制释放锁之前主动放弃。这解决了之前提到的无界操作问题。
一个注意事项:前一节提到的 inline 辅助函数在这里也可以使用,因为从 suspend 上下文调用时,inline lambda 可以调用 suspend 函数。
为每个任务分配独立的锁
在不相关功能之间共享同一个 WakeLock 对象会使引用计数难以理解,且会使电池统计失去意义,因为每个功能都会报告相同的标签。为每个任务创建带有描述性标签的独立锁几乎不消耗资源,且使内存泄漏更容易追踪。
如果确实需要共享锁,请禁用引用计数或确保单个所有者负责管理。
正确归因工作负载
如果您的代码代表其他应用或 UID 获取唤醒锁(这在系统服务和 SDK 中很常见),请调用 setWorkSource(),以便将电池消耗正确归因于相关方。应用程序很少需要此功能,但平台和 OEM 工程师应了解其存在。
完全不需要唤醒锁的场景
避免唤醒锁泄漏的最佳方式是根本不持有唤醒锁。现代 Android 的大多数后台工作都应使用更高层次的 API,这些 API 会内部管理唤醒锁。
| 需求场景 | 建议替代方案 | |---------|-------------| | 可延迟的后台工作(同步、上传、清理) | WorkManager 或 JobScheduler | | 必须在精确时间运行的工作 | AlarmManager.setExactAndAllowWhileIdle() 加上短时任务 | | 广播后需要执行的简短后续工作 | BroadcastReceiver.goAsync() | | 用户知晓的长时间运行工作(音乐、导航、健身) | 带有适当类型的前台服务 |
保持屏幕常亮
FLAG_KEEP_SCREEN_ON用户发起的数据传输
用户发起的数据传输任务(API 34 及更高版本)
左侧列描述了应用试图执行的操作,右侧列列出了涵盖该操作的 API 名称。这些 API 的共同点在于,它们都会代表应用获取唤醒锁并在工作完成或超时时释放唤醒锁。
WorkManager 和 JobScheduler 会在 doWork() 或 onStartJob() 执行期间持有唤醒锁,并强制执行限制。goAsync() 会在你完成处理时延长系统广播唤醒锁约 10 秒。FLAG_KEEP_SCREEN_ON 与窗口可见性相关,当用户离开时会消失。
以下是替换上传示例的 WorkManager 工作者:
class UploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
uploadPendingFiles()
Result.success()
} catch (e: IOException) {
Result.retry()
}
}
}CoroutineWorker 在 JobScheduler 持有唤醒锁期间执行 doWork()。当 doWork() 返回时,无论结果是成功、失败还是重试,唤醒锁都会被释放。如果工作执行时间过长,系统会停止该工作并取消协程。代码中没有 acquire() 或 release(),因此不存在泄漏风险。在 I/O 失败时返回 Result.retry(),可以让 WorkManager 通过回退机制安排下一次尝试,而不是让应用在循环重试时持续占用 CPU。
手动唤醒锁仍然有其应用场景。媒体播放器、VoIP 套件以及通过蓝牙从连接设备流式传输数据的应用,通常需要在前台服务运行时保持 CPU 运行。在这些情况下,服务是生命周期所有者,唤醒锁应在 onCreate() 或播放开始时获取,并在对应的停止路径和 onDestroy() 中释放。
了解 Doze 模式对唤醒锁的限制也很重要。当设备进入 Doze 模式时,系统会忽略不在电池优化白名单中的应用持有的部分唤醒锁。这并不是允许泄漏的许可,因为唤醒锁在维护窗口期间和设备活跃时仍会被计数,但这解释了为什么仅靠唤醒锁无法保证代码整夜持续运行。
如何检测唤醒锁泄漏
由于设备连接电源、屏幕保持开启或有其他因素维持唤醒状态,泄漏在开发过程中很少出现。你需要主动查找它们。
使用 dumpsys power 检查当前持有的锁
最快的方法是询问 PowerManagerService 当前正在持有什么。
adb shell dumpsys power | grep -A 20 "Wake Locks:"此命令会转储电源管理器的完整状态,并过滤到唤醒锁部分。输出会列出当前持有的每个应用级唤醒锁,每行显示一个。
Wake Locks: size=2
PARTIAL_WAKE_LOCK 'myapp:upload' ACQ=-14m32s110ms (uid=10245 pid=8123)
PARTIAL_WAKE_LOCK 'AudioMix' ACQ=-3s204ms (uid=1041 ws=WorkSource{10112})每一行显示锁级别、标签、获取时间(ACQ)、拥有者 UID 和进程。
在本示例中,myapp:upload 已保持超过14分钟,这强烈暗示存在内存泄漏(正常上传通常只需数秒)。第二个条目由音频服务器代表另一个应用持有,其工作源显示了这一点。关闭屏幕,等待一分钟,再次运行此命令。如果您的应用仍有锁存在且 ACQ 时间持续增长,就需要进行调查。
使用 batterystats 测量总计时
dumpsys power 显示的是当前时刻的数据。要查看一段时间内累积的唤醒锁时间,请使用电池统计信息。
adb shell dumpsys batterystats --reset
# 断开连接,正常使用设备或让它保持空闲一段时间
adb shell dumpsys batterystats com.example.myapp | grep -i "wake lock"第一条命令会清除已收集的统计数据,使测量从零开始。在电池供电状态下运行设备一段时间(至少一小时可获得有用数据)后,第二条命令会打印您应用的统计数据,并过滤出唤醒锁相关行。
您将看到每个标签及其总持有时间和获取次数。某个标签若总时间很长但获取次数很少,通常意味着锁被长时间持有且未及时释放。
如需可视化时间线,可通过 adb bugreport 捕获崩溃报告并加载到 Battery Historian 中,或启用 android.power 数据源记录 Perfetto 轨迹。两者都会在时间线中以条形图显示唤醒锁,与屏幕状态、网络活动和 CPU 频率并列,便于发现相关操作停止后仍长时间持有的锁。
在 Play 控制台中监控 Android 核心指标
Google Play 将过度使用部分唤醒锁视为 Android 核心指标之一。当应用在后台时,若非豁免的部分唤醒锁累计持有时间过长(当前定义为24小时内累计2小时或以上),则会被视为用户会话的过度使用。由某些系统管理任务持有的唤醒锁(如音频播放或特定任务类型)属于豁免范围。
Play 已将此指标设为关键核心指标,这意味着超出不良行为阈值的应用可能会在商店中降低可见性,其应用列表页面还可能显示警告。具体阈值和豁免范围会随时间变化,因此请查阅最新的 Android 核心指标文档。但实际结论很明确:泄漏的唤醒锁现在已成为影响商店排名的问题,而不仅仅是电池耗电投诉。
启用 Lint 检查
Android Lint 包含两个相关检查。Wakelock 会标记出获取了锁但可能未释放的代码路径,WakelockTimeout 会标记未设置超时的获取操作。默认情况下两者均为警告级别。在 lint.xml 或 Gradle 配置中将它们提升为错误级别,可阻止新泄漏被合并。
android {
lint {
error += listOf("Wakelock", "WakelockTimeout")
}
}此 Gradle 配置块将这两个检查从警告级别提升为错误级别,当出现相关模式时构建将失败。Lint 是静态分析工具,无法跟踪所有异步路径,因此无法发现前面提到的回调泄漏,但能以较低成本捕获简单情况。
如何测试唤醒锁行为
使用 Robolectric 可轻松通过单元测试覆盖唤醒锁处理逻辑,它提供了 PowerManager 的影子实现。
@RunWith(RobolectricTestRunner::class)
class ContactSyncTest {@Test
fun releasesWakeLockWhenSyncFails() {
val context = ApplicationProvider.getApplicationContext<Context>()
val syncer = ContactSyncer(context, api = FailingApi())
runCatching { syncer.syncContacts() }
val lock = ShadowPowerManager.getLatestWakeLock()
assertThat(lock).isNotNull()
assertThat(lock.isHeld).isFalse()
}该测试使用一个始终抛出异常的伪造API创建被测组件。它运行同步操作并忽略异常,然后向Robolectric查询最近创建的唤醒锁。
两个断言验证了在失败后确实使用了锁且锁已释放。这类测试可以低成本地为每个错误路径编写,能精准捕获审查中最难发现的异常和提前返回导致的泄漏。将WakeLock(或其小型包装接口)注入到类中会使测试更简单,因为可以直接使用普通伪造对象而无需Robolectric。
框架层以下的唤醒锁
如果你开发平台代码、硬件抽象层(HAL)或原生守护进程,需要在更低一层处理唤醒锁。原生代码可通过libpower库调用acquire_wake_lock()和release_wake_lock(),在当前Android版本中这些调用会经过SystemSuspend服务,该服务负责与内核的/sys/power/wake_lock文件接口通信。
SystemSuspend的AIDL接口通过acquireWakeLock()返回一个IWakeLock对象。只要该对象存在锁就会保持激活状态,当最后一个引用被释放时锁会解除,这为原生代码提供了与Kotlin finally模式相同的生命周期范围。
在近期内核中,/sys/class/wakeup/目录下暴露了完整的唤醒源列表(包括驱动程序持有的唤醒源,而不仅是PowerManagerService持有的那个),旧版内核则在/sys/kernel/debug/wakeup_sources(需要debugfs支持)。读取这些文件需要提升权限。当dumpsys power显示没有应用唤醒锁但设备仍拒绝休眠时,通常是某个驱动程序或原生守护进程持有了内核唤醒源,这些文件会显示具体是哪一个。
总结
唤醒锁告知系统即使屏幕关闭也需要保持CPU运行。应用层级的唤醒锁由PowerManagerService管理,该服务按UID和标签追踪每个锁,并为所有锁维护一个内核唤醒源。由于保持唤醒是显式请求的,未释放的锁会阻止设备休眠并持续耗电直到超时或进程终止。
大多数泄漏源于普通控制流:跳过release()的异常和提前返回、忘记释放的错误回调、引用计数失衡、以及围绕未完成操作持有的锁。
解决方案同样普通。始终传递超时参数、在finally块中释放(理想情况下通过withLock等小型辅助函数)、为每个任务分配独立的带标签锁,并在锁内操作设置截止时间。
在大多数情况下,最佳选择是完全避免手动管理唤醒锁。WorkManager、JobScheduler、goAsync()、前台服务和FLAG_KEEP_SCREEN_ON都会为你管理锁并在工作结束时释放。确实需要使用时,请通过dumpsys power、batterystats、Perfetto或Battery Historian、Lint以及覆盖失败路径的单元测试进行验证。
使用 Play 现在将过多的唤醒锁视为核心关键指标,因此在发布前检测这些泄漏对用户的电池寿命和应用在商店中的可见性都至关重要。
合作伙伴推荐课程
(在新标签页中打开)
广告
- Udemy 上的 Android 应用开发(Kotlin)| 从入门到高级 Oak Academy、OAK Academy Team、Mehmet ÖNGEL 4.3(满分5分)(2,368 人评价)2天7小时49分钟 入门 证书 查看课程 (在新标签页中打开)
- MasterClass 上的 John Legend 教授作曲 John Legend 4小时4分钟 入门 查看课程 (在新标签页中打开)
- Udemy 上的完整 Android Oreo(8.1)、N、M 和 Java 开发 Kavita Mandal 4.4(满分5分)(1,157 人评价)8小时36分钟 入门 查看课程 (在新标签页中打开)
- Udemy 上的 Android 应用开发大师课(使用 Kotlin)Tim Buchalka、Jean-Paul Roberts、Tim Buchalka's Learn Programming Academy、David Reidy 4.5(满分5分)(6,194 人评价)2天14小时14分钟 入门 证书 查看课程 (在新标签页中打开)
更多 Android 开发课程请访问 Class Central
→
阅读更多文章。
如果这篇文章对你有帮助,请分享。
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者。立即开始