拓冰建站拓冰建站
首页 / 资讯中心 / 正文

小满Android研发岗笔试复盘:从四组件到构建工具链的思维升级

收到小满春招Android研发岗第一批笔试邀请的时候我正在一家中厂做应用开发日常主要写业务组件和性能优化自认为Android基础还算扎实。但看到HR发来的在线笔试链接用90分钟做完之后我发现自己对“Android开发”的理解还是太窄了。这套卷子不是常见的“背八股”而是把四大组件、Handler、Framework、Binder、构建工具链、甚至OTA和底层系统组件全部揉在一起考很多题看着眼熟真正落笔才发现自己只是“知道”而不是“理解”。考完和朋友对完答案趁着记忆还热乎我把它整理成一份回忆版复盘希望能给后面投递小满或者其他中大型公司Android岗位的同学一点参考。1. 笔试总览先看清这套卷子的“出题脾气”1.1 考试安排与题型分布小满的笔试是在一个在线考试系统里完成的整体时长90分钟总分100分题量不小。我看了一眼大致分布题型可以分成四块题型题量大致分值主要考察方向单选题10题每题3分Java/Kotlin语言、四大组件、Handler、进程优先级多选题5题每题5分Binder、View绘制、构建工具链、广播机制简答题3题每题10分AMS/WMS职责、AGP版本兼容、R8混淆策略开放设计题1题15分系统升级/蓝牙音频/文件共享等真实场景设计这个结构其实已经暗示了这家公司的技术栈倾向不是单纯的业务开发而是很看重候选人对Android整个系统体系的理解。单选题里大量出现“以下哪个进程优先级最高”“Handler的MessageQueue在什么时候阻塞”多选题里频繁出现“R8会做哪些事情”“AGP 8.0带来了哪些破坏性变化”。如果你平时只用Android Studio点Run、靠AS自动打Release包这些题基本靠蒙。1.2 从高频搜索词反推考点覆盖这里有个很有意思的细节。考试前后我在Android开发者社区看到很多人搜索“android studio hedgehog支持agp8吗”“android r8”“android ams”“android ota”“android apex”这类关键词说明这套题确实打中了很多人的盲区。我当时也顺手搜了不少结合回忆的试题整理出一张考点地图语言基础约20%Kotlin的lateinit和by lazy、协程调度器、Java线程池四组件与消息机制约30%Activity生命周期与进程优先级、Handler/Looper/MessageQueue、ContentProvider和FileProvider、BroadcastReceiverFramework与IPC约25%AMS/WMS、Binder传输限制、Zygote/SystemServer启动链构建与工程化约15%Android Studio版本与AGP对应关系、R8和ProGuard差异、Resource适配开放场景约10%OTA升级回滚、蓝牙音频焦点、Android 11文件分享说实话前两项是传统笔试重点多数Android开发者都能答个七七八八。但后三项明显偏系统向和工程化如果你平时只做应用层不关注Gradle插件版本、不研究R8规则、不读AOSP源码很容易在这几块翻车。这也是我写这篇复盘的核心原因三年前我自己面试的时候考的还是“Activity四种启动模式”“handler内存泄漏为什么”现在这批题已经明显换血了。2. 选择题复盘那些让人纠结的“送分题”与“陷阱题”2.1 进程优先级、生命周期和回收顺序单选题第一题就问“用户按Home键之后App内没有可见Activity、没有前台服务、没有正在运行的广播系统内存吃紧时这个进程属于哪个优先级”我差点选了“后台进程”就提交后来仔细看了一眼选项发现还有“缓存进程”这个词。Android的进程优先级从高到低大致是前台进程、可见进程、服务进程、后台进程、空进程。按Home键后Activity走onPause和onStop此时如果没有其他组件进程确实属于后台进程。但如果Activity只是被对话框遮挡了一部分还处在onPause但没有onStop的状态那就属于“可见进程”优先级会高很多。这道题真正想考察的是你能不能把Activity生命周期和进程五级模型对应起来而不是背出五个名词。我答题的时候用的方法是先画生命周期图再填优先级onResume对应当前可交互属于前台进程onPause未onStop窗口仍可见属于可见进程onStop之后才降级到后台进程。如果你的App里同时有一个正在播放音乐的MusicService那进程会被拉到“服务进程”档比后台进程不容易被回收。这个知识点在工作中同样重要很多“App被杀”“WebView频繁重建”的问题归根结底都是进程优先级和组件生命周期没有配合好。2.2 FileProvider与content://协议的前世今生有一道题出得很实际“应用A要把一张图片分享给应用BA使用了FileProvider最终分享出去的URI是什么格式为什么不能直接传file:///storage/emulated/0/...路径”选项里有content://、file://、http://、content://com.xxx.fileprovider/...。这个知识点一定要和Android 7.0的行为变更绑在一起记。从Android 7.0开始应用间直接暴露file://URI会直接抛FileUriExposedException因为系统认为这种方式不安全——file://暴露的是绝对路径接收方拿到后可以绕过权限访问同一个文件的其他部分而且很容易被恶意应用利用。FileProvider本质上是ContentProvider的子类它把底层文件路径映射成一个content://URI同时通过Intent的FLAG_GRANT_READ_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSION临时授权给接收方。我答完这道题后专门去翻了一下真实App的manifest很多大厂的FileProvider authority都是“包名.fileprovider”比如你在搜索引擎里偶尔能看到类似content://com.xxx.searchbox.fileprovider/baiddpath/...这样的日志这就是FILE provider在做跨进程文件分享时留下的URI痕迹。实际配置的时候要在AndroidManifest里声明androidx.core.content.FileProvider然后在res/xml里放一个file_paths.xml把需要暴露的目录列出来不能图省事直接暴露整个外部存储。2.3 Kotlin/协程细节题lateinit、by lazy和Dispatchers.IO这套卷子对Kotlin的考察不是简单语法而是偏“什么时候选择哪个”。有一道多选题问“关于lateinit和by lazy的说法哪些正确”选项里有“lateinit适用于varby lazy适用于val”“lateinit不能用于Int等基本类型”“by lazy默认线程安全”“两者都是编译期检查”。前三个选项都是对的。lateinit编译期只是做了声明运行时如果没初始化就访问会抛UninitializedPropertyAccessException它没法用于基本类型因为基本类型有默认值无法通过“是否初始化”来做区分。by lazy是委托属性第一次访问时才执行初始化代码默认使用LazyThreadSafetyMode.SYNCHRONIZED底层有锁线程安全。这个题区分度很高因为很多人只记得“lateinit用于varlazy用于val”不知道基本类型限制和线程安全细节。协程那道题则考到Dispatchers.IO的底层实现“频繁使用Dispatchers.IO会不会耗尽线程资源”说实话这种题如果没读过源码很容易凭感觉答。Dispatchers.IO底层是一个按需伸缩的线程池和Dispatchers.Default共用了协程调度器里的机制但IO调度器会根据任务需要创建新的线程极限情况下线程数量可以很大所以它适合IO密集任务但不能拿来做CPU密集运算。我当时的答题策略是强调“IO调度器是弹性线程池Default是固定为CPU核心数的线程池两者适合的场景不同”这个答案在面试里也经常被追问建议系统看一下kotlinx.coroutines里CoroutineScheduler的实现。3. Framework题解析AMS、Binder和系统启动链路3.1 AMS/WMS职责边界别把“管理Activity”和“管理窗口”混为一谈简答题第一题我记得很清楚“简述ActivityManagerService和WindowManagerService各自的职责以及它们在一次Activity启动过程中如何配合。”这题看起来基础实际上对源码没读过的人很难答出层级感。AMS负责的是“Activity的一生”包括组件调度、生命周期状态机、任务栈管理、进程优先级更新。ActivityThread启动后通过Binder拿到AMS的代理然后调用ApplicationThread的scheduleLaunchActivity最终回到主线程的Handler执行启动流程。WMS负责的是“窗口的一生”每个Activity对应一个PhoneWindow和DecorViewWMS管理窗口的层级、尺寸、焦点以及输入事件分发。两者的配合点在于Activity启动时AMS会先确保进程存在再通知ActivityThread创建Activity而Activity创建后调用WindowManager.addView这个过程会跨进程通知WMS添加窗口。窗口加完之后才能开始测量、布局、绘制。我在答题时特意强调了“AMS是逻辑管理者WMS是界面呈现者”并且补充了从Android 10开始Activity任务栈相关职责下沉到ActivityTaskManagerService这一句话在简答题里很加分因为大多数人的知识体系还是停在AMS只用来管Activity的旧版本。3.2 Binder传输限制与TransactionTooLargeException有一个多选题直接问“Binder传输的数据量限制是多大避免TransactionTooLargeException的正确做法有哪些”我记得选项里有1MB、4KB、64KB、无限制以及“避免在Intent里塞大Bitmap”“用文件或共享内存代替跨进程大数据”“用oneway异步消息”……Binder事务缓冲区是通过mmap映射的默认大小是1MB但这个1MB不是让你随便用的系统本身需要保留一部分空间所以每次事务的实际可用上限会小于1MB。如果你把一张2MB的Bitmap塞进Bundle再通过Intent传给另一个Activity很可能就爆了。有人会说“那我把Bitmap放在一个静态变量里不就行了”这在跨进程场景下根本不管用因为静态变量只在当前进程可见。正确做法是将Bitmap先存成文件然后传URI接收方再解析。至于oneway它确实是异步Binder调用发送方调完立刻返回不阻塞但接收方不能返回结果。这道题我给的建议是凡是跨进程传数据第一反应先想大小超过几百KB就换文件或共享内存。这个习惯在很多公司实际代码评审里也是检查重点。3.3 Zygote、SystemServer与OTA、APEX的底层关联这套卷子还有一道“超纲”多选“关于Android系统启动过程下列说法正确的是”选项有“Zygote由init进程启动后创建SystemServer”“SystemServer里面运行着AMS、WMS、PackageManagerService等服务”“APEX包可以用于OTA更新系统组件”“OTA升级失败后系统必须整机重装”。前三个是对的第四个是错的。Zygote由init进程启动它预加载了很多类和资源后续所有应用进程都由它fork出来SystemServer是Zygote fork的第一个Java进程在这个进程里会启动SystemServiceManager然后创建各种核心服务包括我们前面说的AMS、WMS、PackageManagerService等。APEX和OTA的关联是我当时复习时漏掉的部分。Android 10开始引入APEX格式它可以看成是“可更新的系统组件包”但和普通APK不同APEX可以被Linux内核直接挂载系统适配后能通过OTA机制单独更新一些底层模块比如媒体编解码器、Conscrypt安全组件不需要完整整机升级。OTA则是整个系统的升级通道现代设备一般用A/B分区方案新系统写到备用分区重启后切换如果启动失败会自动回滚到旧分区。这题让我意识到Android研发岗的笔试边界正在从“应用开发”慢慢扩大到“系统开发”尤其是有车机业务的公司这种题会越来越多。4. 工程化考点AGP版本、R8混淆和构建脚本里的坑4.1 Android Studio版本与AGP版本兼容表工程化方向的简答题是“你正在使用Android Studio Hedgehog 2023.1.1 Patch 2项目是否可以直接使用AGP 8.x为什么”这题考察的不是手机号一样的硬背而是构建工具链的依赖关系。先给一个我当时整理的对应表Android Studio版本AGP推荐版本最低Gradle版本需要JDKAndroid Studio FlamingoAGP 8.0Gradle 8.0JDK 17Android Studio GiraffeAGP 8.1Gradle 8.0JDK 17Android Studio HedgehogAGP 8.2Gradle 8.2JDK 17Android Studio IguanaAGP 8.3Gradle 8.4JDK 17Hedgehog 2023.1.1 Patch 2这个IDE版本按Google的兼容策略可以支持的AGP范围主要是8.1到8.2虽然理论上你手动指定到AGP 8.3甚至更高也能跑但官方兼容性测试一般只覆盖到推荐版本超过之后容易出现构建缓存、IDE侧插件不匹配等奇怪问题。所以我的结论是可以用AGP 8.x推荐直接用8.2配合Gradle 8.2和JDK 17这是最稳的组合。如果你是在老项目上升级AGP需要注意AGP 8.0开始有几条硬性变化AndroidManifest里的package不能再写了必须移到build.gradle里的namespaceR类不再可传递子模块必须通过显式import访问其他模块的R默认开启R8而且内置的Kotlin支持会让你重新调整kotlin-android插件的配置方式。这些不是“选做题”是很现实的升级坑我身边好几个同事升级完项目编译报错都是卡在namespace和R类这些点。4.2 R8和ProGuard别再把它们划等号简答题里有一道是“R8在Release构建中做了哪些事情如果某个类因为反射被误删你如何保留”我答题时先讲概念R8不是一个单纯的混淆器它在执行时同时负责压缩shrinking、优化optimization、混淆obfuscation和脱糖desugaring。ProGuard主要就是压缩和混淆R8在AGP 8.0之后是默认的代码处理器并且和D8/R8工具链深度集成。如果遇到反射调用最容易踩的坑是Java/Kotlin代码里通过字符串反射拿到一个类的方法R8只看静态调用关系不知道这个字符串对应的类名于是把目标类当作无用类删掉。这时候要在proguard-rules.pro里加keep规则-keep class com.example.api.** { *; } -keepattributes Signature -keep class **.R$* -keepclassmembers class com.example.api.User { public methods; }-keep会把匹配到的类及其成员全部保留-keepclassmembers只保留被指定的成员。加规则之后要用mapping.txt和usage.txt去验证最终包里的类是否还在release包崩溃时也要能根据mapping反混淆堆栈。很多开发者在本地跑没问题上了线上就崩最后导出mapping查一下基本都是keep规则没写全。还有一个冷知识点R8的优化模式比ProGuard激进如果代码里大量使用Gson/Jackson等基于反射的序列化库一定要给模型类加上keep规则。我记得网上有人遇到某个版本更新后release包突然解析不了服务端返回的数据最后发现是R8优化把Gson泛型签名信息给剥离了导致TypeToken匹配失败。所以在工程化方向我的建议是每次升级AGP后用release包做一轮全量回归重点检查第三方SDK的动态初始化。4.3 主题图标、动态图标和Resources适配选择题里有一道是“Android 13上用户开启主题图标Themed Icons后系统用的是什么机制”代码库里有AndroidManifest里的adaptive-icon定义我答了这个机制。这里要展开讲一下自适应图标分为三层background、foreground以及Android 13新加的monochrome。monochrome层是单色矢量图形系统在用户开启主题图标时会根据壁纸取色对monochrome做着色替换掉原来的彩色图标形成桌面统一色调。如果你的App只提供了background和foreground而没有monochrome在Android 13的Pixel设备上主题图标会失效桌面仍然显示旧图标。如果你看到“android动态图标主题”这个热词它其实还包括两类东西一类是ShortcutManager动态快捷方式可以在长按图标时创建动态入口另一类是应用内通过AlarmManager定时切换桌面图标Icon这种玩法在日历、天气类App里比较常见。但系统层面真正的新能力是monochrome和主题图标这两个概念容易混建议在简历里不要写“支持动态图标”这种模糊话术要说清楚是主题图标还是shortcut。5. 场景设计题从文件分享、OTA到蓝牙音频切换5.1 Android 11以上实现文件下载与分享的完整思路开放设计题给了一个非常具体的场景“用户下载了一份PDF点击预览时希望调用系统或其他应用打开请你设计实现方案并说明不同场景下的权限适配。”这道题没有标准答案但得分点非常清晰。我当时的思路分三层第一层是“下载到哪”第二层是“如何授权给其他应用”第三层是“如何兼顾不同Android版本”。下载到哪普遍做法是把文件写到getExternalFilesDir()应用专属目录好处是不需要申请存储权限卸载App时系统会自动清理坏处是其他应用默认无法访问。如果要写进公共Downloads目录就需要MediaStore.DownloadsAndroid 10及以上可以用MediaStore插入Android 11及以上推荐用MediaStore.Downloads这个集合。这个差异本身就是考点很多人会直接写“申请权限然后写公共存储”这在Android 11之后已经被系统限制得很严正确姿势是先插入MediaStore再拿Uri通过OutputStream写内容。打开和分享时最稳妥的方案是把文件Uri交给FileProvider生成权限URI再通过Intent的setDataAndType配合Intent.FLAG_GRANT_READ_URI_PERMISSION启动第三方Viewer。如果目标是预览也可以用Intent.ACTION_VIEW如果需要持久化内容可以考虑使用SAFStorage Access Framework让用户指定目录。我答题的时候还特意提到了一个权衡点临时分享给其他应用一次用FileProvider最合适如果要让用户长期管理文件用MediaStore和SAF更好千万不要再直接使用file://这在Android 7.0以后就是非法路径。5.2 面向车机/系统开发OTA失败回滚与i2c-tools小满这套卷子里还有一道“另类”的开放题“在一个已量产的Android车机系统上你想通过OTA推一个新版本如果新版本启动失败如何保证设备不变成砖”这个题明显是在招系统方向或者偏底层的人。我当时的回答分三步第一步分区方案。现代Android设备基本都会用A/B分区系统固件同时存在A槽和B槽OTA把新版本写入当前未使用的槽位。升级完成后重启引导器切换到新槽位启动如果启动失败或者连续几次启动没有进系统引导器会自动切换回旧槽位。这个机制对应的HAL接口是boot_control它还负责记录当前槽位和重试次数。第二步更新引擎。update_engine在写入新版本前会做签名校验、dm-verity校验和OTA包完整性校验。车辆场景下更加严格因为车内电子控制单元ECU和IVI系统常常需要联动所以一般会要求升级过程不能断电并且要有擦除后恢复的能力。第三步应用层回滚。就算系统分区启动成功了App层也可能因为数据库版本不兼容导致业务崩溃所以车机OTA方案里通常会设计“软件包版本和业务数据版本”一起校验发现业务数据无法兼容时恢复到上一个版本的备份数据。这个设计在手机厂商的云端升级中也很常见。我在回答里还提到了一个工程工具i2c-tools。在Android系统上如果要做硬件调试比如读取屏幕、触摸芯片、传感器寄存器通常需要把i2c-tools交叉编译进系统镜像然后在adb shell里使用i2cdetect、i2cget、i2cset这些命令。如果你有“android openocd”“android ota”这些搜索词出现在面试记录里说明对方在考察你愿不愿意往下钻到硬件层。这个方向不一定人人要做但系统工程师岗位基本必问。5.3 蓝牙音频切换和AudioFocus的联动还有一道开放题是“音乐类App在用户连接或断开蓝牙耳机时如何做到自动暂停/恢复播放”这题结合了设备监听和音频焦点非常好地考察了实际开发能力。我的方案分成两个部分。第一部分是监听音频设备变化使用AudioManager的registerAudioDeviceCallback当音频设备列表变化时回调里检查当前是否有蓝牙A2DP设备处于连接状态。如果从有线耳机切到蓝牙耳机或者从蓝牙切回扬声器App会收到通知然后决定暂停还是恢复播放。第二部分是处理AudioFocus。播放音乐前应该先申请音频焦点使用AudioManager.requestAudioFocus并且处理OnAudioFocusChangeListener回调。如果焦点被其他App抢走比如导航语音开始播报音乐应该暂停焦点暂时丢失但允许duck可以降低音量焦点重新被授予时再恢复播放。蓝牙设备断开本身也会触发系统音频焦点变化很多手机在蓝牙断开时会暂停当前媒体播放App层如果不对焦点做处理会出现播放器状态和系统音量状态不一致的bug。我给的代码关键伪码大致是这样AudioManager am (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); am.registerAudioDeviceCallback(new AudioDeviceCallback() { Override public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { // 检查是否有TYPE_BLUETOOTH_A2DP // 有则播放 } Override public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { // 蓝牙设备移除主动暂停播放 } }, null); AudioFocusRequest request new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()) .setOnAudioFocusChangeListener(focusChangeListener) .build(); am.requestAudioFocus(request);这个场景我还特意联系了一下热词“android蓝牙”因为在蓝牙耳机连接状态下出现“音量自动跳变”“播放暂停异常”这类问题十有八九不是设备的问题而是App没有处理好音频焦点和音频设备回调的联动。这种题就是用来区分“会写代码”和“会处理真实问题”的。5.4 CoordinatorLayoutBanner嵌套滚动的经典实战最后一道代码相关的题目是“页面顶部有一张Banner下面是一个信息流列表要求上滑列表时Banner先被收回下拉时再展开你会怎么做”这题一看就是CoordinatorLayout AppBarLayout CollapsingToolbarLayout RecyclerView的组合。关键点在于滚动联动机制而不是说你直接把布局文件里写上CollapsingToolbarLayout就完事。要理解原理必须先理解嵌套滚动机制。CoordinatorLayout是通过Behavior实现联动规则的AppBarLayout有一个默认的ScrollingViewBehavior它会监听RecyclerView滚动同时AppBarLayout内部的子View通过layout_scrollFlags声明自己如何响应滚动。典型的配置是Banner所在的那个CollapsingToolbarLayout设置app:layout_scrollFlagsscroll|exitUntilCollapsed下面的RecyclerView设置app:layout_behaviorstring/appbar_scrolling_view_behavior这样才能把滚动事件传给AppBarLayout。实际开发中会踩到两个坑一是ViewPager2和CoordinatorLayout的嵌套滚动存在冲突因为ViewPager2本身也是NestedScrollingChild它会把子RecyclerView的滚动事件“吞掉”导致顶部的AppBarLayout不响应解决方法一般是自定义Behavior或者在ViewPager2之外单独处理滚动。二是Banner内部的轮播定时器在页面滑动时容易误触常用做法是在列表滚动时暂停自动轮播停止滚动后恢复需要复写RecyclerView的addOnScrollListener做一个标志位。这道题如果你能答出Behavior、scrollFlags、嵌套滚动事件分发的调用链基本能拿到大部分分数。6. 考后复盘这套题透露的Android招聘风向6.1 哪些题是真实业务里每天都能用上的考完回顾一下这套卷子的设计比很多“纯八股”笔试要认真得多。FileProvider、Binder传输限制、R8混淆、协调布局Banner这些是日常开发里高频遇到的东西任何一个有三年经验的Android开发都不可能避开。它们真正想考察的不是“背答案”而是你有没有在真实项目里思考过“为什么”。比如Binder传输限制大多数人只在报错的时候才去查TransactionTooLargeException但如果你在设计AIDL接口时就已经知道跨进程数据量有限制你会主动把Bitmap转成文件路径再传递这种意识就是区分普通开发者和高级开发者的地方。再比如R8混淆本地Debug跑得好好的Release一打开闪退很多新手第一反应是“写得好好的怎么崩了”老手会直接看mapping.txt定位到被混淆的类这就是能不能把构建工具链吃透的差距。6.2 给下一批投递者的备考建议如果你准备投递Android研发岗尤其是小满这种明显带系统倾向的岗位我建议按下面几条查漏补缺语言基础别只刷Java面试题Kotlin协程要读底层源码。线程池和协程调度器的区别、Dispatchers.IO的实现已经在笔试题里反复出现。四大组件、Handler、Binder三件套必须能联系源码讲清楚。从Activity启动到窗口显示中间经过AMS/WMS/Binder这条链路要能画出来。工程化能力要跟上。Android Studio版本、AGP版本、Gradle版本、JDK版本这一套依赖关系至少要有一张自己的兼容表R8和ProGuard规则至少要会写keep。系统方向的知识不是超纲。OTA、APEX、启动流程、文件系统权限这些开始成为中大型公司Android岗位的正常考察范围尤其是车机、系统组、性能优化组相关岗位。实战场景题最好提前准备一套自己的套路。文件分享、音频焦点、嵌套滚动、数据恢复每个方向都整理一个“从问题出发、到方案设计、再到版本适配”的回答框架比临时组织语言强很多。最后再分享一个从这次笔试里悟出来的土办法不要只看《Android开发艺术探索》和面试题至少要把自己机器上的Android SDK目录翻一翻读一读android.jar里的系统类源码把LayoutInflater、Handler、Binder这些类的注释看一遍。很多笔试里你觉得偏、觉得难的知识点其实AOSP源码和官方文档里都写得很清楚只是以前没花时间去看。小满这套题不算刁钻但它逼着你把“Android开发”从一个IDE按钮变成了一整个系统这种思维转变可能比笔试成绩本身更有价值。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门