【Android进阶】Android热门原理流程总结

本文介绍了在Android应用层开发过程中,比较重要的运行流程总结
JVM内存模型

- 程序计数器:一小块区域, 线程私有 。记录了每个线程的代码执行到了哪一行,各种循环,判断都是通过这个区域存的数值来走的。Java多线程是时间分片,各个线程在一段时间内占用这个核来执行任务,这个线程切换到另一个线程,其恢复的依据也是计数器的值。
- 虚拟机栈:周期与线程相同,也是 线程私有 。每个方法执行时,都会创建一个栈帧, 栈帧里面存储方法内的局部变量表,方法出口等等信息 。每个方法执行到退出的过程,就是一个个的方法栈帧入栈出栈的过程。这个区域有两个异常,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError异常;如果JVM允许动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。 - 本地方法栈:和虚拟机栈作用一样,但是服务于 本地的Native方法 。同样会抛出上面的两种异常。
- Java堆:最大的一块,所有 线程共享 的数据。几乎所有的对象实例都在这里保存。Java堆是垃圾收集器管理的内存区域。Java堆可以处于物理上不连续的内存空间中,可以选择固定大小或可扩展。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出
OutOfMemoryError异常。 - 方法区: 线程共享 。用于存储已被虚拟机加载的 对象类型信息、常量、静态变量、即时编译器编译后的代码缓存 等数据。对其要求比较宽松,几乎不用考虑垃圾回收,但是回收也是有必要的,主要针对常量的回收和类型卸载。如果方法区无法满足新的内存分配需求时,将抛出
OutOfMemoryError异常。- 运行时常量池,其是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行期间也可以将新的常量放入池中。
类加载流程
父子类加载的具体流程
- 加载阶段
- 父类优先:当加载一个类时,JVM会先检查其父类是否已加载
- 递归加载:如果父类未被加载,则会递归加载父类及其父类,直到Object类
- 子类后加载:所有父类加载完成后,才开始加载子类
- 准备阶段
- 父类优先:为父类的静态变量分配内存并设置默认值
- 子类后处理:然后为子类的静态变量分配内存并设置默认值
- 初始化阶段
- 父类优先:执行父类的静态代码块和静态变量赋值
- 子类后处理:然后执行子类的静态代码块和静态变量赋值
实例化时的加载顺序
当创建子类实例时,加载顺序如下:
- 父类静态成员和静态块(只在第一次加载类时执行一次)
- 子类静态成员和静态块(只在第一次加载类时执行一次)
- 父类实例变量初始化
- 父类构造代码块
- 父类构造函数
- 子类实例变量初始化
- 子类构造代码块
- 子类构造函数
代码示例
class Parent {
static {
System.out.println("Parent静态代码块");
}
{
System.out.println("Parent构造代码块");
}
public Parent() {
System.out.println("Parent构造函数");
}
}
class Child extends Parent {
static {
System.out.println("Child静态代码块");
}
{
System.out.println("Child构造代码块");
}
public Child() {
System.out.println("Child构造函数");
}
}
public class Test {
public static void main(String[] args) {
new Child();
}
}
输出:
Parent静态代码块 Child静态代码块 Parent构造代码块 Parent构造函数 Child构造代码块 Child构造函数
设计模式
见另一篇详细文章: 设计模式
垃圾回收流程
JVM
根搜索算法(GC ROOT Tracing)
Java中采用了该算法来判断对象是否是存活的,也叫可达性分析。
通过一系列名为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论来说就是从GC Roots到这个对象不可达)时,则证明对象是不可用的,即该对象是“死去”的,同理,如果有引用链相连,则证明对象可以,是“活着”的。
哪些可以作为GC Roots的对象呢?Java 语言中包含了如下几种:
1)虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2)方法区中的类静态属性引用的对象。
3)方法区中的常量引用的对象。
4)本地方法栈中JNI(即一般说的Native方法)的引用的对象。
5)运行中的线程
6)由引导类加载器加载的对象
7)GC控制的对象
回收流程
现代商用虚拟机基本都采用分代收集算法来进行垃圾回收,当然这里的分代算法是一种混合算法,不同时期采用不同的算法来回收。
由于不同的对象的生命周期不一样,分代的垃圾回收策略正式基于这一点。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。该算法包含三个区域:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。

年轻代(Young Generation)
所有新生成的对象首先都是放在年轻代中。年轻代的目标就是尽可能快速地回收哪些生命周期短的对象。
新生代内存按照8:1:1的比例分为一个Eden区和两个survivor(survivor0,survivor1)区。
- Eden区,字面意思翻译过来,就是伊甸区,人类生命开始的地方。当一个实例被创建了,首先会被存储在该区域内,大部分对象在Eden区中生成。
- Survivor区,幸存者区,字面理解就是用于存储幸存下来对象。
回收时机:
- 一开始都在Eden区里,当Eden快满了就触发回收,之后,先将Eden区还存活的对象复制到一个Survivor0区,然后清空Eden区。
- 当这个Survivor0区也存放满了后,则将Eden和Survivor0区中存活对象都复制到另外一个survivor1区,然后清空Eden和这个Survivor0区,此时的Survivor0区就也是空的了。
- 然后将Survivor0区和Survivor1区交换,即保持Servivor1为空,如此往复。
这种回收算法也叫 复制算法 ,即将存活对象复制到另一个区域,然后尽可能清空原来的区域。
新生代发生的GC也叫做 Minor GC ,MinorGC发生频率比较高,不一定等Eden区满了才会触发。
为什么设置两个survivor区域?
如果只有一个eden区和一个survivor区,那么假设场景,当发生ygc后,存活对象从eden迁移到survivor,这样看好像没什么问题,很棒,但是假设eden满了,这个时候要进行ygc,那么发现此时,eden和survivor都保存有存活对象,那么你是不是要对这两个区域进行gc,找出存活对象,那么你想想是不是难度很大,还容易造成碎片,如果你使用复制算法,那么难度很大,那么耗时很长。如果你使用标记清除算法,那么很容易造成内存碎片。
所以如果存在两个survivor区,那么工作就非常的轻松,只需要在eden区和其中一个survivor(b1)找出存活对象,一次性放到另一个空的survivor(b2),然后再直接清除eden区和survivor(b1),这样效率是不是就很快了。
年轻代往老年代转移的条件
- 有一个JVM参数
-XX:PretenureSizeThreshold,默认值是0,表示任何情况都先把对象分配给Eden区。若设置为1048576字节,也就是1M。则表示当创建的对象大于1M时,就会直接把这个对象放入到老年区,就根本不会经过新生区了。这么做的原因: 大对象在经历复制算法进行GC的时候会降低性能 。 - 如果新生区中的某个对象 经历了15次GC 后,还是没有被回收掉,那么它就会被转入老年区。
- 如果当Survivor1区不足以存放Eden区和Survivor0的存活对象时,就将存活对象 直接放到年老代 。
如果年老代也满了,就会触发一次Major GC(即Full GC),即新生代和年老代都进行回收。
年老代(Old Generation)
在新生代中经历了多次GC后仍然存活的对象,就会被放入到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
年老代比新生代内存大很多(大概比例2:1?),当年老代中存满时触发Major GC,即Full GC,Full GC发生频率比较低,年老代对象存活时间较长,存活率比较高。
一开始对象都是任意分布的,在经历完垃圾回收之后,就会标记出哪些是存活对象,哪些是垃圾对象,然后就会把这些存活的对象在内存中进行整理移动,尽量都挪到一边去靠在一起,然后再把垃圾对象进行清除,这样做的好处就是避免了垃圾回收后产生的大片内存碎片。
即此处采用的叫 Compacting 算法,由于该区域比较大,而且通常对象生命周期比较长,compaction需要一定的时间,所以这部分的GC时间比较长。较为耗时,比复制算法慢10倍;
所以如果系统频繁出现Full GC,会严重影响系统性能,出现卡顿。所以JVM优化的一大问题就是减少Full GC频率。
持久代(Permanent Generation)
持久代用于存放静态文件,如Java类、方法等,该区域比较稳定,对GC没有显著影响。这一部分也被称为运行时常量,有的版本说JDK1.7后该部分从方法区中移到GC堆中,有的版本却说,JDK1.7后该部分被移除,有待考证。
DVM & Art
这两个虚拟机的垃圾回收见另一篇详细文章:
强引用、弱引用、软引用、虚引用
强引用是最常见的引用类型,通过new创建的对象默认都是强引用。只要强引用存在,垃圾回收器永远不会回收被引用的对象。可能导致内存泄漏(当对象不再需要但仍有引用指向它时),使用场景为普通对象创建,需要长期持有的对象。
弱引用通过WeakReference类创建,当垃圾回收器执行时,无论内存是否足够,都会回收被弱引用引用的对象。使用场景为缓存对象,当对象不再需要时,可以被垃圾回收器回收。一般为临时缓存,主要防止内存泄漏。
软引用通过SoftReference类创建,当内存不足时,垃圾回收器会回收软引用引用的对象。使用场景为内存敏感的对象,当内存不足时,可以被垃圾回收器回收。例如缓存对象,图片缓存等。
虚引用通过PhantomReference类创建,虚引用不会影响对象的生命周期,主要用于跟踪对象被垃圾回收器回收的状态。使用场景为对象被垃圾回收器回收时的回调。
内存泄漏常见场景
见性能优化篇: 【Android性能优化】内存
线程间通信
如何实现多线程安全?synchronized 和 ReentrantLock 的区别?
线程池
见另一篇文章: 【通用开发】Java线程池
安卓设备开机流程
作为应用开发,除了SystemUI和Launcher外,我们更多关注的是应用层的启动流程。对于系统启动稍作了解即可。
冷启动流程
见另一篇文章: 【Android进阶】APP冷启动流程解析
Handler & 消息处理机制
【Android进阶】Handler消息机制的上下层设计与流程详解
Activity & Window 初始化
在 Android 中,Activity 和 Window 通过一系列紧密的协作关系绑定在一起,共同构成用户界面的基础架构。以下是它们的绑定机制详细分析:
基本关系框架
Activity
└── PhoneWindow (Window的唯一实现)
└── DecorView (顶级View)
└── 内容区域(包含开发者设置的布局)
绑定过程的关键步骤
Activity创建时初始化Window,在Activity的attach()方法中完成初始绑定:
// Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
...) {
// 创建PhoneWindow实例
mWindow = new PhoneWindow(this, window);
// 设置Window回调
mWindow.setCallback(this);
// 设置Window管理器
mWindow.setWindowManager(...);
}
Window的创建时机
在Activity的attach()方法中创建,实际类型是PhoneWindow(Window的唯一实现类),与Activity生命周期绑定,一个Activity对应一个Window。
关键绑定点说明
| 绑定点 | 说明 |
|---|---|
| mWindow.setCallback(this) | 将Activity设置为Window的回调接口,用于接收Window的各种事件通知 |
| mWindow.setWindowManager() | 建立与WindowManager的连接,用于管理Window的显示位置和状态 |
| setContentView() | 通过Window将视图层级与Activity关联,开发者设置的布局最终会添加到DecorView中 |
Activity → Window 的通信
主要通过直接调用Window的方法:
// Activity中调用Window方法的示例
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
//通过Window.Callback接口回调:
// Window.Callback接口主要方法
public interface Callback {
boolean dispatchKeyEvent(KeyEvent event);
boolean dispatchTouchEvent(MotionEvent event);
void onContentChanged();
void onWindowFocusChanged(boolean hasFocus);
// ...
}
Activity实现了这个接口:
// Activity.java
public class Activity extends ContextThemeWrapper
implements Window.Callback, ... {
// 实现回调方法
public boolean dispatchTouchEvent(MotionEvent ev) {
// 处理触摸事件
}
}
视图层级绑定
通过setContentView()建立视图绑定关系:
Activity.setContentView()
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
}
PhoneWindow.setContentView()
Activity与Window的生命周期关键交互点
- onCreate() Window已创建但视图未显示 通常在这里调用setContentView()
- onStart()/onResume() Window开始变得可见 ViewRootImpl建立连接
- onAttachedToWindow() View被附加到Window时回调 可以获取真实的宽高参数
- onWindowFocusChanged() Window获得/失去焦点时回调 标志真正的用户交互开始/结束
设计原理分析
这种绑定机制实现了以下设计目标,职责分离:
- Activity负责业务逻辑和生命周期
- Window负责视图管理和系统交互
布局膨胀(Layout Inflation)流程分析
布局膨胀是将XML布局文件转换为实际的View对象层次结构的过程。
基本流程概述
- 布局文件解析:将XML文件转换为可处理的节点结构
- View对象创建:根据XML标签创建对应的View实例
- 属性应用:将XML属性设置到View对象上
- 层次构建:递归处理子View,构建完整的View树
核心类与组件
- LayoutInflater:执行膨胀过程的核心类
- XmlPullParser:用于解析XML布局文件
- AttributeSet:表示XML属性集合的接口
初始化LayoutInflater
LayoutInflater inflater = LayoutInflater.from(context);
// 或
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflate()方法调用
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
实际膨胀过程
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// 1. 解析XML
final AttributeSet attrs = Xml.asAttributeSet(parser);
// 2. 临时存储结果View
View result = root;
try {
// 3. 查找根节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// 跳过非开始标签
}
// 4. 获取根元素名称
final String name = parser.getName();
// 5. 处理特殊标签
if (TAG_MERGE.equals(name)) {
// 处理<merge>标签
rInflate(parser, root, attrs, false);
} else {
// 6. 创建根View
final View temp = createViewFromTag(root, name, attrs);
// 7. 递归创建子View
rInflateChildren(parser, temp, attrs);
// 8. 决定是否附加到root
if (root != null && attachToRoot) {
root.addView(temp);
}
result = temp;
}
} catch (Exception e) {
// 异常处理
}
return result;
}
}
View创建过程(createViewFromTag)
View createViewFromTag(View parent, String name, AttributeSet attrs) {
// 1. 处理<blink>等特殊标签(已废弃)
if (name.equals("blink")) {
// ...
}
// 2. 尝试使用Factory创建View
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 3. 没有Factory则使用系统默认方式创建
if (view == null) {
try {
// 4. 处理带点号的全限定名
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} catch (Exception e) {
// 异常处理
}
}
return view;
}
递归膨胀子View(rInflate)
void rInflate(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) {
// 1. 获取布局深度
final int depth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
// 2. 获取当前标签名
final String name = parser.getName();
// 3. 处理特殊标签
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// 处理<include>标签
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge> must be the root element");
} else {
// 4. 创建普通View
final View view = createViewFromTag(parent, name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
// 5. 递归处理子View
rInflateChildren(parser, view, attrs);
// 6. 添加到父View
viewGroup.addView(view);
}
}
}
性能优化相关
- 使用
AsyncLayoutInflater进行异步加载(API 24+) - 预加载常用布局并缓存
- 减少布局层级,避免不必要的嵌套
- 使用
merge标签减少层级
自定义View问题:
- 确保实现了所有必要的构造函数
- 检查自定义属性是否正确定义和引用
- 理解布局膨胀流程有助于优化布局性能,解决布局相关问题,以及实现高级自定义功能
setContentView流程
setContentView 是 Android 开发中用于设置 Activity 界面布局的核心方法。以下是它的详细工作流程:
基本调用流程
- Activity.setContentView(),这是开发者最常调用的入口方法,有多个重载版本:传入布局资源ID、View对象等
- 委托给 Window 对象,Activity 内部通过 getWindow().setContentView() 委托处理。Window 是抽象类,实际实现是 PhoneWindow
- PhoneWindow.setContentView(),这是真正的实现核心
PhoneWindow.setContentView() 主要步骤
public void setContentView(int layoutResID) {
// 1. 检查是否有DecorView,没有则创建
if (mContentParent == null) {
installDecor();
} else {
// 如果已有内容视图,则移除
mContentParent.removeAllViews();
}
// 2. 将布局inflate到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
// 3. 通知Activity内容已改变
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
installDecor() 过程
这是创建窗口装饰的关键方法:
private void installDecor() {
if (mDecor == null) {
// 1. 创建DecorView
mDecor = generateDecor();
// 配置DecorView属性
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
}
if (mContentParent == null) {
// 2. 生成mContentParent(实际是ContentView的父容器)
mContentParent = generateLayout(mDecor);
// 3. 设置其他窗口装饰元素
// 如标题栏、ActionBar等
}
}
generateLayout() 过程
这个方法根据窗口特性选择不同的窗口装饰布局:
- 根据主题风格选择基础布局(如 R.layout.screen_simple)
- 将选定的布局inflate到DecorView中
- 找到内容视图的容器(ID为android.R.id.content的FrameLayout)
- 返回这个内容容器作为mContentParent
重要注意事项
- 多次调用setContentView:后续调用会替换之前的内容视图,但DecorView不会重建
- 主题影响:窗口装饰布局的选择受Activity主题影响
- 性能考虑:inflate布局是相对耗时的操作,应优化布局文件
- 时机问题:必须在Activity.onCreate()之后调用,某些窗口特性需要在setContentView之前设置
- 异步inflate:Android 8.0+支持异步inflate(使用AsyncLayoutInflater)
View绘制三部曲
创建Activity时,实际调用了ActivityThread的performLaunchActivity,这时候DecorView会被创建。
在handleResumeActivity时,DecorView会被Activity里的windowManager添加到PhoneWindow窗口中,实际是了ViewRootImpl的setView方法将DecorView传进去。
再之后会走到 ViewRootImpl 的 performTraversals 方法,真正开始ViewTree的工作流程。这个方法非常长,非常重要,这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw。
Measure
在Measure测量的时候,会用到一个MeasureSpec类,这个类内部的一个32位的int值,其中高2位代表了SpecMode,低30位则代表SpecSize。SpecMode指的是测量模式,SpecSize指的是测量大小 通过位运算来给这个常量的高2位赋值,有三个情况:
- 00—UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
- 11—- AT_MOST:最大模式,对应于wrap_comtent属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
- 01—–EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值。
每一个普通View都有一个 MeasureSpec 属性来对其进行测量。而对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定。
performMeasure这个方法里,会对一众的子ViewGroup和子View进行测量。
View的onMeasure方法:实际是看 getDefaultSize() 来解析其宽高的,注意对于View基类来说,为了扩展性,它的两个MeasureSpec,AT_MOST和EXACTLY处理是一样的,即其宽高直接取决于所设置的specSize,所以自定义View直接继承于View的情况下,要想实现wrap_content属性,就需要重写onMeasure方法,自己设置一个默认宽高值。
ViewGroup的Measure方法:它没有onMeasure,有一个 measureChildren() 方法:简单来说就是 根据自身的MeasureSpec 和 子元素的的LayoutParams属性 来得出的子元素的MeasureSpec 属性。有一点注意的是如果父容器的 MeasureSpec 属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT,最后计算出的子元素MeasureSpec为AT_MOST,相当于设置matchparent。
每一种ViewGroup的计算方式都不尽相同,像LinearLayout的就是单纯的在其方向上所有子元素的宽/高都加在一起。
Layout
ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。
所以一般都是ViewGroup来计算子View的参数,并调用子控件的layout方法。
View的layout方法,其中分别传入 left,top,right,bottom 四个参数,表示其距离父布局的四个距离,再走到setFrame,最后到onLayout,这是一个空方法,由继承的类自己实现。
像LinearLayout,其各个子控件会按照顺序排布,childTop值越来越大,子View就会按照顺序排布,而不是叠到一起。
Draw
官方注释清楚地说明了每一步的做法,它们分别是:
(1)如果需要,则绘制背景。
(2)保存当前canvas层。
(3)绘制View的内容。
(4)绘制子View。
(5)如果需要,则绘制View的褪色边缘,这类似于阴影效果。
(6)绘制装饰,比如滚动条。
绘制背景drawBackGround的时候,如果有偏移值,就会在偏移之后的Canvas上绘制。
第三步onDraw和第四步dispatchDraw都是空实现,由子View自定。
像ViewGroup就重写了dispatchDraw方法,遍历子View去绘制,需要注意的是会检索是否有缓存,如果有会直接拿缓存来显示。
热门八股问题
onresume获取不到View的宽高,而View.post就可以拿到:
- onCreate和onResume中无法获取View的宽高,是因为还没执行View的绘制流程。
- view.post之所以能够拿到宽高,是因为在绘制之前,会将获取宽高的任务放到Handler的消息队列,等到View的绘制结束之后,便会执行。
三种动画
补间动画
核心特性
- 操作对象:作用于整个 View
- 动画类型:平移(Translate)、缩放(Scale)、旋转(Rotate)、透明度(Alpha)
- 资源定义:可通过 XML 或代码定义
- 视觉限制:只改变绘制位置,不改变实际属性
Animation anim = AnimationUtils.loadAnimation(this, R.anim.slide_in);
view.startAnimation(anim);
优缺点分析
- 简单易用,容易实现
- 动画效果简单
- 资源占用低
- 无法实现复杂的动画
- 无法精确控制动画效果
- 只改变绘制位置,不改变实际属性
属性动画
核心特性
- 操作对象:可作用于任何对象的任意属性
- 核心类:ValueAnimator、ObjectAnimator、AnimatorSet
- 高级功能:插值器、估值器、动画组合
- 真实改变:实际修改目标属性值
// 透明度动画
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
alphaAnim.setDuration(1000);
alphaAnim.start();
// 组合动画
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f),
ObjectAnimator.ofFloat(view, "rotation", 0f, 360f)
);
set.setDuration(500).start();
优缺点分析
- 功能强大,可操作任何属性
- 动画效果更真实
- 支持复杂的动画组合
- 实现相对复杂
- 资源消耗较高
帧动画
帧动画(Frame Animation)是Android中最基础的动画类型之一,它通过快速切换一系列静态图片来产生动画效果,类似于传统电影或GIF动画的工作原理。
核心特性
- 逐帧播放:按顺序显示一系列图片
- 资源形式:通常使用多张PNG/JPG图片
- 实现方式:通过AnimationDrawable类实现
- 控制方式:可控制播放速度、循环次数等
性能优化建议
图片优化:
- 使用WebP格式替代PNG可减小体积
- 确保图片尺寸不过大
- 使用适当的压缩工具处理图片
- 避免重复创建AnimationDrawable实例
- 考虑使用单例模式管理常用动画
- 根据设备性能调整帧率
- 在Activity/Fragment不可见时停止动画
@Override
protected void onPause() {
super.onPause();
if (animation != null && animation.isRunning()) {
animation.stop();
}
}
事件分发
Android 的点击事件分发机制是一个典型的 责任链模式 ,事件从最外层的 ViewGroup 开始,沿着视图层级依次传递,直到被某个 View 消费为止。
事件分发三大核心方法
dispatchTouchEvent(MotionEvent event)- 事件分发入口onInterceptTouchEvent(MotionEvent event)- 事件拦截(仅ViewGroup)onTouchEvent(MotionEvent event)- 事件处理
完整分发流程
Activity 层级分发
// Activity.dispatchTouchEvent() public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); // 用户交互回调 } if (getWindow().superDispatchTouchEvent(ev)) { return true; // 被Window处理 } return onTouchEvent(ev); // 最后由Activity处理 }- ViewGroup 层级分发,ViewGroup 的分发流程最为复杂:
// ViewGroup.dispatchTouchEvent() 简化流程 public boolean dispatchTouchEvent(MotionEvent ev) { // 1. 检查拦截 if (onInterceptTouchEvent(ev)) { return super.dispatchTouchEvent(ev); // 转为View的处理流程 } // 2. 遍历子View寻找能处理事件的View for (int i = childrenCount - 1; i >= 0; i--) { View child = getChildAt(i); if (child.dispatchTouchEvent(ev)) { mFirstTouchTarget = child; // 记录触摸目标 return true; // 事件已消费 } } // 3. 没有子View处理则自行处理 return super.dispatchTouchEvent(ev); } - View 层级处理
// View.dispatchTouchEvent() public boolean dispatchTouchEvent(MotionEvent event) { // 1. 先检查OnTouchListener if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) { return true; } // 2. 再调用onTouchEvent return onTouchEvent(event); }
事件序列处理机制 一个完整的触摸事件通常包含:
- ACTION_DOWN - 手指按下(必须处理)
- ACTION_MOVE - 手指移动(可能多次)
- ACTION_UP - 手指抬起
- ACTION_CANCEL - 事件被取消
关键规则:
- 如果 View 不消费 ACTION_DOWN,后续事件不会传递给它
- 一旦某个 View 开始消费事件,整个事件序列都会交给它
- 父View可以通过 onInterceptTouchEvent 中途拦截事件
事件分发UML序列图:
[Activity] -> [Window] -> [DecorView] -> [RootViewGroup]
-> [ChildViewGroup] -> [TargetView]
- 自上而下传递询问是否拦截
- 自下而上传递询问是否处理
- 确定目标后直接传递给目标View
详细图解:

常见场景分析
场景1:点击按钮
- Activity 收到事件,传递给 Window
- DecorView 的 ViewGroup 开始分发
- 遍历子View找到按钮View
- 按钮的 onTouchEvent 返回 true 消费事件
场景2:滑动冲突
// 解决滑动冲突示例:外部拦截法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false; // 必须不拦截DOWN
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
场景3:自定义事件处理
// 自定义View处理双击事件
private GestureDetector mGestureDetector;
public MyView(Context context) {
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
// 处理双击
return true;
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
性能优化建议
- 减少视图层级 - 层级越深,分发路径越长
- 避免过度拦截 - 只在必要时使用 onInterceptTouchEvent
- 使用 TouchDelegate - 扩大小View的点击区域
- 合理使用 requestDisallowInterceptTouchEvent - 子View阻止父View拦截
多点触控处理
Copy
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
// 非第一个手指按下
break;
case MotionEvent.ACTION_POINTER_UP:
// 非最后一个手指抬起
break;
}
return true;
}
两列表嵌套滑动
横向和纵向列表,滑动冲突解决。
例如横向列表内部嵌套了一个纵向列表,对外部ReccyclerView的onInterceptTouchEvent方法进行拦截,然后判断内部RecyclerView是否需要拦截,需要则拦截。
public class HorizontalRecyclerView extends RecyclerView {
private float startX, startY;
public HorizontalRecyclerView(Context context) {
super(context);
}
public HorizontalRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = e.getX();
startY = e.getY();
// 必须不拦截DOWN,否则子View无法收到后续事件
return false;
case MotionEvent.ACTION_MOVE:
float endX = e.getX();
float endY = e.getY();
float distanceX = Math.abs(endX - startX);
float distanceY = Math.abs(endY - startY);
// 横向滑动距离大于纵向,且角度小于30度时拦截事件
if (distanceX > distanceY && distanceX > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
return true; // 拦截事件,父RecyclerView处理
}
break;
}
return super.onInterceptTouchEvent(e);
}
}
AIDL & Binder基础原理
Android中的Binder和AIDL是实现跨进程通信(IPC)的关键机制。
Binder通信原理
架构组成
- 用户空间:应用程序和服务运行的地方,通过Binder提供的接口进行通信。
- 内核空间:包含Binder驱动,负责处理进程间的数据传递和同步。
- Binder驱动:管理Binder操作的核心组件,处理通信请求,管理内存映射,确保数据正确传递。
工作流程
- 服务注册:服务端创建Binder对象并注册到ServiceManager,表明可接受请求。
- 客户端查找服务:客户端通过ServiceManager查询服务,获取服务端Binder代理对象。
- 建立通信通道:Binder在客户端和服务端之间建立通信通道,通过共享内存区域直接访问数据。
- 数据传输:客户端调用服务端接口方法,Binder将方法调用和参数打包成数据,通过共享内存传递给服务端,服务端处理后返回结果。
通信细节
- 基于内存映射:Binder通信基于内存映射(mmap)实现,通过映射内存区域,数据传输只需一次复制,提高性能。
- 线程池机制:服务端通过Binder线程池处理IPC请求,线程池动态扩展、复用线程,提升并发能力。
AIDL通信原理
- 定义与作用
- AIDL(Android Interface Definition Language)是Android提供的一种用于定义跨进程通信接口的语言。
- 通过AIDL,可以定义客户端和服务端之间通信的接口和方法,实现跨进程调用。
工作流程
- 定义AIDL接口:创建AIDL文件,定义接口和方法。
- 实现AIDL接口:服务端实现AIDL接口,创建Binder对象,并在Service中返回该Binder对象。
- 客户端绑定服务:客户端通过bindService绑定服务,获取服务端返回的Binder代理对象。
- 调用接口方法:客户端通过Binder代理对象调用服务端的方法,Binder机制负责底层通信。
关键组件
- Stub类:服务端实现的Binder类,继承自AIDL生成的Stub类。
- Proxy类:客户端的Binder代理类,用于代理服务端的方法调用。
- Parcel:用于封装和传输数据,支持多种数据类型,高效且轻量。
总结
- Binder是Android跨进程通信的核心机制,通过内核驱动和内存映射实现高效通信。
- AIDL是基于Binder实现的通信接口定义语言,简化了跨进程通信的开发流程,使客户端和服务端能够以面向对象的方式进行通信。