谈谈 SparseArray

用法

使用 Key-Value 保存对象的集合,类似 HashMap,但只允许使用 int 型的 Key

特点

  • (优点)内存效率高,避免自动装箱,避免了使用 Entry 来构建数据结构,int 4 bytes,Integer 16 bytes
  • (缺点)使用二分法查找对象,在数据体量大(more than hundreds of items)的场景下查找/插入/删除效率不如 HashMap
  • 删除过程优化:先标记 item,待 GC 时真正删除

操作

  • 随机插入:put(int, x),会替换掉已有的对象
  • 顺序插入:append(int, x),当 Key 比目前所有的都要大时执行效率更高,否则同 put
  • 随机删除:delete(int),删除 Key 对应的对象
  • 顺序删除:removeAt(int),删除第 N 项
  • 范围删除:removeAtRange(int, int)
  • 随机访问:get(int),根据 Key 获取 Value,如果没有则返回 null
  • 顺序访问:valueAt(int)
  • 遍历:keyAt(int),获取第 n 位的 key;valueAt(int),获取第 n 位 key 对应的 value;indexOfKey(int),获取 key 的 index;indexOfValue(int),获取 value 的 index

源码阅读

成员变量部分

1
2
3
4
5
6
7
8
9
10
private static final Object DELETED = new Object(); // 删除对象时,并非真正删除,而是将 Value 替换成 DELETE 对象
private boolean mGarbage = false; // 标示是否需要进行 gc

private int[] mKeys;
private Object[] mValues;
private int mSize;

public SparseArray() {
this(10); // 默认长度是10
}

在调用 append, put, size 等方法时会触发 GC,是一个逐项复制的过程,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void gc() {
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;

for (int i = 0; i < n; i++) {
Object val = values[i];

if (val != DELETED) { // 判断是否已经标记
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}

o++;
}
}

mGarbage = false;
mSize = o;
}

put 过程,核心是先判断有没有同一个 key 存在,有则替换,没有的话,再判断是否目标位置刚好被 DELETED 标记,最后才进行加项操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找 key 的位置

if (i >= 0) { // 如果已经有同一个 key
mValues[i] = value;
} else {
i = ~i; // 求异或

if (i < mSize && mValues[i] == DELETED) { // 目标位置项目已经标记 DELETE,可以直接替换
mKeys[i] = key;
mValues[i] = value;
return;
}

if (mGarbage && mSize >= mKeys.length) { // 需要寻找新的位置,此时先 gc
gc();

// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}

mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 运用 System.arraycopy 进行添加(有必要则扩容)
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}

delete 过程,先二分查找到目标位置,标记 DELETED,不直接删除

1
2
3
4
5
6
7
8
9
10
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}

Handler 内部原理,HandlerThread 与普通 Thread 区别

Handler 的两个用途

  1. 管理消息/任务队列,可以控制立即执行或者延迟执行
  2. 调度任务在不同线程运行f

主要概念

  • Handler:消息分发器,一个 Handler 里面会关联一个 MessageQueue,这个 MessageQueue 来自当前线程的 Looper
  • Looper:消息循环器,从 MQ 里面不断取出消息运行。非 UI 线程是没有默认创建 Looper 的,需要人工调用 prepare 和 loop 来启动 Looper。被声明为 ThreadLocal,每一个线程独立拥有
  • Message:消息单元,内部用消息池管理,默认 capacity = 50
  • MessageQueue:消息队列,单链表,每个消息入队时会带上一个运行时间 when,根据这个 when 将其放入队列中相应的位置(早执行的放在队首),整个队列是按照运行时间排序的

原理流程概述

创建 Handler 时会关联当前线程 Looper 中的 MQ,当用 Handler 对象发送消息时,消息会进入 MQ,经 Looper 轮询取出后进行处理,处理时会调用所实现的 handleMessage 方法。

UI 线程创建 Looper

  1. ActivityThread 在 main 方法里通过 Looper.prepareMainLooper() 创建主 Looper,并将其存入 ThreadLocal 变量中。同时将它另存一份作为主线程 Looper,供其他线程访问。
  2. 在 main 方法最后通过 looper.loop() 启动轮询
1
2
3
4
5
6
public static void main(String[] args) {
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
Looper.loop();
}

Handler 发送消息的流程

构造过程中取出当前线程持有的 Looper,并保存其 MQ 对象

1
2
3
4
5
6
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

handler通过sendMessage(msg) 将消息发出,消息最终走向 queue.enqueueMessage(msg, uptimeMillis) 进入队列,同时将当前 Handler 以 target 保存在消息对象中,当 Looper 轮询时,会取出 target 用于处理消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
return queue.enqueueMessage(msg, uptimeMillis);
}

public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // 取出消息,无消息则阻塞
if (msg == null) { return; }
msg.target.dispatchMessage(msg);//发送消息 其中target就是Handler
}
}

dispatchMessage 最终会调到 Handler 中实现的 handleMessage

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

HandlerThread

构造函数里创建了自己的 Looper 并且开启轮询,普通 Thread 不含 Looper

Picasso VS Glide

项目 Picasso Glide
图片格式 ARGB8888 RGB_565
缓存 原图尺寸 显示尺寸
GIF 不支持 支持
video 不支持 支持
Library size 100K 500K
Methods count 500 2500

View 的绘制过程

一个 Activity 的窗口页面,可以分为PhoneWindow、DecorView、TitleBar & ContentView 几个层级

window.png
window.png

整个 View 树的绘图流程在 ViewRootImpl.performTraversals() 方法中,它主要做的事情是根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否需要重新放置视图位置(layout)以及是否需要重新绘制(draw),代码如下

  • measure:测量,指测量 View 的宽高
  • layout:布局,指确定在父容器中的位置坐标
  • draw:绘制并显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
perform_traversals
perform_traversals

measure 过程分析

整个 View 树从根 View 开始,递归进行 measure

measure.png
measure.png

View.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
//final方法,子类不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}

//View的onMeasure默认实现方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

MeasureSpec 的语义是父 View 对子 View 在长宽上的约束,有三种取值

  • EXACTLY:不管子 View 想多大,它只能这么大
  • AT_MOST:最大只能这么大
  • UNSPECIFIED:不加约束,子 View 想多大就多大

measure 过程主要就是从顶层父 View 向子 View 递归调用 view.measure 方法(measure中 又回调 onMeasure 方法)的过程。具体 measure 核心主要有如下几点

  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
  • View的布局大小由父View和子View共同决定。
  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

layout 过程分析

View.performTraversals 在 measure 之后,会执行 layout 过程

1
2
3
4
5
private void performTraversals() {
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
}

layout() 方法有四个参数,分别是 left、top、right、bottom,表示当前 View 相对 Parent 的四个坐标。layout 过程也是一个递归的过程:

layout.png
layout.png

View.layout 方法实际上会调用到 onLayout 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void layout(int l, int t, int r, int b) {
......
//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//需要重新layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//回调onLayout
onLayout(changed, l, t, r, b);
......
}
......
}

layout 也是从顶层父 View 向子 View 的递归调用 layout 方法的过程,即父 View 根据上一步 measure 子 View 所得到的布局大小和布局参数,将子 View 放在合适的位置上。具体 layout 核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑
  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的
  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的
  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值

draw 过程分析

performTraverls 在 layout 后会进行 draw 的操作

1
2
3
4
5
6
7
8
9
private void performTraversals() {
......
final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
}

draw 也是一个递归的过程

draw.png
draw.png
  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
  • View的绘制是借助onDraw方法传入的Canvas类来进行的。
  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。
  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

参考:Android应用层View绘制流程与源码分析

类加载过程/ClassLoader机制

Apk 打包过程

ListView 和 RecyclerView 原理

性能优化

考虑性能时一般从以下几个角度

  • 内存优化
  • UI优化(布局、绘制)
  • 速度优化(线程、网络)
  • 电量优化
  • 启动优化

内存优化

内存优化是为了解决内存溢出(OOM)的问题,内存溢出通常有两个原因

  • 使用不当造成的内存泄漏,内存无法释放
  • 虽然不属于泄漏,但一些代码逻辑不当,导致消耗大量内存,难以及时释放

内存泄漏

常见原因与处理方法

  1. 单例模式里使用了 Activity 的 Context,导致其生命周期延长至整个应用使用周期,应该使用 ApplicationContext
  2. 非静态内部类持有 Activity 对象引用,应该改成静态内部类,同时使用 ApplicationContext;以上两步说明尽量不要使用 Activity 的 Context,而是应当用 ApplicationContext
  3. Handler 持有 Activity 对象引用,同样应该改成静态,并使用弱引用,并在 Activity 的 onDestroy 方法里清空 Handler 消息,mHandler.removeCallbacksAndMessages(null);
  4. 线程进行耗时操作,导致 Activity 没释放,应当使用弱引用,并且在 Activity 的 onDestroy 方法里 cancel 掉线程
  5. 占有系统资源后没有关闭,在使用完 BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap,Animation 等资源时,一定要在 Activity 中的 onDestry 中及时的关闭、注销或者释放内存。

参考:Android中五种常见内存泄漏原因

常用工具

  • Heap SnapShot
  • Heap Viewer
  • LeakCanary
  • MAT
  • TraceView(Device Monitor)

消耗大量内存:通常加载 Bitmap 时会发生这种情况,解决思路是

  • 加载单张图片时候进行压缩,或者使用缩略图
  • 控制每次加载的数量
  • 加载多张图片时,滑动过程中不进行加载,仅在滑动完成后加载

UI优化(布局、绘制)

Android 屏幕刷新的频率是 60fps,意味着每一帧的绘制必须要在 16ms 内完成,如果 UI 绘制超过了 16ms,在体验上就会出现卡顿。

以下罗列造成 App 卡顿的一些原因

  1. 在 UI 线程里进行轻微耗时操作
  2. 布局 Layout 过于复杂,无法在 16ms 内完成渲染
  3. 动画执行次数过多,导致 CPU 或 GPU 负载过高
  4. View 过度绘制,屏幕某些像素在一帧时间内绘制多次,CPU 或 GPU 负载过高
  5. View 频繁触发 measure 和 layout,累计耗时过多,频繁渲染造成负载过高
  6. 内存频繁 GC,导致阻塞渲染操作
  7. 冗余资源和逻辑导致运行缓慢
  8. ANR

UI优化,通常是指布局优化和 View 绘制优化

布局优化

在“开发者选项”里可以打开“过度绘制开关”,从而查看当前页面 View 是否存在过绘,红色表示层级最多(4+),浅紫色表示层级最低(1),如下所示。找到那些过绘的部分,在布局文件里减少它们的背景,比如把底层 View 的背景设置成透明。同时,尽量减少 ViewGroup 嵌套的情况,通常 LinearLayout 会比 RelativeLayout 的层级要少。还有一点就是要使用设备对应分辨率的资源文件,并不是图片越清晰就越好。

Overdraw
Overdraw

在“开发者模式”里面还有另一项相关的设置,叫做“GPU呈现模式分析”,可以将渲染时间以条形图📊的方式显示在屏幕底部,同时 16ms 的基准线也会以绿色绘制在屏幕中央。条形图中同一个条形的不同颜色表示绘制不同阶段

呈现模式分析
呈现模式分析

绘制优化

主要是针对自定义 View 里面的代码编写,在 onDraw 这一步要尽量减少开销,因为 onDraw 方法是实时执行的,在一帧内会执行多次。因此,在 onDraw 中要避免出现以下两种情况

  • 创建局部对象,这会导致占用大量内存,频繁 GC
  • 执行耗时操作,出现循环,这会占用 CPU 时间

过度绘制优化,使用对应分辨率的资源文件

速度优化(线程、网络)

线程优化

需要在子线程进行耗时操作,避免阻塞主线程

  • HandlerThread:拥有自己 Looper 的线程类,可以在这个线程里进行耗时操作,然后通知主线程
  • AsyncTask:见下面一节“AsyncTask 知识点”
  • IntentService:运行在独立线程的 Service,原理是创建一个 HandlerThread,然后在 onStart 时把消息丢给 Handler 处理
  • ThreadPool:用 Executor、ThreadPoolExecutor 来管理线程

网络优化

网络优化主要是从时间、速度、成功率几个角度来进行,对于提高速度,比较典型的是请求图片的场景

  • 使用WebP格式,能比 JPG 节约25%~35%的流量,比 PNG 节约80%流量
  • 使用缩略图

另外一些网络优化的知识点

  • 对网络请求进行缓存,若请求数据仍在有效期内则直接使用缓存,不走网络
  • 减少 GPS 定位使用,条件允许则多用网络定位
  • 下载过程中使用断点续传
  • 刷新数据时采用局部刷新,少用全局刷新

电量优化

  • 需要进行网络请求时,先判断网络当前状态
  • 批量处理网络请求
  • 在同时有wifi和移动数据的情况下,我们应该直接屏蔽移动数据的网络请求
  • 减少后台任务

启动优化

根据应用启动时候的状态,可以分为冷启动、热启动、暖启动三种,其中冷启动的耗时最长,对用户体验影响最大,因此谈启动优化主要是从冷启动的角度给出优化建议。

冷启动初始时,系统完成三个任务

  1. 启动和加载应用
  2. 创建应用进程
  3. 显示启动视图(白屏)

当应用进程创建完毕后,开始创建应用

  1. 创建应用对象
  2. 启动主线程 (MainThread)
  3. 创建 Main Activity
  4. 加载视图 (Inflating views)
  5. 渲染布局 (Laying out)
  6. 执行初始绘制

冷启动优化

  • 减少首页使用的资源,懒加载
  • 优化首页布局,减少层级,不绘制不可见的 UI,而是使用 ViewStub 对象在适当的时间布局绘制
  • 在闪屏页(Fragment)预先加载

AsyncTask 知识点

是什么

是为了解决 UI 线程无法进行耗时操作的问题而应用的一种 API,通常处理短时耗时任务(长时任务建议使用 Executor、ThreadPoolExecutor、FutureTask),有四个方法需要覆盖

  • onPreExecute(),在 UI 线程调用,进行任务运行前的准备工作
  • doInBackground(Params…),在后台线程运行,读取 execute 传来的参数,运行过程中可以通过 publishProgress(Progress…) 来发布进度信息,进度信息会以 onProgressUpdate(Progress…) 回调的方式通知 UI 线程
  • onProgressUpdate(Progress…),在 UI 线程调用,接收进度信息
  • onPostExecute(Result),在 UI 线程调用,接受任务运行结果

变更历史

  • 最初问世,采用单线程模型,所有任务串行执行
  • DONUT(1.6),采用线程池模型,并发运行
  • HONEYCOMB(3.0),又改回单线程模型,为了简化使用,避免并发带来的同步问题

内部原理(API 26)

  1. AsyncTask 无参构造函数里会创建一个任务对象 mWorker(在其中执行 doInBackground),一个任务完成回调对象 mFuture,同时获取到 UI 线程的 Looper,用来在任务完成后通知 UI 线程
  2. 维护单例 SERIAL_EXECUTOR,用来串行执行任务
  3. 调用 AsyncTask.execute 时,会进入 executeOnExecutor 方法,在其内部先调用 onPreExecute,然后通过 executor 运行 mWorker,由 SERIAL_EXECUTOR 保证了串行运行,真正运行任务的是 THREAD_POOL_EXECUTOR
  4. 在任务完成后,通过 UI 线程的 Looper 通知 UI 线程

一个关于类的静态变量归属的问题

父类有一个 protected 的静态变量 foo,基于父类 A 创建两个子类 B1 和 B2

  • 如果 B1 和 B2 不声明变量 foo,那么它们使用的 foo 是同一个,来自父类 A
  • 如果 B1 和 B2 都声明变量 foo,那么它们使用各自的变量

简单概括就是说静态变量与类绑定,demo 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// case 1
abstract class A {
protected static int foo = 0;
}

class B1 extends A {
}

class B2 extends B {
}

public static void main(String[] args) {
A b1 = new B1();
A b2 = new B2();
System.out.println("b1.foo = " + b1.foo + "& b2.foo = " + b2.foo); // b1.foo = 0 & b2.foo = 0
}


// case 2
abstract class A {
protected static int foo = 0;
}

class B1 extends A {
static int foo = 100;
}

class B2 extends B {
static int foo = 200;
}

public static void main(String[] args) {
A b1 = new B1();
A b2 = new B2();
System.out.println("b1.foo = " + b1.foo + "& b2.foo = " + b2.foo); // b1.foo = 100 & b2.foo = 200
}