谈谈 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 | private static final Object DELETED = new Object(); // 删除对象时,并非真正删除,而是将 Value 替换成 DELETE 对象 |
在调用 append, put, size 等方法时会触发 GC,是一个逐项复制的过程,代码如下
1 | private void gc() { |
put 过程,核心是先判断有没有同一个 key 存在,有则替换,没有的话,再判断是否目标位置刚好被 DELETED 标记,最后才进行加项操作
1 | public void put(int key, E value) { |
delete 过程,先二分查找到目标位置,标记 DELETED,不直接删除
1 | public void delete(int key) { |
Handler 内部原理,HandlerThread 与普通 Thread 区别
Handler 的两个用途
- 管理消息/任务队列,可以控制立即执行或者延迟执行
- 调度任务在不同线程运行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
- ActivityThread 在 main 方法里通过 Looper.prepareMainLooper() 创建主 Looper,并将其存入 ThreadLocal 变量中。同时将它另存一份作为主线程 Looper,供其他线程访问。
- 在 main 方法最后通过 looper.loop() 启动轮询
1 | public static void main(String[] args) { |
Handler 发送消息的流程
构造过程中取出当前线程持有的 Looper,并保存其 MQ 对象
1 | public Handler(Callback callback, boolean async) { |
handler通过sendMessage(msg) 将消息发出,消息最终走向 queue.enqueueMessage(msg, uptimeMillis) 进入队列,同时将当前 Handler 以 target 保存在消息对象中,当 Looper 轮询时,会取出 target 用于处理消息
1 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { |
dispatchMessage 最终会调到 Handler 中实现的 handleMessage
1 | public void dispatchMessage(Message 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 几个层级
整个 View 树的绘图流程在 ViewRootImpl.performTraversals() 方法中,它主要做的事情是根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否需要重新放置视图位置(layout)以及是否需要重新绘制(draw),代码如下
- measure:测量,指测量 View 的宽高
- layout:布局,指确定在父容器中的位置坐标
- draw:绘制并显示
1 | private void performTraversals() { |
measure 过程分析
整个 View 树从根 View 开始,递归进行 measure
View.java
1 | /** |
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 | private void performTraversals() { |
layout() 方法有四个参数,分别是 left、top、right、bottom,表示当前 View 相对 Parent 的四个坐标。layout 过程也是一个递归的过程:
View.layout 方法实际上会调用到 onLayout 方法
1 | public void layout(int l, int t, int r, int 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 | private void performTraversals() { |
draw 也是一个递归的过程
- 如果该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)的问题,内存溢出通常有两个原因
- 使用不当造成的内存泄漏,内存无法释放
- 虽然不属于泄漏,但一些代码逻辑不当,导致消耗大量内存,难以及时释放
内存泄漏
常见原因与处理方法
- 单例模式里使用了 Activity 的 Context,导致其生命周期延长至整个应用使用周期,应该使用 ApplicationContext
- 非静态内部类持有 Activity 对象引用,应该改成静态内部类,同时使用 ApplicationContext;以上两步说明尽量不要使用 Activity 的 Context,而是应当用 ApplicationContext
- Handler 持有 Activity 对象引用,同样应该改成静态,并使用弱引用,并在 Activity 的 onDestroy 方法里清空 Handler 消息,
mHandler.removeCallbacksAndMessages(null);
- 线程进行耗时操作,导致 Activity 没释放,应当使用弱引用,并且在 Activity 的 onDestroy 方法里 cancel 掉线程
- 占有系统资源后没有关闭,在使用完 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 卡顿的一些原因
- 在 UI 线程里进行轻微耗时操作
- 布局 Layout 过于复杂,无法在 16ms 内完成渲染
- 动画执行次数过多,导致 CPU 或 GPU 负载过高
- View 过度绘制,屏幕某些像素在一帧时间内绘制多次,CPU 或 GPU 负载过高
- View 频繁触发 measure 和 layout,累计耗时过多,频繁渲染造成负载过高
- 内存频繁 GC,导致阻塞渲染操作
- 冗余资源和逻辑导致运行缓慢
- ANR
UI优化,通常是指布局优化和 View 绘制优化
布局优化
在“开发者选项”里可以打开“过度绘制开关”,从而查看当前页面 View 是否存在过绘,红色表示层级最多(4+),浅紫色表示层级最低(1),如下所示。找到那些过绘的部分,在布局文件里减少它们的背景,比如把底层 View 的背景设置成透明。同时,尽量减少 ViewGroup 嵌套的情况,通常 LinearLayout 会比 RelativeLayout 的层级要少。还有一点就是要使用设备对应分辨率的资源文件,并不是图片越清晰就越好。
在“开发者模式”里面还有另一项相关的设置,叫做“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和移动数据的情况下,我们应该直接屏蔽移动数据的网络请求
- 减少后台任务
启动优化
根据应用启动时候的状态,可以分为冷启动、热启动、暖启动三种,其中冷启动的耗时最长,对用户体验影响最大,因此谈启动优化主要是从冷启动的角度给出优化建议。
冷启动初始时,系统完成三个任务
- 启动和加载应用
- 创建应用进程
- 显示启动视图(白屏)
当应用进程创建完毕后,开始创建应用
- 创建应用对象
- 启动主线程 (MainThread)
- 创建 Main Activity
- 加载视图 (Inflating views)
- 渲染布局 (Laying out)
- 执行初始绘制
冷启动优化
- 减少首页使用的资源,懒加载
- 优化首页布局,减少层级,不绘制不可见的 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)
- AsyncTask 无参构造函数里会创建一个任务对象 mWorker(在其中执行 doInBackground),一个任务完成回调对象 mFuture,同时获取到 UI 线程的 Looper,用来在任务完成后通知 UI 线程
- 维护单例 SERIAL_EXECUTOR,用来串行执行任务
- 调用 AsyncTask.execute 时,会进入 executeOnExecutor 方法,在其内部先调用 onPreExecute,然后通过 executor 运行 mWorker,由 SERIAL_EXECUTOR 保证了串行运行,真正运行任务的是 THREAD_POOL_EXECUTOR
- 在任务完成后,通过 UI 线程的 Looper 通知 UI 线程
一个关于类的静态变量归属的问题
父类有一个 protected 的静态变量 foo,基于父类 A 创建两个子类 B1 和 B2
- 如果 B1 和 B2 不声明变量 foo,那么它们使用的 foo 是同一个,来自父类 A
- 如果 B1 和 B2 都声明变量 foo,那么它们使用各自的变量
简单概括就是说静态变量与类绑定,demo 如下
1 | // case 1 |