news 2026/9/6 6:53:17

Android高频面试十题:从Handler到Binder与性能优化核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android高频面试十题:从Handler到Binder与性能优化核心原理

先把这件事说清楚

我这几年一直在做 Android 相关的技术面试官,也在社区里帮不少人做模拟面试。每次问下来,发现一个很有意思的现象:很多候选人项目经历写得很好看,但一聊到基础题,反而支支吾吾。问他 Handler 为什么不会阻塞主线程,能扯到线程池;问他 Activity 启动模式,直接背出四种模式的名字,再问 taskAffinity 和 allowTaskReparenting 的关系就沉默了。

说句实在话,现在的面试风向确实在变,算法题、系统设计题越来越多,但“Android 高频八股文”依然是绕不开的基本盘。尤其对大厂来说,第一轮技术面基本就是靠这些题快速筛人的。原因很简单,八股文背后考察的其实是候选人有没有真正理解 Android 的底层运行机制,是不是只停留在 API 调用层。

这篇文章我把近期面试中最高频、最能区分水平的十道题整理出来,每一道都按“怎么答 + 原理是什么 + 面试官追问方向”来讲。这些题覆盖了组件、消息机制、View 体系、性能优化、Jetpack、跨进程通信等核心领域,适合正在准备面试的 Android 开发者,也适合想系统查漏补缺的朋友。我不保证背完就能进大厂,但至少能让你在基础题环节不拉胯。

1. 十道题概览与自测

先别急着往下看答案,建议你先拿出一张纸,把这十道题默写一遍,能写多少写多少,再对照后文查漏。这个动作比直接看答案高效得多。

序号高频题考察维度
1Activity 启动模式与任务栈组件与任务栈原理
2Handler 消息机制线程通信与阻塞唤醒
3View 的 measure / layout / draw绘制流程与源码追踪
4事件分发机制Touch 事件流向
5RecyclerView 缓存复用列表性能与源码细节
6Binder 跨进程通信进程通信与内核映射
7ANR 的本质与排查稳定性问题定位
8内存泄漏与优化性能优化实操
9ViewModel 与 Lifecycle 原理Jetpack 源码与设计思想
10启动优化与卡顿优化性能优化综合题

从自测情况来说,我见过太多候选人第 1、2、3 题答得不错,但从第 5 题开始就露馅。因为前三题是“背答案能过关”的题,而从第 5 题开始,面试官稍微往深处一问,就需要真正读过源码、做过实践才能接住。如果你自测时发现第 5、6、9 题完全没思路,那这篇文你要重点看这几块。

2. 组件与消息机制:两道必考题

2.1 Activity 启动模式:别只知道四种模式的名字

这是老生常谈的题,但很多人答得很浅。正确姿势是:先说出四种模式,再说它们与任务栈的关系,最后一定要提到 Intent 标志位的覆盖规则,这才是加分点。

四种模式分别是 standard、singleTop、singleTask、singleInstance。standard 是默认模式,每次启动都会创建新实例压入当前任务栈;singleTop 要求栈顶复用;singleTask 是栈内复用,如果目标 Activity 不在当前任务栈,会创建一个新任务栈;singleInstance 则更严苛,整个系统里只有一个实例,且独享一个任务栈。

光背这些不够。面试官一定会问“singleTask 复用后,它上面的 Activity 去哪了”,答案是会被出栈销毁,onNewIntent 会被回调,onCreate 不会再走。这里有个高频延伸考点:singleTask 配合 taskAffinity 使用时,会把任务栈的栈名改成 taskAffinity 指定的名字,这也是为什么很多第三方 SDK 的页面能单独出现在另一个任务栈里的原因。

还有一个常见的坑,就是 Intent 标志位与 launchMode 同时存在时,标志位优先。比如你在代码里同时设置了 singleTop 和 FLAG_ACTIVITY_CLEAR_TOP,系统会以后者为准执行出栈再创建的逻辑。这个问题我每次面试都问,能答对的人不超过三成。

2.2 Handler 机制:为什么主线程 Looper 不会卡死

Handler 是必考题中的必考题,而且面试官通常会从一个反直觉的问题切入:主线程的 Looper.loop() 是个死循环,为什么 App 不会卡死?如果答“因为这个循环在等待消息”,等于没答。

完整答案应该分四层讲。第一层,Handler 发送消息到 MessageQueue,消息队列是一个基于时间优先级的单向链表。第二层,Looper.loop() 不断从队列取消息,取不到就调用 nativePollOnce 进入休眠。第三层,这个休眠不是忙等,而是通过 Linux 的 epoll 机制挂起线程,同时注册了管道用于唤醒。第四层,当 MSG 到达或超时时间到,epoll 唤醒线程,继续取消息执行。

内存模型也是高频追问点。ThreadLocal 保证了每个线程只有一个 Looper,这也是为什么你可以在子线程创建 Handler 前必须先 Looper.prepare() 的原因。再往深处问就是 Handler 内存泄漏问题,非静态内部类持有外部 Activity 引用,导致 Activity 无法回收,这个我在后面第 8 题会细说。

面试官如果心情好,还会让你画一下“子线程到主线程通信”的完整链路:子线程获得主线程 Handler 引用,调用 sendMessage,消息入队,主线程 Looper 被唤醒,执行 handleMessage。能把这个链路画清楚,基本就过关了。

3. View 体系:绘制与事件分发

3.1 View 的 measure / layout / draw 流程

这道题的常规答法是从 Activity.setContentView 开始,经过 PhoneWindow 到 DecorView,再触发 ViewRootImpl.performTraversals,最终进入 measure、layout、draw 三个阶段。

但真正的区分点在于细节。

measure 阶段的核心是 MeasureSpec。它是一个 32 位 int 值,高两位代表模式,低 30 位代表大小。三种模式分别是 UNSPECIFIED、EXACTLY、AT_MOST。父 View 会根据自身的 MeasureSpec 和子 View 的 LayoutParams 共同生成子 View 的 MeasureSpec,然后向下传递。这里面试官常问一个问题:一个宽高为 match_parent 的 LinearLayout,如果父布局是 wrap_content 的 FrameLayout,它实际测量出多大?答案是父布局会先以自己的 MeasureSpec 和子 View 的 LayoutParams 算出子 View 的 MeasureSpec 为 AT_MOST + 父布局当前大小,而不是 EXACTLY。

layout 阶段就是确定位置。ViewGroup 会遍历子 View,调用 child.layout(l, t, r, b) 设置坐标。注意 onLayout 是 ViewGroup 特有的回调,普通 View 没有实际布局逻辑。

draw 阶段分为六个步骤:绘制背景、保存画布图层、绘制自身内容、绘制子 View、绘制装饰(滚动条等)、绘制渐变。很多人会漏掉“保存画布图层”这步,其实这关系到 View 的 fade 效果和 ViewGroup 的裁剪逻辑。

面试官如果想加大难度,会问“自定义 View 时,onMeasure 里一定要调用 setMeasuredDimension 吗”,这个问题可以回答:不调用的话会抛 IllegalStateException,因为 getDefaultSize 不是帮你设置的,而是帮你计算默认尺寸的。

3.2 事件分发:一次完整的 Touch 事件流向

事件分发的源头是 Activity.dispatchTouchEvent,经过 PhoneWindow 到 DecorView,再传给 ViewGroup。核心方法是 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 三个。

三个方法之间的关系可以用一个口诀记住:“从上往下拦截,从下往上处理”。dispatchTouchEvent 负责分发,onInterceptTouchEvent 只存在于 ViewGroup 中,负责决定是否拦截;onTouchEvent 负责真正消费。

常见的追问切入点是“子 View 的 onTouchEvent 返回 false,事件会怎样”以及“一个按钮的点击事件和 onTouch 的先后顺序”。分事件模型的话,需要分成 ACTION_DOWN、ACTION_MOVE、ACTION_UP 三个阶段,如果 DOWN 事件被子 View 消费,后续 MOVE 和 UP 也都会优先传给这个子 View,直到它返回 false 或者父 View 强制拦截。

还有两个进阶点,一个是 requestDisallowInterceptTouchEvent 方法,它可以让子 View 禁止父 View 拦截后续事件,典型的应用场景是 ViewPager 嵌套横向滑动的子列表。另一个是事件机制与点击事件的关联,View.onTouchEvent 中在 UP 事件时会触发 performClick,这也是为什么自定义 View 重写 onTouchEvent 时,lint 建议调用 performClick 的原因。

3.3 RecyclerView 缓存机制:从四级缓存到复用

RecyclerView 的缓存设计是性能优化基础题里的重点,也是我比较喜欢深挖的方向,因为它能直接看出候选人有没有做过真实的长列表优化。

RecyclerView 的四级缓存是:Scrap 缓存、CacheView、ViewCacheExtension、RecycledViewPool。Scrap 缓存用于同一布局中正在被重新布局的 ViewHolder,不需要重新绑定;CacheView 默认容量 2,存放被移出屏幕但还未到回收池的 ViewHolder,复用条件是 position 必须匹配;ViewCacheExtension 是留给开发者自定义的;RecycledViewPool 按照 viewType 复用,需要手动绑定数据。

关于 ViewHolder 的创建和绑定时机,有一个高频追问:同一个 ViewHolder 第一次进入屏幕时,会走 onCreateViewHolder 和 onBindViewHolder;滑出屏幕进 CacheView 后,再滑回来如果 CacheView 里正好有且 position 匹配,只走 onBindViewHolder 做数据绑定。但如果滑出屏幕后缓存超过 2 个,会被移入 RecycledViewPool,此时再滑回来,依然要重新走 onBindViewHolder,这个过程不会重新 onCreateViewHolder。

再深一层,很多人忽略了 RecyclerView 的预取机制(Prefetch)。Google 在 Support Library 25 之后加入了 GapWorker,会在空闲时预取即将进入屏幕的 item,从而避免滑动时的明显卡顿。能答出这一点的候选人,面试官通常会对他的源码阅读能力加分。

4. 进阶主题:进程通信与稳定性

4.1 Binder 机制:为什么是 Android 进程通信的首选

Binder 是 Android 进程间通信的核心,也是面试中最容易冷场的题,因为它牵扯到 Linux 内核、内存映射、代理模式,不只是几个 API 就能糊弄过去的。

答案可以从“为什么不用 Linux 原生 IPC”切入。Linux 传统的 IPC 方式如管道、消息队列、共享内存,要么性能差,要么安全性差。共享内存性能最好,但缺少访问控制,容易越权。Binder 通过 mmap 实现了一次拷贝,发送方把数据拷贝到内核缓冲区,接收方通过内存映射直接读取同一个物理页,避免了第二次拷贝,同时驱动层做了权限校验,比传统的 Unix Domain Socket 更安全。

AIDL 只是 Binder 的上层封装,它生成的 Stub 与 Proxy 是面试重点。Stub 是 Binder 服务端,Proxy 是客户端,客户端调用 Proxy 方法时,会把参数写入 Parcel,通过 transact 发送到内核,服务端在 onTransact 中解析参数并执行真实逻辑。能说清楚“Stub 和 Proxy 其实是同一个 Binder 实体的两种视角”,面试官就知道你真读过了。

追问方向一般是“Binder 拷了几次数据”。很多人答一次拷贝,这是对的,但要补充说明:这个一次拷贝是指用户态到内核态的那一次,读端通过 mmap 直接映射,不需要再次拷贝。如果能再答出“Binder 是 Android 系统里 View、IPC、AIDL、AMS 等所有机制的底层支撑”,分数会更高。

4.2 ANR 的本质与排查思路

ANR 常见五种类型:输入事件超时(约 5 秒)、广播前台超时(约 10 秒)、后台广播超时(约 60 秒)、Service 前台超时(约 20 秒)、ContentProvider 超时(约 10 秒)。数字不是死记硬背的,关键是理解触发机制:主线程在规定时间内没有处理完关键任务,系统会向用户弹窗“应用无响应”。

排查 ANR 的思路,我总结为三步。第一步看 /data/anr/ 目录下的 traces 文件,找到主线程堆栈,看当前卡在哪个方法;第二步看 logcat,搜索 ANR in 关键字,能拿到具体超时类型;第三步结合 CPU 使用率判断,是 CPU 被抢占了还是主线程死锁了。

很多人忽略的一点是 CPU 占比的解读。如果 CPU 整体使用率很高,而主线程堆栈不在执行,那说明主线程被调度延迟,往往是后台任务太多;如果主线程堆栈停在某个同步锁上,那基本是死锁。这两种情况的解决方案完全不一样,前者要优化后台任务优先级,后者要修锁逻辑。

第三问通常是“你在项目里怎么监控 ANR”。这个问题没有标准答案,比较常见的方案是:自定义 Application 的主线程 Handler,post 一个延迟 5 秒的空消息,如果这个空消息被执行了,说明主线程没被卡住;如果执行前被移除,说明主线程在执行其他耗时任务。这种方式虽然粗暴,但确实能在线上环境低成本捕获大多数卡顿。

4.3 内存泄漏:如何在开发阶段就发现

内存泄漏是性能优化的常客,考察点集中在三块:泄漏场景、检测工具、Android 内存模型。

最常见的泄漏场景包括:静态变量持有 Activity 引用、Handler 持有 Activity、内部类或匿名类持有外部类、单例模式持有 Context、资源未关闭(BroadcastReceiver、Cursor、Stream)。回答时建议先分类再举例,这样显得有体系。

检测工具方面,目前主流的是 LeakCanary,但很多人只停留在接入阶段,不知道它的原理。LeakCanary 的核心是 WeakReference 和 ReferenceQueue 的配合:对象被回收前,GC 会把它的弱引用放入 ReferenceQueue;当 Activity onDestroy 后,LeakCanary 会启动一个延迟检测,如果一段时间后弱引用仍然没有被放入 ReferenceQueue,就说明对象没有被回收,接着手动触发 GC 再确认,最后 dump hprof 文件分析引用链。

面试官通常还会追问“dump hprof 后怎么定位”。这个要提到 Android Studio 自带的 Memory Profiler,或者用 MAT(Memory Analyzer Tool)分析 Dominator Tree,找到持有目标的 GC Root 引用链。要注意的是,从开发角度来说,写完代码随手跑一遍 LeakCanary 并不能保证线上不泄漏,线上的做法是接入 Matrix 这类框架做常态化监控,或者定期做整体内存水位分析。

5. Jetpack 与架构:设计思想的追问

5.1 ViewModel 为什么在旋转屏幕后仍然存活

Jetpack 相关题目越来越高频,尤其是 ViewModel、LiveData、Lifecycle 三件套。对于 ViewModel,面试官最爱问的问题是“旋转屏幕后,Activity 重建了,ViewModel 为什么还在”。

答案是 ViewModelStore。Activity 在配置变更时不会销毁 ViewModelStore,而是由系统通过 retain 机制保存下来。在 Activity 的 onRetainNonConfigurationInstance 中,ViewModelStore 被保留,新 Activity 会取回同一个 ViewModelStore,所以 ViewModel 实例不变。

继续追问就往深处走。ViewModel 的作用域是什么?它跟随 ViewModelStoreOwner 的生命周期。为什么 ViewModel 里不能持有 Activity 的 View 引用?因为 ViewModel 的生命周期长于 View,即使屏幕旋转,ViewModel 依然存活,如果持有 View,会导致 View 泄漏。

如果面试官再问 LiveData 与 StateFlow 的对比,这个没有标准答案,但可以从生命周期感知、粘性事件、线程切换三个维度去答。我的建议是不要贬低 LiveData,而是说 StateFlow 在复杂数据流场景下更好用,但 LiveData 与生命周期绑定更直接,小项目中零学习成本。

5.2 Lifecycle 的原理:从注解处理器到 LifecycleRegistry

Lifecycle 组件的原理,考察的是你能不能读懂源码。按照事件流程来说,LifecycleRegistry 是核心实现类,它持有当前状态,通过 addObserver 注册观察者,状态变化时遍历观察者列表,同步状态与事件。

很多人只答到“观察者模式”就结束了。如果再深一层,应该提到 Lifecycle 是如何感知 Activity 生命周期的。Activity 会实现 LifecycleOwner,并在每个生命周期回调里通过 LifecycleRegistry.handleLifecycleEvent 上报事件。LiveData 的 setValue 会触发 onStateChanged,从而回调到观察者的 onChanged。

还有一个比较偏门但常见的追问,是 Lifecycle 是如何保证事件顺序的。答案里要提到 Sync 与 Async 两种事件处理方式,以及 ObserverWithState 的 mLastState。如果状态出现跳变,比如从 RESUMED 跳到 DESTROYED,LifecycleRegistry 会主动将中间事件补发,保证观察者不会漏掉关键生命周期。

6. 性能优化综合:启动优化与卡顿优化

6.1 启动优化:从冷启动到首帧

启动优化是近几年面试必考的实战题,经常和“怎么做线上监控”一起出现。冷启动的时间是从进程创建到第一帧绘制完成,主要分为三个阶段:Application 创建、Activity 创建与绘制、首帧渲染上屏。

常规方案是:Application 的 onCreate 里不要做耗时操作,全部改成延迟初始化或异步初始化;ContentProvider 启动的任务能省则省;用启动器(如 AndroidX Startup)管理初始化任务的依赖关系;首帧之前避免主线程 IO。

更进阶的操作是 Baseline Profile。Google 推荐的做法是通过 Android Studio 的 Baseline Profile Generator 生成 profile 文件,把热启动路径上的类和方法提前做 AOT 编译,减少解释执行的开销。这个方案现在已经成熟,集成成本不高,收益立竿见影。

面试官如果追问“怎么证明你的启动优化有效”,可以答:通过 adb 命令手动统计冷启动时间,或者集成 Matrix 的启动监控,对比灰度前后的数据。有个小细节是,统计口径要统一,一般取“进程创建到 Activity 首帧”的时间,而不是看桌面图标消失的时间。

6.2 卡顿优化:掉帧的原理与定位

卡顿问题的本质是帧率不达标,也就是 16.6 毫秒内没有完成一帧的绘制。面试时很多人一上来就说“避免在主线程做耗时操作”,这是正确的废话,面试官真正想听的是定位手段。

第一层是系统工具。开发者选项里开启 GPU 渲染模式分析,可以看到条形图,但只能定方向,不能定位代码。第二层是使用 Profile GPU Rendering 或者 adb shell dumpsys gfxinfo,可以看到各个阶段的耗时,比如 Draw、Process、Execute 分别占多少。第三层是接入自研或开源的卡顿监控,通过主线程 Looper 的 setMessageLogging 或者 Choreographer 的 FrameCallback 来统计帧率。

从实战经验来看,最常见的卡顿原因集中在三类:主线程做了 IO、View 层级太深导致过度绘制、频繁 GC 导致卡顿。排查时先看 logcat 里有没有 Choreographer 的 skipped frames 日志,再看 TraceView 或 CPU Profiler 里的热点方法。如果堆栈里全是 GC 相关方法,那大概率是对象分配太频繁,可以考虑对象池、StringBuilder 代替字符串拼接等方式。

7. 冲刺建议:最后两周怎么准备

如果你离面试还有两周,我的建议是不要盲目刷题,而是按下面的节奏来安排复习:

  • 第 1 到 2 天:把本文十道题过一遍,确保能不看参考默写核心流程。
  • 第 3 到 4 天:阅读 Handler、Binder、View 绘制、事件分发、RecyclerView 五块关键源码,不用细读每一行,但要能画出关键流程。
  • 第 5 到 6 天:整理自己项目里的性能优化、架构演进、崩溃排查案例,用 STAR 法则写成文字,这个是项目面的弹药库。
  • 第 7 到 8 天:做模拟面试。有条件的话找朋友互问,或者自己对着录音讲,重点练“三分钟讲清一个原理”。
  • 第 9 到 10 天:重点复习 Android 14 的适配点、协程、Compose 这些新方向,很多面试官喜欢用新技术话题来探测候选人的学习能力。
  • 最后 4 天:回归基础,把启动优化、内存泄漏、ANR、组件生命周期过一遍,保持题感。

复习的时候不要只看答案,一定要动手画图。你可以用纸笔画出消息循环的时序图、事件分发的流程图、Binder 的一次拷贝模型、RecyclerView 的缓存流转图。画不出来的地方,就是你没真正理解的地方。

8. 面试问答中常见的坑

最后分享几个我面试候选人时反复遇到的错误示范,这些坑你提前避开,至少能少丢一半印象分。

第一个坑,只回答概念不举例。比如问内存泄漏,只说“静态引用导致泄漏”是不够的,要举出具体代码场景,哪怕是你自己写的小 Demo。面试官想听的是你的排查思路,不是八股文的背诵能力。

第二个坑,不理解就问“是不是这样”,语气不确定。技术面试里,哪怕是猜的,也尽量用“我记得是……再确认一下”这样的表达,而不是全程疑问句,这不只是表达问题,也会让人对你的知识扎实程度产生质疑。

第三个坑,源码含糊其辞。比如被问“onMeasure 里 setMeasuredDimension 不调用会怎样”,很多人会想半天说“好像会报错”,但报什么错、为什么会报错,完全说不清。这类问题面试官不指望你背注释,但要能说出“保存测量结果的变量没有被赋值,最终在 getMeasuredWidth 时返回 0 或者抛出 IllegalStateException”。

第四个坑,项目经验的描述过度包装。简历写“主导了启动优化,优化 50% 启动时间”,但问怎么统计、怎么保证灰度,就答不上来。这种反而比没有项目经验更减分,因为面试官会默认你造假。

说实话,我见过不少候选人,简历很漂亮,但基础题全崩。也见过相反的,项目普通但基础扎实,反而拿到了不错的 offer。面试这件事,说到底是在验证“你能不能干活”和“你遇到问题能不能搞定”。八股文背得再熟,也只能保证你过了第一关,真正能让你拉开的,还是你如何把知识串起来,去解决实际项目的复杂问题。

如果你时间紧迫,优先把 Handler、Binder、View 绘制、事件分发、RecyclerView 这五道题吃透。它们就是 Android 开发的七寸,拿下了,剩下的大部分问题都能触类旁通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 1:05:12

逆变器H桥维修:高压三极管为何不能用普通管代换

逆变器维修中,H桥驱动电路是故障率最高的区域之一。很多维修者拆下原机功率管后,看到一只普通三极管,随手找一只“能开机、能点亮指示灯”的管子替换,结果出现空载正常、带载炸管,或者输出波形严重畸变的情况。这个问题…

作者头像 李华
网站建设 2026/9/5 16:37:56

从零开发免费深度八股文网站:技术面试与内容站搭建全记录

金三银四还没到,身边已经有不少朋友开始焦虑了。每天后台收到的问题都差不多:"Java面试到底背哪些题?""项目问完必问八股,怎么答才能不显得像背书?""有没有免费又能看深度的题库,…

作者头像 李华
网站建设 2026/9/5 16:22:27

23、功耗日志分析与问题定位

遇到功耗异常,第一件事就是打开 kernel log。这里面藏着大量电源管理信息。说白了,系统在睡觉还是醒着,谁把它叫醒了,都能从日志里找到线索。 解读 kernel log 中的电源管理信息 MTK 平台的 kernel log 里,电源管理相关的信息主要分布在几个关键位置。我一般用 dmesg 命…

作者头像 李华
网站建设 2026/9/5 19:04:13

STM32磁悬浮项目从原理到调试:控制算法与硬件设计全解析

简介:这是一份面向高校本科生的嵌入式系统实践资源,专为毕业设计与课程作业场景打造,聚焦基于STM32的磁悬浮控制系统开发,覆盖电磁驱动、闭环控制算法实现与硬件协同调试等核心难点。压缩包共8个文件(2.37MB&#xff0…

作者头像 李华
网站建设 2026/9/5 10:13:30

AI论文阅读高效方法梳理 快速掌握前沿学术要点的实用指南

最近,国家自然科学基金和国家自然科学基金青年科学基金的评审结果陆续公布。有人成功获批,开始准备后续研究;也有人暂时没有通过,需要根据评审意见重新梳理研究方向和申请书。无论结果如何,基金申请都不是临时抱佛脚&a…

作者头像 李华
网站建设 2026/9/6 10:45:09

工艺卡片系统数据库设计:从表结构到事务处理全解析

简介:基于ASP.NET(C#)与SQL Server的工艺卡片管理系统,作为数据库课程设计项目,面向计算机相关专业学生,定位为一个可直接运行、可用于课程验收的完整参考方案。系统围绕工艺流程记录、工位设备数据、参数标准等核心信息建立数据表…

作者头像 李华