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

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

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

JVM内存模型

jvm_ram

  • 程序计数器:一小块区域, 线程私有 。记录了每个线程的代码执行到了哪一行,各种循环,判断都是通过这个区域存的数值来走的。Java多线程是时间分片,各个线程在一段时间内占用这个核来执行任务,这个线程切换到另一个线程,其恢复的依据也是计数器的值。
  • 虚拟机栈:周期与线程相同,也是 线程私有 。每个方法执行时,都会创建一个栈帧, 栈帧里面存储方法内的局部变量表,方法出口等等信息 。每个方法执行到退出的过程,就是一个个的方法栈帧入栈出栈的过程。这个区域有两个异常,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果JVM允许动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
  • 本地方法栈:和虚拟机栈作用一样,但是服务于 本地的Native方法 。同样会抛出上面的两种异常。
  • Java堆:最大的一块,所有 线程共享 的数据。几乎所有的对象实例都在这里保存。Java堆是垃圾收集器管理的内存区域。Java堆可以处于物理上不连续的内存空间中,可以选择固定大小或可扩展。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出 OutOfMemoryError 异常。
  • 方法区: 线程共享 。用于存储已被虚拟机加载的 对象类型信息、常量、静态变量、即时编译器编译后的代码缓存 等数据。对其要求比较宽松,几乎不用考虑垃圾回收,但是回收也是有必要的,主要针对常量的回收和类型卸载。如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
    • 运行时常量池,其是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行期间也可以将新的常量放入池中。

类加载流程

父子类加载的具体流程

  1. 加载阶段
    • 父类优先:当加载一个类时,JVM会先检查其父类是否已加载
    • 递归加载:如果父类未被加载,则会递归加载父类及其父类,直到Object类
    • 子类后加载:所有父类加载完成后,才开始加载子类
  2. 准备阶段
    • 父类优先:为父类的静态变量分配内存并设置默认值
    • 子类后处理:然后为子类的静态变量分配内存并设置默认值
  3. 初始化阶段
    • 父类优先:执行父类的静态代码块和静态变量赋值
    • 子类后处理:然后执行子类的静态代码块和静态变量赋值

实例化时的加载顺序

当创建子类实例时,加载顺序如下:

  1. 父类静态成员和静态块(只在第一次加载类时执行一次)
  2. 子类静态成员和静态块(只在第一次加载类时执行一次)
  3. 父类实例变量初始化
  4. 父类构造代码块
  5. 父类构造函数
  6. 子类实例变量初始化
  7. 子类构造代码块
  8. 子类构造函数

代码示例

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)

jvm_find

年轻代(Young Generation)

所有新生成的对象首先都是放在年轻代中。年轻代的目标就是尽可能快速地回收哪些生命周期短的对象。

新生代内存按照8:1:1的比例分为一个Eden区和两个survivor(survivor0,survivor1)区。

  • Eden区,字面意思翻译过来,就是伊甸区,人类生命开始的地方。当一个实例被创建了,首先会被存储在该区域内,大部分对象在Eden区中生成。
  • Survivor区,幸存者区,字面理解就是用于存储幸存下来对象。

回收时机:

  1. 一开始都在Eden区里,当Eden快满了就触发回收,之后,先将Eden区还存活的对象复制到一个Survivor0区,然后清空Eden区。
  2. 当这个Survivor0区也存放满了后,则将Eden和Survivor0区中存活对象都复制到另外一个survivor1区,然后清空Eden和这个Survivor0区,此时的Survivor0区就也是空的了。
  3. 然后将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),这样效率是不是就很快了。

年轻代往老年代转移的条件

  1. 有一个JVM参数 -XX:PretenureSizeThreshold ,默认值是0,表示任何情况都先把对象分配给Eden区。若设置为1048576字节,也就是1M。则表示当创建的对象大于1M时,就会直接把这个对象放入到老年区,就根本不会经过新生区了。这么做的原因: 大对象在经历复制算法进行GC的时候会降低性能
  2. 如果新生区中的某个对象 经历了15次GC 后,还是没有被回收掉,那么它就会被转入老年区。
  3. 如果当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

这两个虚拟机的垃圾回收见另一篇详细文章:

【Android进阶】JVM&DVM&ART虚拟机对比

强引用、弱引用、软引用、虚引用

强引用是最常见的引用类型,通过new创建的对象默认都是强引用。只要强引用存在,垃圾回收器永远不会回收被引用的对象。可能导致内存泄漏(当对象不再需要但仍有引用指向它时),使用场景为普通对象创建,需要长期持有的对象。

弱引用通过WeakReference类创建,当垃圾回收器执行时,无论内存是否足够都会回收被弱引用引用的对象。使用场景为缓存对象,当对象不再需要时,可以被垃圾回收器回收。一般为临时缓存,主要防止内存泄漏。

软引用通过SoftReference类创建,当内存不足时,垃圾回收器会回收软引用引用的对象。使用场景为内存敏感的对象,当内存不足时,可以被垃圾回收器回收。例如缓存对象,图片缓存等。

虚引用通过PhantomReference类创建,虚引用不会影响对象的生命周期,主要用于跟踪对象被垃圾回收器回收的状态。使用场景为对象被垃圾回收器回收时的回调。

内存泄漏常见场景

见性能优化篇: 【Android性能优化】内存

线程间通信

如何实现多线程安全?synchronized 和 ReentrantLock 的区别?

线程池

见另一篇文章: 【通用开发】Java线程池

安卓设备开机流程

作为应用开发,除了SystemUI和Launcher外,我们更多关注的是应用层的启动流程。对于系统启动稍作了解即可。

【Android进阶】Android设备开机流程

冷启动流程

见另一篇文章: 【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传进去。

再之后会走到 ViewRootImplperformTraversals 方法,真正开始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就可以拿到:

  1. onCreate和onResume中无法获取View的宽高,是因为还没执行View的绘制流程。
  2. 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) - 事件处理

完整分发流程

  1. 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处理
     }
    
  2. 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);
     }
    
  3. 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]
  1. 自上而下传递询问是否拦截
  2. 自下而上传递询问是否处理
  3. 确定目标后直接传递给目标View

详细图解:

常见场景分析

场景1:点击按钮

  1. Activity 收到事件,传递给 Window
  2. DecorView 的 ViewGroup 开始分发
  3. 遍历子View找到按钮View
  4. 按钮的 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实现的通信接口定义语言,简化了跨进程通信的开发流程,使客户端和服务端能够以面向对象的方式进行通信。

Service 的两种启动方式(startService 和 bindService)区别?

BroadcastReceiver 的动态注册和静态注册区别?

ContentProvider 的作用?如何实现跨进程数据共享?

插件化原理?如何实现热修复?

如果让你优化一个卡顿的页面,你会从哪些方面入手?

如何保证 App 的稳定性?(Crash 监控、异常捕获)