资讯动态

Android智能锁屏开发:悬浮窗方案与场景联动实现指南

发布时间:2026/10/14 17:42:44 来源:尧图企业网站定制
简介这份Android智能UI锁屏源码工程面向具备一定Android基础、希望研究锁屏定制与交互优化的开发者。资源完整呈现了智能锁屏App从界面布局到功能实现的全过程涵盖动态壁纸、通知预览、解锁动画、快捷入口等核心模块并涉及WallpaperService、NotificationManager等系统API与第三方动画库的实际应用。资源包共57个文件以java源码、xml布局与配置、class与dex编译产物为主附带jar库及png资源其中apk可直接验证效果。压缩包整体约722KB结构清晰适合作为锁屏个性化开发或毕业设计参考。已有249人学习下载对快速上手并改造出符合自身需求的智能锁屏方案具有不错的落地价值。1. Android 智能UI锁屏为什么说它是“手机上最特殊的界面”Android 智能UI锁屏本质上是把系统默认的锁屏替换成一套可自定义的交互界面同时让它具备场景感知能力——能根据充电状态、音乐播放、夜间时间、通知优先级等条件自动切换展示内容和操作入口。它解决的不是“换个好看的壁纸”这种表层需求而是让锁屏从“解锁前的等待页”变成“信息聚合和快速操作的中转站”。这是用户每天看几十次、每次停留几秒的界面也是交互价值密度最高的界面。但市面上的自研锁屏大多止步于“能用”真正投入项目的很少。原因不复杂锁屏界面的实现跨越 WindowManager、系统广播、电源管理、传感器监听、多任务保护多个领域随便一个环节出错手机就会卡顿、黑屏或无法亮屏。很多人不知道悬浮窗锁屏和系统级锁屏是两条完全不同的技术路线选错一条后续所有努力都在给厂商定制 ROM 的规则填坑。这篇笔记会从路线选择讲到底层实现和参数调试把能落地的那条路说透也把坑提前标出来。2. 先选路线再动手悬浮窗与系统级锁屏的取舍2.1 悬浮窗方案为什么它是多数自研锁屏的默认选择我见过的自研锁屏项目九成以上走的是悬浮窗路线。原因直白它不需要系统签名不需要刷机也不需要伪装系统组件普通应用就能把一套自绘 UI 挂在锁屏之上。这种方案依赖 WindowManager 添加一个应用悬浮层监听系统屏幕开关广播在屏幕熄灭时展示锁屏内容在用户解锁后移除这个层。技术实现上有三个判断点。第一权限门槛清晰Android 6.0 以后需要动态申请“显示在其他应用上层”权限国内定制 ROM 额外要求“后台弹出界面”权限这两个权限引导做得好不好直接决定用户能不能装上就用。第二系统锁屏和你的锁屏是“上下叠放”的关系你的 UI 永远在系统锁屏之上所以视觉一致性是可控的。第三分发路径简单不依赖厂商合作适合快速验证产品想法。选悬浮窗方案前要接受一个不完美的体验系统锁屏本身还在运行指纹解锁、人脸解锁的动作触发后系统锁屏会被解掉你的悬浮层如果没被及时清理就会出现“系统锁屏一闪而过、自己的界面还悬着”的撕裂感。解决这个问题的常见做法是监听 ACTION_USER_PRESENT 和 KEYGUARD 状态变化在系统解锁成功的瞬间同步移除悬浮窗并把移除动画控制在 80 毫秒以内。2.2 系统级锁屏替换什么情况下才值得碰系统签名系统级替换意味着你要修改或 Hook SystemUI 的 Keyguard 模块让系统直接启动你的锁屏 Activity 作为唯一锁屏。这条路的技术门槛和商业门槛都高一大截——需要设备管理器权限、系统签名、或者厂商定制固件的合作普通应用完全走不通。它适合的场景集中在行业定制领域。比如某类物流手持终端要求员工扫码后自动登录指定业务界面不允许跳过扫描或者某类展示终端要求锁定在营销页面并把所有系统入口藏掉。这类需求没法靠悬浮窗解决因为悬浮窗无法阻止用户下拉通知栏、无法阻止系统锁屏的密码键盘弹出。如果你的项目在评估这条路线我的建议是先做一次成本调查确认目标的 ROM 是否开放了 KEYGUARD 替换接口确认设备管理器 API 在当前系统版本上的行为再跟团队估算适配周期。企业级定制场景下这是值得投入的泛用户型 App 不建议因为每个手机厂商的 Keyguard 实现都不一样适配成本随机型数量线性增长。对比维度悬浮窗方案系统级替换方案系统权限悬浮窗 后台弹出界面设备管理器 / 系统签名分发难度应用商店正常上架需预置或行业定制系统锁屏叠加在上层完全接管适配成本中主要适配定制 ROM高每套 ROM 独立适配推荐场景普通自研锁屏、锁屏工具行业终端、企业管控选型没有绝对的对错关键在于你要先搞清楚自己手里的资源和目标设备的系统现状。如果只是做一款让用户愿意装、愿意用的锁屏应用悬浮窗路线是唯一现实的选择下面的章节都围绕这条路线展开。3. 把锁屏 UI 挂到系统上悬浮窗实现与按键拦截3.1 申请悬浮窗权限Android 8.0 前后的行为差异悬浮窗权限是整套方案的入口也是第一个坑。Android 8.0API 26把 SYSTEM_ALERT_WINDOW 权限的申请行为改了8.0 之前用户只要在设置里允许过一次权限就长期有效8.0 之后权限变为动态请求并且每个悬浮窗类型都需要单独判断。更麻烦的是国内厂商普遍在系统设置里把悬浮窗权限和后台弹出界面拆成两个开关用户经常只开其中一个导致你的锁屏 window 能被 addView 添加却显示不出来。下面是建议用到的权限检查与请求逻辑包含两个开关的差异化引导// 检查是否有悬浮窗权限 public boolean hasOverlayPermission(Context context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { return Settings.canDrawOverlays(context); } return true; } // 请求悬浮窗权限Android 8.0 后必须动态跳转设置页 public void requestOverlayPermission(Context context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { Intent intent new Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(package: context.getPackageName()) ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } // 判断国产 ROM 的“后台弹出界面”是否开启跳转至对应设置页 public boolean checkBackgroundPopupPermission(Context context) { // 通过 AppOpsManager 的 OP_SYSTEM_ALERT_WINDOW 做兜底判断 AppOpsManager appOps (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); // 部分 ROM 上该方法返回 MODE_DEFAULT此时建议直接用 Intent 引导一次设置 int mode appOps.checkOpNoThrow(system_alert_window, Process.myUid(), context.getPackageName()); return mode AppOpsManager.MODE_ALLOWED; }注意代码里的分岔点checkOpNoThrow 在部分 ROM 上不可靠返回 MODE_DEFAULT 并不代表权限被拒但也不能确认权限已授予。我一般会做双重判断——先走 Settings.canDrawOverlays再取安全兜底策略如果两个 API 都无法确认就在应用内展示一个权限引导页明确告知用户哪两个开关必须打开而不是直接进入锁屏逻辑。关于第一个 Intent 参数的完整写法ACTION_MANAGE_OVERLAY_PERMISSION 的 Uri 带上包名可以直接定位到当前应用的悬浮窗权限项避免用户在设置页里翻找。华为、小米、OPPO、vivo 都支持这个 Intent只是落地后的界面样式不同。Vivo 的悬浮窗设置藏在“更多设置-权限管理”里部分系统版本不响应 ACTION_MANAGE_OVERLAY_PERMISSION这种情况需要额外配置厂商专用的跳转 Intent没法用一个通用方案覆盖所有机型。3.2 监听屏幕熄屏亮屏用延迟策略避免闪白悬浮窗的添加和移除必须绑定系统屏幕广播。熄屏时注册 ACTION_SCREEN_OFF 回调将锁屏 View 通过 WindowManager.addView 加到屏幕上亮屏时注册 ACTION_SCREEN_ON 和 ACTION_USER_PRESENT决定是否继续显示或直接移除。这里的核心是时序控制。ACTION_SCREEN_ON 触发时系统锁屏界面正在显示如果你的悬浮窗立刻 addView会覆盖在系统壁纸还没绘制完成的窗口上屏幕会出现明显的白色或黑色闪帧。更合理的序列是延迟 120 到 200 毫秒再添加 View给系统锁屏一个完整的绘制窗口。另外熄屏广播在收起悬浮窗时必须立即执行千万别用一个 300 毫秒的动画慢慢淡出。电源键按下时系统会在极短时间内切到深睡眠你的动画才播了一半窗口就已经被系统回收再次亮屏时会看到残影。private void onScreenOff() { // 熄屏时立刻移除锁屏不能给动画时间 removeOverlayView(); // 这里保存一个“熄屏时间戳”用于稍后点亮屏时做延迟判断 screenOffTime System.currentTimeMillis(); } private void onScreenOn() { // 如果距上次熄屏太短直接跳过避免频繁 add/remove 的抖动 long elapsed System.currentTimeMillis() - screenOffTime; if (elapsed 500) { return; } if (hasOverlayPermission(this) checkBackgroundPopupPermission(this)) { // 延迟 150ms 再添加锁屏 UI避开系统亮屏的窗口重建阶段 handler.postDelayed(() - showOverlayView(), 150); } } private void onUserPresent() { // 用户成功解锁密码/指纹/人脸立刻移除悬浮层 removeOverlayView(); }代码里有三个时间参数值得调500 毫秒是防抖动阈值防止息屏后快速亮屏造成 UI 闪现150 毫秒是亮屏后添加悬浮窗的延迟这个值在普通手机上刚好避开系统窗口刷帧在低端机上可能需要调到 220 毫秒onUserPresent 回调的触发时机和系统解锁方式强相关密码解锁时它会晚于 ACTION_SCREEN_ON 几百毫秒指纹解锁时几乎同时触发。做真机验证时多试几种解锁方式最好让这段逻辑在每种解锁方式下都跑一遍。3.3 处理返回键与 Home 键悬浮窗触摸事件的边界锁屏 UI 出现后用户按返回键会不会直接退到系统桌面是衡量锁屏是否“锁得住”的关键指标。普通悬浮窗 View 默认不接收按键事件你必须给 WindowManager.LayoutParams 设置 FLAG_NOT_FOCUSABLE 的反向组合也就是允许窗口获得焦点并且为 View 注册 OnKeyListener。返回键在这个体系里相对容易处理因为返回键事件会先分发给焦点窗口的 onKeyDown。Home 键则是整个方案中最难缠的悬浮窗无法拦截 Home 键系统把 Home 键事件直接交给 Launcher 处理悬浮窗甚至收不到它的按下回调。行业通行做法是引入无障碍服务AccessibilityService通过服务的 onKeyEvent 监听全局按键事件任何类型的窗口都可以在无障碍服务里消费这个事件从而让 Home 键不触发启动器跳转。public class LockScreenKeyguardService extends AccessibilityService { private boolean isKeyguardActive false; Override public void onAccessibilityEvent(AccessibilityEvent event) { // 监听窗口状态变化用于确认锁屏是否处于展示中 if (event.getEventType() AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { String className ; if (event.getPackageName() ! null) { className event.getClassName() ! null ? event.getClassName().toString() : ; } // 锁屏界面通常是应用内一个独立的 Activity 或 Dialog 容器 isKeyguardActive className.contains(LockScreen); } } Override public boolean onKeyEvent(KeyEvent event) { if (isKeyguardActive event.getKeyCode() KeyEvent.KEYCODE_HOME event.getAction() KeyEvent.ACTION_DOWN) { // 锁屏激活时消费 Home 键阻止系统跳回桌面 return true; } return super.onKeyEvent(event); } }无障碍方案有个前提用户必须在系统设置里为你的 App 手动开启无障碍服务这一步的引导文案要写清楚否则很多用户根本找不到。另外从 Android 10 开始部分系统版本对无障碍服务能消费的按键区域做了限制Home 键的消费行为在不同厂商的 ROM 上不一致适配时要逐个机型验证不能默认行为一致。操作逻辑走到这一步锁屏 UI 已经能挂上、能拦截大部分按键接下来要解决的才是“智能”二字——UI 内容如何根据场景自动变化这是本章没有覆盖到的另一半工作。4. 智能场景联动用状态切换驱动 UI而不是堆动画4.1 用状态表达式描述锁屏场景“智能”在锁屏里最常见的落地方式是场景感知根据一组输入条件决定 UI 展示什么模块、隐藏什么模块、突出什么操作。条件包括当前时间、充电状态、耳机连接状态、前台媒体播放状态、通知优先级等。这些条件如果分散写在各个回调里代码会迅速失控。我一般会设计一个 SceneState 数据类把所有条件收敛成布尔字段再用一组规则判断当前处于什么场景。相比直接判断每个条件场景优先级更好用音乐播放中 充电中 夜间 默认。data class SceneState( val isPlaying: Boolean, // 是否有媒体正在播放 val isCharging: Boolean, // 是否正在充电 val hourOfDay: Int, // 当前小时用于夜间场景判断 val hasNotification: Boolean // 是否存在高优先级通知 ) enum class SceneType { MUSIC, CHARGING, NIGHT, DEFAULT } fun resolveScene(state: SceneState): SceneType { return when { state.isPlaying - SceneType.MUSIC state.isCharging - SceneType.CHARGING state.hourOfDay in 23..24 || state.hourOfDay in 0..5 - SceneType.NIGHT state.hasNotification - SceneType.DEFAULT else - SceneType.DEFAULT } }这段规则代码是“智能”的中枢它的执行时机很关键。不要在每次系统广播时都重新计算一遍场景这会造成 UI 闪烁。我习惯的做法是维护一个 currentScene 变量每当广播事件到达时计算新场景只有新旧场景不一致时才触发 UI 切换否则只更新模块内数据。比如音乐播放时从曲目 A 切到曲目 B仍然属于 MUSIC 场景UI 只更新封面和进度不整套重建。4.2 充电、音乐、夜间模式的联动逻辑三个典型场景能说明这种联动长什么样。充电场景下锁屏 UI 展示大号充电百分比、预计充满时间、充电电流实时变化同时把快捷开关区换成“关闭通知”“开启深色模式”这类省电操作。充电场景的更新频率比其他场景高每 30 秒轮询一次电量就够不要用 BroadcastReceiver 监听 ACTION_BATTERY_CHANGED 高频更新——这个广播的频率不固定电池曲线平稳时几分钟不触发一次充电器抖动时会连续触发UI 很容易跳动。音乐场景下锁屏要展示专辑封面、歌曲名、进度条并提供上一曲/播放暂停/下一曲三个控制按钮。这里的技术重点是获取媒体元数据和发送控制命令。走 MediaSessionManager 获取当前活跃会话注册 Callback 监听播放状态变化通过 session.getController().getTransportControls() 发送控制指令。这里有个常见坑多个媒体应用同时存在时你拿到的不一定是用户实际正在听的会话这时需要增加“最近活跃”判断通过 session 的 playbackState.lastPosition 和 updateTime 筛选出真实活跃的那个。夜间场景的逻辑最简单也最容易踩坑判断时间切深色壁纸只是第一步还要关掉通知预览、调低亮度上限、延长自动息屏时间但不要动系统的自动亮度曲线。如果夜间锁屏重绘整张背景图用户第二天早晨打开手机会因为锁屏壁纸切换产生明显的卡顿这是因为夜间场景的深色位图没有走内存缓存。建议用同一个 Drawable 对象做深浅双色 tint而不是加载两张不同图片。4.3 传感器监听与 UI 抖动的控制参数智能场景如果只用广播驱动变化是粗糙的——用户从口袋拿出手机时光线和距离的变化其实可以触发更精确的 UI 响应。用手势感应器距离传感器能做到“拿出手机自动展开完整锁屏放回口袋自动折叠成简易时钟”这就需要注册 SensorManager 监听距离变化。传感器监听在锁屏上最容易引发的问题是回调过于频繁。距离传感器的 onSensorChanged 在值发生微小波动时就会触发一次如果每次回调都刷新 UI锁屏界面看起来就像在不断抖动。参数控制有两个原则一是只在监听开始时读取当前距离值作为基准后续回调用“变化幅度超过 3cm”才触发动作二是亮屏后的前 2 秒不做任何基于传感器的 UI 切换让系统稳定下来。SensorEventListener proximityListener new SensorEventListener() { Override public void onSensorChanged(SensorEvent event) { float distance event.values[0]; // 阈值过滤只有变化超过 3.0 厘米才触发 UI 更新 if (Math.abs(distance - lastDistance) 3.0f) { return; } lastDistance distance; // 亮屏后 2 秒内忽略传感器事件避免开机帧抖动 if (System.currentTimeMillis() - screenOnTime 2000) { return; } // 距离接近 0 表示遮挡收起信息卡片否则展开完整内容 toggleDetailPanel(distance 3.0f); } };关键参数在这里3.0 厘米是设备公用的遮挡判定阈值大部分手机的传感器量程在 5 厘米左右调太近了容易漏判太远了会在用户正常握持时误触发2 秒是亮屏稳定期这个值可以按设备性能调整低端机调到 3 秒更安全。距离传感器最终返回什么值依赖具体硬件有些设备在无遮挡时返回最大量程比如 5.0有些返回传感器上限值比如 8.0代码里不要硬编码某一个数值要取 onSensorChanged 首次回调时的稳定值做校准。另外一个精密参数是传感器监听器的注册模式。锁屏期间建议使用 SENSOR_DELAY_NORMAL采样频率大约 200ms 一次不要在 UI 切换过程中改成 SENSOR_DELAY_GAME那样会把 100 次/秒的事件全压到主线程界面绘制直接掉到 20 帧以下。不要问我是怎么知道的问就是血泪经验。5. 锁屏实现避坑记录5 个高频翻车点的现象与解法5.1 悬浮窗显示异常的 3 个典型问题现象一锁屏广播正常触发log 也能打印 addView 的调用记录但屏幕上就是看不到自己的 UI。原因多半出在“后台弹出界面”权限被系统拦截。国产 ROM 在检测到应用尝试添加全屏悬浮窗时会把该操作当成后台弹出行为直接丢弃不会抛异常。这是悬浮窗锁屏最常见的拒载方式log 无法直接暴露问题。解决方法是每次 addView 前先检查后台弹出权限。注意这个检查没有官方 API只能通过 AppOpsManager 的 checkOpNoThrow 间接判断。另外一个补救思路是把锁屏 UI 从纯粹的悬浮窗改成“伪 Activity”形式——启动一个透明 Activity主题设置为全屏无动画这样能绕过后台弹出限制但代价是解锁瞬间的 Activity 切换会让 UI 轻微闪一下是否接受需要实测。现象二锁屏页面出现大面积黑屏时间区域是可见的其余内容全黑。原因是锁屏 View 使用了长宽固定的布局没有适配全面屏加挖孔区域的 cutout 安全区。Android 10 以前悬浮窗默认绘制在安全区内挖孔屏不会出问题Android 10 后的设备全面屏铺满全屏悬浮窗不会自动规避状态栏和挖孔区域。解决方法是读取 WindowInsets 的 cutout 信息动态设置 View 的 padding把锁屏的核心内容限制在安全区内背景层继续全屏绘制。如果你在调试时发现某一块永远是黑的不用怀疑就是 cutout 安全区没适配。现象三锁屏拨打紧急电话或查看通知时 UI 底层的窗口层级错乱锁屏显示在系统弹窗之下。原因默认的 TYPE_APPLICATION_OVERLAY 窗口层级低于系统 Dialog。锁屏 UI 被系统弹窗盖住时用户看到的情况就是“通知中心弹出来自己的锁屏消失了”。解决方法是使用 setType(TYPE_APPLICATION_OVERLAY) 后配合 LayoutParams 的 setFitInsetsTypes 调整窗口层级如果系统版本允许可以临时切换成 TYPE_SYSTEM_ERROR 提升层级但这个类型在部分机型上会被安全策略限制只在极端场景下用不要作为默认设置。5.2 解锁成功与失败状态下的同步问题现象四指纹解锁成功自己的锁屏 UI 还残留在屏幕上约 1 秒用户已经进入主界面悬浮窗才消失。原因onUserPresent 广播在部分机型上不受控于支付宝指纹解锁的时序指纹解锁的“成功瞬间”和 ACTION_USER_PRESENT 到达时间存在 200-800ms 的硬件级延迟悬浮层没能及时撤出。解决方法是引入一个“解锁伪完成”快速路径在检测到指纹 HAL 层成功回调可通过 FingerprintManager 的状态回调旁路判断时立即启动一个 80ms 的透明动画把锁屏 UI 淡出同时继续监听 onUserPresent 做兜底清理。这样即使广播延迟用户也感知不到锁屏残留。另外一个兜底技巧是每次 onWindowFocusChanged 回调主界面时主动再调用一次 removeOverlayView。现象五解锁失败指纹错误、密码输错系统锁屏弹提示动画自己的 UI 跟着闪白然后重新绘制布局位置还会偏移几像素。原因悬浮窗在系统锁屏动画过程中收到 KEYGUARD_STATE_CHANGED 广播部分代码在此处触发 addView 重建导致窗口位置被重置。解决方法是解锁失败时不要重建 View用同一个 WindowManager.LayoutParams 调用 updateViewLayout 更新内容保留窗口身份。窗口位置偏移问题通常是 LayoutParams 没有设置 Gravity.TOP加上 FLAG_LAYOUT_IN_SCREEN 后窗口正常情况下会稳定在固定位置。这次的教训是锁屏 UI 的窗口生命周期要和系统 Keyguard 动画保持一致不要频繁 add 和 remove尽量用 updateViewLayout 做内容刷新。6. 进阶用场景优先级重构锁屏逻辑并跑一轮 200 次锁屏循环验证把第 4 章的规则引擎再做一层抽象可以让整个锁屏模块的扩展性上一个台阶。定义一个场景优先级列表每个场景对应一个 UI 控制器注册时声明优先级切换时按优先级从高到低找第一个匹配项。这样你的代码不再是“when 语句堆场景”而是真正的插件式架构——未来加一个“运动场景”或者“会议场景”只需要新增一个 Controller 并注册主轮询逻辑一行不改。interface SceneController { val type: SceneType fun shouldActivate(state: SceneState): Boolean fun onEnter(container: ViewGroup) fun onExit() } class SceneManager( private val controllers: ListSceneController ) { // 按优先级排序数值越大越优先 private val sorted controllers.sortedByDescending { it.priority() } fun dispatch(state: SceneState, container: ViewGroup) { val target sorted.firstOrNull { it.shouldActivate(state) } ?: DefaultSceneController if (target ! activeController) { activeController?.onExit() activeController target activeController.onEnter(container) } } }这个模式的直接收益是当音乐播放中、同时正在充电、又处于夜间UI 只会展示音乐控制器充电信息折叠成一个角标夜间样式作为背景基调。三条规则互不打扰新增场景也不会破坏已有逻辑。把 dispatch 方法的调用放到一个统一入口——比如锁屏主服务收到的所有广播都先汇聚到 SceneManager由它决定 UI 变化与数据刷新各场景控制器之间零耦合。验证这套逻辑有一个可复现的日常习惯用 adb 跑 200 次锁屏循环统计亮屏后锁屏 UI 的显示成功率、平均绘制帧时间、以及残留时间。脚本思路是用 shell 循环交替触发 ACTION_SCREEN_OFF 和 ACTION_SCREEN_ON并通过 dumpsys window 读取当前可见窗口信息作为断言依据。for i in $(seq 1 200); do # 模拟电源键熄屏 adb shell input keyevent KEYCODE_SLEEP sleep 0.8 # 模拟电源键亮屏 adb shell input keyevent KEYCODE_WAKEUP sleep 0.3 # 检查锁屏窗口是否处于 mCurrentFocus不在则记录第 i 次失败 FOCUS$(adb shell dumpsys window | grep mCurrentFocus) if [[ $FOCUS ! *LockScreen* ]]; then echo 第 ${i} 次循环悬浮窗焦点丢失 fi done跑这轮循环时把手机放在桌上不要碰它确保没有传感器干扰。失败率控制在 1% 以内才算这个锁屏能交付。我现在的习惯是每次改完 UI 布局或者触摸逻辑都会先跑一轮这样的循环不是测试功能对不对而是用肉眼盯住屏幕看有没有闪帧。闪帧是锁屏最伤体验的细节肉眼观察比任何性能工具都管用。读到这里的你如果刚准备做一个自研锁屏先从第 2 章的选型对照表开始如果已经在悬浮窗方案里挣扎重点看第 4 章的场景优先级和第 5 章的权限坑。这套路线我验证过多次按它走能少踩一半的厂商适配坑希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑