1. 先把练习场的地图铺开:我为什么要搭这么一套东西
先说个现象。这几年我面试过的Android开发,十个里有七八个能把业务代码写得挺顺,一问到基础技能就含糊:AGP和Android Studio的版本关系说不清、R8混淆规则全靠网上抄、setColor和setBackgroundColor到底有什么区别也得想半天。这不是个例,是整个行业被高节奏业务推着走之后留下的通病——大家习惯了"搜到答案就跑通",很少停下来问一句"为什么是这样"。
所以我给自己搭了一个名为RYG的Android开发基础技能练习场。名字的含义很简单:Recruit Your Growth,把零散的Android基础技能拆成一个个可以反复练、可以验证的关卡,每一关对应一类真实开发中高频出现的问题。这个练习场不是再写一遍教程,而是把知识点转成"能跑起来的小项目"和"能自查的清单",练完一个关卡,你不仅知道答案,还能跟别人讲清楚来龙去脉。
我梳理练习场的技能地图时,参考了一个很有用的信号:各大平台的热搜词。热搜词里反复出现的主题,就是真实开发者踩坑最密集的地方。我曾经拉过一段时间的Android相关搜索词,发现高热度主要集中在几个方向:
| 技能层 | 对应练习关卡 | 典型热搜/痛点 |
|---|---|---|
| 工具链与构建 | Android Studio调教、AGP版本匹配、Gradle报错排查 | android studio汉化、hedgehog支持agp8吗、could not load compiled classes |
| 代码安全与发布 | R8混淆规则、多形态产品构建 | android r8、谷歌android马甲包代码混淆 |
| UI基础 | 颜色与透明度、协调布局、Banner联动、动态图标 | setcolor、透明度对照表、协调布局+banner、动态图标主题 |
| Framework机制 | AMS、电话状态监听、OTA升级、启动优化 | android ams、phonestatelistener、android ota、android studio火焰图 |
| 架构与组件 | MVVM、蓝牙、播放器SDK、剪贴板与文件访问 | mvvm代码示例、蓝牙、smartplayer集成、file:///storage/emulated/0 |
| 进阶方向 | 车载系统、多端适配、嵌入式调试 | android车载、harmonyos next、openocd、i2c-tools |
当一个话题被反复搜,它大概率不是"冷门偏题",而是无数人卡过的真实瓶颈。把这些瓶颈编进练习场,练的就是最有性价比的基本功。
这套练习场适合三类人:刚入行想建立完整知识体系的初级开发、准备跳槽想系统梳理基础的进阶开发、以及带团队想快速摸底组员能力的Leader。使用方式也很简单——每个关卡都包含四步:跑通Demo、改参数观察现象、断掉某个依赖看会不会崩、最后用自己的话把原理讲出来。前两步是"会用",后两步是"真懂"。
2. 练手第一步不是写代码,而是把Android Studio这套家伙什儿调到顺手
2.1 下载、汉化和插件仓库,这些事看着小,坑起来也耽误时间
Android Studio的下载渠道其实只有一个官方源头,就是Android开发者官网。网上搜"android studio下载"会出来一堆第三方站点,有些还捆绑了奇怪的安装包,我建议一律不碰。官网会按操作系统区分安装包,Windows用户选.exe版本,macOS用户注意区分Intel芯片和Apple Silicon芯片,下错了装不上是小事,装上了跑起来各种莫名其妙卡顿才麻烦。
下载完成后第一件事不是新建项目,而是先把界面语言调好。很多初学者英文界面看着吃力,搜索"android studio中文"或者"android studio汉化"。新版Android Studio直接用插件方式支持中文:打开Settings,进入Plugins,搜索"Chinese (Simplified) Language Pack",安装后重启就是中文界面。这个中文语言包是JetBrains官方出的,质量有保障,不要从乱七八糟的渠道下载汉化包,之前有人从非官方渠道下载汉化插件结果IDE卡死还报一堆错。
插件仓库这块,新版Android Studio默认从JetBrains官方插件市场拉取插件,也支持从本地磁盘安装自定义插件。你在Settings的Plugins面板里看到的"Marketplace"就是官方仓库。国内网络环境下,插件市场偶尔加载不出来,我一般会先在浏览器访问插件市场网页版,确认插件兼容的IDE版本号,再回到IDE里搜索安装。如果Marketplace一直转圈,可以尝试修改IDE的DNS设置或者稍后再试,但不要乱配置网络代理,尤其是不要用那些来历不明的"加速工具",很多安全问题就是这么来的。
2.2 AGP版本与Android Studio版本的对应关系,别在这上面凭感觉
热搜词里有一条很典型:"android studio hedgehog | 2023.1.1 patch 2支持agp8版本吗"。答案是支持。Android Studio Hedgehog(2023.1.1)官方默认配套的AGP版本就是8.2.x,所以Patch 2不仅支持AGP 8,而且是专门为AGP 8系列调校的。
这里的关键是理解"Android Studio版本"和"AGP版本"不是一回事。Android Studio是IDE,AGP是Gradle插件,它们各自有版本号,但有官方约定的兼容边界。选错了组合,轻则构建警告,重则直接无法编译。我整理过一张常用对应表:
| Android Studio版本 | 默认AGP版本 | 要求的JDK版本 |
|---|---|---|
| Giraffe(2022.3.1) | 8.1.x | JDK 17 |
| Hedgehog(2023.1.1) | 8.2.x | JDK 17 |
| Iguana(2023.2.1) | 8.3.x | JDK 17 |
| Jellyfish(2023.3.1) | 8.4.x | JDK 17 |
| Koala(2024.1.1) | 8.5.x | JDK 17 |
AGP 8.x要求JDK 17,这是很多项目升级时崩溃的第一个原因——项目里还在用JDK 11甚至JDK 8,Gradle同步直接报错。练习场里我专门加了一个关卡:新建一个项目,手动把AGP版本降一个主版本号,观察Gradle同步报什么错,再升回来。这样折腾一次,你对版本约束的理解比看十篇文章都深刻。
2.3 一个高频Gradle报错:could not load compiled classes for settings file
热搜词里有一长串报错:"could not load compiled classes for settings file 'd:\android\coffee\setting..."。这报错我前后遇到过五六次,每次原因还不完全一样,但排查链路是通用的。首次遇到时,我一度以为是项目代码写错了,后来发现根因往往是Gradle缓存损坏与项目路径问题叠加。
完整的排查路径应该是这样的:
- 先看完整错误日志,注意是"settings file"而非"build.gradle",说明问题出在Gradle初始化阶段,跟业务代码无关。
- 去项目根目录删除
.gradle文件夹,再去用户目录下的.gradle/caches清理对应版本的缓存,重新同步。 - 检查项目路径有没有中文、空格、特殊符号。热搜里那个路径是d:\android\coffee,看起来正常,但如果你路径里有中文,Gradle在某些版本下会编译settings脚本失败。
- 检查JDK版本是否匹配当前AGP要求,不匹配就改Project Structure里的SDK location和Gradle JDK设置。
- 如果以上都不行,用命令行执行
gradlew clean并加--stacktrace参数,看完整堆栈定位到具体是哪一行的settings配置出问题。
我在练习场里把这条报错做成了必练关卡,因为排查Gradle构建问题的思路,跟排查业务代码崩溃的思路是一模一样的:先缩小范围、再复现、再验证。这套方法论练熟了,构建问题就不再是玄学。
3. 构建与代码安全的基本功:R8不是"混淆器"那么简单
3.1 R8到底在构建时对你的代码做了什么
很多开发者的理解是"R8就是混淆代码防止别人反编译",这个理解太窄了。R8在Android构建链里承担的是压缩、优化、混淆、资源收缩四件事。
- 压缩:遍历所有代码入口,把从未被引用的类、方法、字段从最终包里删掉。你依赖了一整个SDK,但只用到一个工具类,R8能帮你把用不到的几百个类丢掉。
- 优化:对字节码做等价变换,比如移除无效的try-catch、合并重复代码、内联短方法。
- 混淆:把类名、方法名、字段名改成a、b、c这样的短名,增加逆向阅读难度。
- 资源收缩:配合
shrinkResources属性,把未引用的资源文件从APK里移除。
从AGP 8.0开始,R8已经取代了原来的ProGuard,minifyEnabled true就会自动启用R8。我在练习场里会让同学做一个实验:同一个HelloWorld项目,记录开启R8前后的APK体积、构建耗时、反编译后的代码效果。这个实验做下来,你会发现R8的作用比想象中大很多,但它的坑也比想象中多——因为压缩和混淆可能把你正常运行的代码"优化"崩了。
3.2 keep规则为什么是混淆配置里最重要的部分
混淆崩了最常见的原因就是反射、JNI、序列化、注解这类"动态调用"机制,R8静态分析无法发现这些调用关系,就会把目标类当作无用代码删除或重命名。解决手段就是写keep规则。我在练习场里给了一个通用模板,可以直接抄:
# 保留实体类(Gson/Jackson反序列化用) -keep class com.ryg.model.** { *; } # 保留枚举类,枚举有values()和valueOf()反射逻辑 -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } # 保留JNI方法名 -keepclasseswithmembernames class * { native <methods>; } # 保留注解,别把运行时注解干掉了 -keepattributes *Annotation* # 保留WebView JS接口 -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } # 保留Parcelable实现类,系统要用CREATOR字段反射创建对象 -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; }写keep规则的核心原则是:不确定会不会用到反射的地方,宁可keep也不要冒险。keep多了只会让包大一点,keep少了直接崩溃。
3.3 多形态产品的混淆与构建差异
热搜词里有"谷歌android马甲包代码混淆"。这里我不讨论那类灰色玩法,单说技术本质:同一套核心代码,要做成多个皮肤包/多形态产品,共享大部分逻辑,但包名、图标、主题色、部分资源不同。这在正规的商业场景里也很常见,比如一个App的海外版和国内版、免费版和Pro版。
这种需求的标准姿势是Gradle的productFlavors,配合维度做变体构建:
android { flavorDimensions += "version" productFlavors { create("free") { dimension = "version" applicationId = "com.ryg.free" versionNameSuffix = "-free" } create("pro") { dimension = "version" applicationId = "com.ryg.pro" versionNameSuffix = "-pro" } } }每个flavor可以有自己的目录src/free/res和src/pro/res,放各自的图标、颜色、字符串。混淆规则这里有个容易踩的坑:不同flavor可能引入不同的第三方SDK,各自的keep规则要放到对应flavor的proguard-rules-xxx.pro里,而不是全部堆在主规则文件里。不然打包pro版本时,把free版SDK的keep规则一起带进去,包体积和崩溃风险都会上升。
我还习惯在构建脚本里给多形态产品加一个"版本身份打印":启动时把BuildConfig.FLAVOR和BuildConfig.APPLICATION_ID打到日志里。这样测试人员报bug时,我们第一眼就知道对方用的是哪个形态的包,排查效率翻倍。
4. UI层的高频基础关卡:颜色、协调布局、动态图标,细节里全是面试题
4.1 setColor、透明度对照表,这些"小东西"串起来能考倒一片人
热搜词里"setcolor"和"透明度对照表"几乎是常青树。这两个词看似简单,背后涉及的是Android颜色系统的完整理解。我先说结论:Android里View.setTextColor()和View.setBackgroundColor()接收的都不是"颜色",而是int值,这个int的四个字节分别代表Alpha、Red、Green、Blue,顺序是ARGB。
很多新手写布局时看到android:background="#FF0000"就以为是"红加蓝",写代码时又把Color.RED直接塞给背景,结果发现颜色不对,其实都是没搞懂透明度对应的前缀。透明度这块我给练习场整理过一个速查表:
| 透明度 | 十六进制前缀 | 常见业务场景 |
|---|---|---|
| 100%(不透明) | FF | 正常文字、图片 |
| 90% | E6 | 弹窗遮罩 |
| 80% | CC | 二级遮罩、水印 |
| 70% | B3 | 大图上的半透明引导 |
| 60% | 99 | 弱化文字 |
| 50% | 80 | 分割线、禁用态 |
| 40% | 66 | 更弱的禁用态 |
| 30% | 4D | 阴影层 |
| 20% | 33 | 极淡的底色 |
| 10% | 1A | 页面背景装饰 |
| 0%(全透明) | 00 | 占位透明色 |
代码里设置颜色时,我强烈建议用带透明度前缀的写法,比如Color.parseColor("#CCFF0000"),意思是"80%透明度的红色"。这里有个新手常犯的错:Color.parseColor("#FF0000")是纯红,Color.parseColor("#80FF0000")才是半透明红。前缀多写两位,很多人一开始不习惯。
另外要区分setColor和setTint:前者是直接设置颜色值,后者是给Drawable着色用的。如果你对一个BitmapDrawable调用setColor,大概率没效果,应该用setColorFilter或者setTint。这个知识点在面试里出现的频率极高,我几乎每次都会追问一句"setColor和setBackgroundColor有什么本质区别",能答清楚的人不多。
4.2 协调布局与Banner联动,练的是嵌套滚动的核心理解
"android中协调布局+banner"是热搜词里我最喜欢的一道题,因为它把CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout、ViewPager2、NestedScrollView这些组件全串起来了,练一次等于复习了整个Material体系。
一个典型场景:顶部是CollapsingToolbarLayout,里面放一个Banner(ViewPager2实现的轮播图),下滑时Banner随着Toolbar折叠收起,页面上滑时Banner展开,下面的内容区域是NestedScrollView。我给的练习模板长这样:
<androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.appbar.AppBarLayout> <com.google.android.material.appbar.CollapsingToolbarLayout app:layout_scrollFlags="scroll|exitUntilCollapsed"> <androidx.viewpager2.widget.ViewPager2 app:layout_collapseMode="parallax" /> <androidx.appcompat.widget.Toolbar app:layout_collapseMode="pin" /> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <androidx.core.widget.NestedScrollView app:layout_behavior="@string/appbar_scrolling_view_behavior"> <!-- 内容列表 --> </androidx.core.widget.NestedScrollView> </androidx.coordinatorlayout.widget.CoordinatorLayout>这里最容易踩的坑有三个。第一,AppBarLayout的layout_scrollFlags如果漏了scroll标志,整个折叠效果不生效;如果漏了exitUntilCollapsed,Toolbar可能被完全滚出屏幕而不是固定在顶部。第二,ViewPager2在CollapsingToolbarLayout里做轮播,如果不处理触摸事件冲突,左右滑动切图跟上下滑动手势会互相抢,需要在onInterceptTouchEvent里判断滑动方向。第三,Banner自动轮播的handler如果没在onPause里停止,页面切后台后会一直发消息,回来时发现Banner"跳帧",这个问题我见过无数人栽过。
练习时我的建议是:先跑通纯XML版本,然后把ViewPager2换成RecyclerView的Banner实现,再手动加一个下拉刷新,观察onNestedPreScroll的回调顺序。这套下来,嵌套滚动的分发机制你就彻底通透了。
4.3 动态图标主题和Settings布局,属于"加分项"里的基础分
热搜词里还有"android动态图标主题"和"android settings布局"。前者指的是Android 13开始支持的Themed Icons——应用图标可以根据系统主题切换为单色模式。实现方式是在res/drawable里放一个ic_launcher_monochrome.xml,用单色矢量图描述图标轮廓,并在AndroidManifest.xml里给<application>配置android:monochrome="@drawable/ic_launcher_monochrome"。
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="48" android:viewportHeight="48"> <path android:fillColor="#000000" android:pathData="M24,4C12.95,4 4,12.95 4,24s8.95,20 20,20 20,-8.95 20,-20S35.05,4 24,4z" /> </vector>注意这个矢量图里fillColor随便写,系统主题会覆盖它,但pathData一定要是单色可识别的轮廓,不能用渐变色或者多色图层。
Settings布局熟悉之后,你会在很多项目里用到PreferenceFragmentCompat来实现设置页,而不是手写一堆LinearLayout。现在Jetpack的androidx.preference库已经支持PreferenceDataStore,可以把设置项直接落到DataStore或Room里。练习场里我要求每个学员写一个"音效开关+推送开关+清除缓存"的设置页,用Preference框架实现,这个关卡练完,你对SharedPreferences和DataStore的差异理解也会上一个台阶。
5. Framework机制专项:AMS、电话监听、OTA、火焰图,把它们从"黑盒"变成"灰盒"
5.1 AMS到底管了哪些事,Activity启动流程别只背口诀
热搜词"android ams"几乎每天都有人搜。AMS是ActivityManagerService的缩写,它是Android系统里最核心的系统服务之一,管着Activity、Service、ContentProvider、BroadcastReceiver这些组件的生命周期,还管着进程调度、内存管理、任务栈。
练习场里我不让大家去读AOSP源码,只要求能按顺序说出一次startActivity的完整调用链:
- app进程调用
Activity.startActivity(),最终通过Binder调用到系统进程的AMS.startActivity()。 - AMS检查调用者的权限和Intent匹配的Activity,完成合法性校验。
- AMS通知ActivityThread暂停当前Activity(
onPause)。 - 如果目标Activity所在进程不存在,AMS向Zygote进程发送请求,Zygote fork出新的app进程。
- 新进程创建后,通过
ActivityThread入口初始化Application、启动主线程Looper。 - AMS通过Binder通知ActivityThread创建目标Activity,执行
onCreate、onStart、onResume。 - 旧的Activity进入
onStop状态,整个启动流程完成。
这套链路看起来简单,但很多崩溃问题的排查思路都藏在里面。比如"启动Activity时闪烁黑屏",本质是第4步新进程创建耗时,Window还是黑的;"起点Activity没走onPause",可能是singleTask启动模式导致旧实例直接onNewIntent了。练这个关卡的验收标准是:不看任何资料,能画出一条startActivity到onResume的生命周期时间线,并标出每个节点发生在新进程还是系统进程。
5.2 PhoneStateListener为什么被废弃了,新API怎么用
"android phonestatelistener"这个热搜词很能说明问题——很多人还在用旧API。PhoneStateListener在Android 12(API 31)开始,大部分回调方法被标记为废弃,原因有两层:一是电话状态属于个人敏感信息,系统收紧了对电话状态的访问权限;二是旧API的架构不好做精细化权限控制。Android 12+系统会直接忽略掉未授权应用的大部分电话监听回调,连READ_PHONE_STATE权限都拿不到完整的信号强度信息。
替代方案是TelephonyCallback,配合TelephonyManager.registerTelephonyCallback()使用:
val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val executor = ContextCompat.getMainExecutor(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { telephonyManager.registerTelephonyCallback(executor, object : TelephonyCallback(), TelephonyCallback.CallStateListener { override fun onCallStateChanged(state: Int) { // 处理通话状态 } } ) } else { // 旧设备走PhoneStateListener }这里面的逻辑是:新API不仅改了名字,还要求调用方指定Executor,强制你明确回调发生的线程。练这个关卡时,我还会额外让学员搜一下READ_PHONE_STATE权限在不同Android版本上的申请时机差异,因为很多App因为这个权限在Play商店被拒。
5.3 OTA升级机制:全量包、增量包、A/B无缝升级
"android ota"热搜词背后,是越来越多的业务开始关注系统升级和App灰度更新。OTA(Over-The-Air)分两层:系统级OTA和App内更新。App开发者重点关注的是App内更新机制,系统级OTA是ROM开发者的事。
练习场里我给的App内更新练习是这样的:用Google Play In-app Updates(国内可替换为各自厂商的升级SDK),实现"立即更新"和"柔性更新"两种策略。核心API就两个:
// 查询是否有可用更新 appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { // 提示用户更新 } } // 发起更新 appUpdateManager.startUpdateFlowForResult( info, activity, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), REQUEST_CODE )系统级OTA这里,我建议只理解概念:全量包是完整镜像,体积大但无依赖;增量包只包含新旧版本差异部分,体积小但强依赖当前版本;A/B无缝升级(Seamless Updates)是双分区方案,系统在后台把新系统装到备用分区,下次重启直接切换,失败也能回滚。理解这些概念对App开发者最大的价值是:当你遇到"系统升级后App数据丢了""升级后app被杀"这类问题时,能快速判断是不是系统OTA策略导致的。
5.4 火焰图怎么抓、怎么读,启动卡顿排查的基本功
"android studio 火焰图 指南"常年进热搜,说明大家卡顿问题都遇到过,但不会用工具。Android Studio自带的CPU Profiler就能生成火焰图,操作路径是:Debug运行App,打开Profiler窗口,选择CPU,点击Record,复现卡顿场景,停止录制,然后从下拉框里选择Flame Chart。
拿到火焰图先看三点:底部是入口线程,横向宽度代表采样时间占比,栈顶是正在执行的函数。一个函数横条特别宽,说明它占了大量CPU时间;一个函数在火焰图里反复出现,说明它被频繁调用。排查卡顿的经典姿势是先把时间轴缩放到卡顿发生的那几秒,然后从最宽的栈顶往下找,通常能定位到过度绘制、主线程IO、死循环或者频繁GC。
这里我要说一个BlockCanary和火焰图的分工:BlockCanary用于线上监控"主线程有没有卡",火焰图用于线下定位"卡在哪"。练习场里我会让学员在项目里故意写一段主线程循环做字符串拼接的代码,然后用火焰图定位到它,再手动改成StringBuilder,重新抓图对比。这个过程会让人对"CPU时间片"有非常直观的感知。
6. 架构与设备能力组合拳:MVVM、蓝牙、播放器SDK、Android/data的地狱级访问
6.1 MVVM不只是一张分层图,而是一套可以抄的骨架
热搜词里"android studio mvvm代码示例"几乎每天都有新增。我见过太多人把MVVM理解成"ViewModel+LiveData"就完事了,实际上MVVM的核心价值在于单向数据流:UI层持有ViewModel,ViewModel暴露状态,UI观察状态并渲染;用户操作通过ViewModel的方法进入,ViewModel调用数据层,数据层返回后更新状态,状态驱动UI刷新。
给一个可以直接抄进练习场的最小骨架:
// 状态类,作为UI的唯一数据源 data class LoginUiState( val isLoading: Boolean = false, val username: String = "", val errorMessage: String? = null ) class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(LoginUiState()) val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow() fun onUsernameChanged(input: String) { _uiState.update { it.copy(username = input) } } fun login() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, errorMessage = null) } // 调用Repository val result = userRepository.login(_uiState.value.username) result.onSuccess { _uiState.update { it.copy(isLoading = false) } }.onFailure { e -> _uiState.update { it.copy(isLoading = false, errorMessage = e.message) } } } } } class LoginActivity : AppCompatActivity() { private val viewModel: LoginViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 用Lifecycle.repeatOnLifecycle收集状态流 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> // 把state渲染到UI } } } } }这里我故意用了StateFlow而不是LiveData,因为从2024年开始,新项目用StateFlow是更主流的选择,它跟Compose协同更好,也没有主线程限制。练习场里会让学员分别用LiveData和StateFlow写一遍同样的登录页,对比两者在postValue、背压、协程取消这几个场景下的行为差异。这个对比做完,你对"状态管理"的理解会超过大多数工作三年的开发。
6.2 蓝牙开发的权限适配,堪称"地狱级"但很有规律
"android蓝牙"是个大话题,我练习场里只练两个方向:经典蓝牙(配对、传输文件)和BLE(低功耗蓝牙,扫描、连接、收发特征值)。权限适配是最大难点,因为Android不同版本对蓝牙权限的要求完全不同:
| Android版本 | 需要的权限 | 说明 |
|---|---|---|
| Android 11及以下 | BLUETOOTH、BLUETOOTH_ADMIN、ACCESS_FINE_LOCATION | 扫描BLE需要定位权限,属于运行时权限 |
| Android 12+ | BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE | 新增专属蓝牙权限,扫描不再依赖定位,但需要运行时申请 |
| Android 13+ | 基本同上,但NEARBY_WIFI_DEVICES可能与蓝牙并发使用 | 需要适配附近设备权限组 |
练习场的蓝牙关卡要求是:在Android 12和Android 13的设备上,完成一次BLE扫描并连接的完整流程。代码层面记住一个关键点——Android 12以上,普通蓝牙权限要跟定位权限分开处理,BLUETOOTH_SCAN虽然不需要定位权限,但如果你的扫描结果需要筛选距离,还是得动态申请ACCESS_FINE_LOCATION。另外,Android 14开始,系统对附近设备权限有更细的运行时窗口,做后台扫描会收到系统提醒。
6.3 视频播放器SDK集成,别在最基础的环节翻车
"android smartplayer 集成"这个热搜词说明很多人在做监控、直播、播放类App时会集成第三方播放器SDK。SmartPlayer本身是一款商用播放器SDK,集成的通用步骤基本适用于任何播放器SDK:引入aar、初始化SDK、创建播放器实例、设置渲染Surface、设置播放URL、控制播放暂停、销毁播放器。
练习场里我总结了三个最容易翻车的点:
第一,Surface生命周期。视频渲染的Surface要等onSurfaceCreated回调之后才能传给播放器,传太早会黑屏;页面退出时,要等播放器停止后再销毁Surface,顺序反了会出现"声音还在、画面没了"的诡异问题。
第二,音频焦点。播放器如果不处理AudioManager.requestAudioFocus,App切后台再接个电话,回来时视频声音可能继续播放,直接被应用商店审核拒掉。
第三,硬解与软解的选择。硬解功耗低、性能好,但兼容性不如软解。我的建议是写个codec_switch开关,默认硬解,遇到特定机型播放花屏时自动切软解。不要把所有机型都强制用同一个解码方式。
6.4 两个"小功能"练手:剪贴板复制和Android/data目录访问限制
热搜词里有"android复制"和两条file:///storage/emulated/0/android/data/...的路径,前者简单,后者是很多App适配时的大坑。
剪贴板这块,Android 13之后系统会在应用读取剪贴板时弹出隐私提示,如果你的App在后台读剪贴板,会被系统直接拦截。正确姿势是在Activity可见且获得焦点时才读取:
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager if (hasWindowFocus()) { val data = clipboard.primaryClip?.getItemAt(0)?.text }Android/data目录(也就是应用专属外部存储目录)的访问限制,是从Android 11开始大幅收紧的。普通文件管理器访问/storage/emulated/0/Android/data/com.baidu.searchbox/这类路径时,系统会提示"此文件夹为空"或直接拒绝访问。App内如果要读取自己Android/data目录下的文件,可以通过getExternalFilesDir()获取路径,但如果你要跟电脑传文件或者让用户手动选择文件,就得用系统的文件选择器(SAF,即ACTION_OPEN_DOCUMENT):
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "*/*" } startActivityForResult(intent, REQUEST_CODE)这个知识点跟"android copy文件到电脑"类问题紧密相关。练习场里我要求学员写一个"导出备份文件"功能,导出路径用MediaStore而不是Android/data,导出后用系统文件管理器验证能不能看到。这关通过了,你对存储分区和权限模型就有完整的实战认知。
7. 练习场的进阶延伸:车载系统、多端适配、嵌入式调试,基础打牢后往哪走
7.1 车载Android不是"手机版换个壳",而是另一套系统
"android 车载"这个热搜词热度持续上升,因为很多开发者都在关注车机方向的职业机会。安卓车载领域主要有两条线:一条是Android Automotive OS(AAOS),这是原生车载系统,运行在车机硬件上,不包含电话、短信这类手机功能,而是强化了多媒体、地图、语音助手和车辆控制接口;另一条是Android Auto,这是手机延伸到车机屏幕的投影方案。
练习场里我建议先学AAOS的Car App Library——它提供了一套组件,让你的媒体App能跟车机的"正在播放""媒体中心""仪表盘"联动。跟手机App最大的区别在两点:一是车机是多屏交互,中控屏、仪表盘、副驾屏可能同时显示同一个App的不同状态;二是车辆状态必须考虑,比如倒挡时中控要立刻切到倒车影像,你的App必须响应CarPropertyManager的车辆状态变化。
7.2 多端适配的版本底线怎么定
热搜词里有一条很有意思:"a+支持ios 11.0及以上,android 4.0及以上,harmonyos next 5.0及以上"。这看起来很像是某个广告SDK或跨端方案写的兼容范围。这里我提醒一句,Android 4.0(API 14)在2024年之后已经基本不可能作为新App的底线了,Google Play要求新上架应用最低API级别不低于API 23(Android 6.0),从2025年开始要求更高。国内应用市场虽然没有统一强制的底线,但目标API低于29的App在部分商店已经无法更新。
跨端适配的决策逻辑应该是:先定你的核心用户用什么设备,再看技术栈能覆盖哪些系统。如果面向国内全量市场,Android最低支持到API 23-26是比较合理的;面向老年人设备,可能得再往下放一点;面向车机、POS机等专用设备,反而要看设备的系统版本上限。鸿蒙NEXT那套5.0适配方案,本质是另一套生态,做原生Android的人可以先通过Flutter或者鸿蒙的兼容层去了解,但不要指望Android代码直接跑在HarmonyOS NEXT上——两者从底层就已经分叉了。
7.3 嵌入式调试与开放配件:OpenOCD、i2c-tools这些词是什么来头
"android openocd"和"i2c-tools在android上使用"这类热搜说明,有一部分Android开发者已经走入硬件交互领域。OpenOCD是一个开源的片上调试器,配合JTAG/SWD接口,可以在命令行里烧录固件、设置断点、读写寄存器,主要用于嵌入式开发。要是你的Android设备是开发板或者工控机,可以用OpenOCD连接芯片调试底层系统。
i2c-tools是Linux下操作I2C总线的一组命令行工具,在Android上使用通常需要root权限,因为有权限控制总线的访问。用法跟嵌入式Linux一样,先扫描总线地址,再读写寄存器:
# 查看总线和设备地址 i2cdetect -l # 扫描0号总线 i2cdetect -y 0 # 读取设备0x48的0x00寄存器 i2cget -y 0 0x48 0x00这些工具在Android上最常见的场景是调试传感器、PMIC电源芯片、触摸屏控制器这类外设。练习场里我把这块列为选修,但对有兴趣往车载、IoT、智能硬件方向走的人,这是性价比很高的加分项——毕竟懂App又懂硬件调试的人,在车载领域非常稀缺。
最后再分享一点做这套练习场的体会。我不主张把知识点塞得越全越好,而是坚持"每个知识点必须配触发条件和验证路径":触发条件是这个技能在什么时候会被用到,验证路径是你能做点什么实验来确认自己真的懂了。比如"我记得R8会做压缩优化"是记忆,亲手跑一遍看到APK体积下降、再故意写个反射代码触发崩溃、最后用keep规则把它修好,这是技能。面试题本质上也是这个逻辑的副产品——当你的练习过关次数足够多,面试题就只是一次随堂测验,而不是临时抱佛脚的猜题游戏。