Android性能优化 -- 大图治理
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android性能优化 -- 大图治理相关的知识,希望对你有一定的参考价值。
实际的android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的;像列表页、查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上RecyclerView的复用机制,一般很少会发生OOM。
但是对于大图查看,通常在外界展示的是一张缩略图,点开之后放大就是原图,如果图片很大,OOM发生也是正常的,因此在加载大图的时候,可以看下面这张图
一张图片如果很大,在手机屏幕中并不能完全展示,那么其实就没有必要讲图片完全加载进来,而是可以采用分块加载的方式,只展示显示的那一部分,当图片向上滑动的时候,之前展示的区域内存能够复用,不需要开辟新的内存空间来承接新的模块,从而达到了大图的治理的目的。
1 自定义大图View
像在微信中点击查看大图,查看大图的组件就是一个自定义View,能够支持滑动、拖拽、放大等功能,因此我们也可以自定义一个类似于微信的大图查看器,从中了解图片加载优化的魅力
1.1 准备工作
class BigView : View
constructor(context: Context):super(context)
initBigView(context)
constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet)
initBigView(context)
private fun initBigView(context: Context)
本节使用的语言为kotlin
这个是我从网站上找的一张长图,大概700K左右,需要的可以自行下载,其实想要了解其中的原理和实现,不一定要找一张特别大的图片,所有的问题都是举一反三的。
class BigView : View, GestureDetector.OnGestureListener, View.OnTouchListener
//分块加载
private lateinit var mRect: Rect
//内存复用
private lateinit var mOptions: BitmapFactory.Options
//手势
private lateinit var mGestureDetector: GestureDetector
//滑动
private lateinit var mScroller: Scroller
constructor(context: Context) : super(context)
initBigView(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
initBigView(context)
private fun initBigView(context: Context)
mRect = Rect()
mOptions = BitmapFactory.Options()
mGestureDetector = GestureDetector(context, this)
mScroller = Scroller(context)
setOnTouchListener(this)
override fun onDown(e: MotionEvent?): Boolean
return false
override fun onShowPress(e: MotionEvent?)
override fun onSingleTapUp(e: MotionEvent?): Boolean
return false
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean
return false
override fun onLongPress(e: MotionEvent?)
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean
return false
override fun onTouch(v: View?, event: MotionEvent?): Boolean
return false
前面我们提到的分块加载、内存复用、手势等操作,直接在view初始化时完成,这样我们前期的准备工作就完成了。
1.2 图片宽高适配
当我们加载一张图片的时候,要让这张图片完全展示在手机屏幕上不被裁剪,就需要做宽高的适配;如果这张图片大小是80M,那么为了获取宽高而将图片加载到内存中肯定会OOM,那么在图片加载到内存之前就像获取图片的宽高该怎么办呢?BitmapFactory.Options
就提供了这个手段
fun setImageUrl(inputStream: InputStream)
//获取图片宽高
mOptions.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream,null,mOptions)
imageWidth = mOptions.outWidth
imageHeight = mOptions.outHeight
mOptions.inJustDecodeBounds = false
//开启复用
mOptions.inMutable = true
mOptions.inPreferredConfig = Bitmap.Config.RGB_565
//创建区域解码器
try
BitmapRegionDecoder.newInstance(inputStream,false)
catch (e:Exception)
requestLayout()
当设置inJustDecodeBounds为true
(记住要成对出现,使用完成之后需要设置为false),意味着我调用decodeStream
方法的时候,不会将图片的内存加载而是仅仅为了获取宽高。
然后拿到了图片的宽高之后呢,调用requestLayout
方法,会回调onMeasure
方法,这个方法大家就非常熟悉了,能够拿到view的宽高,从而完成图片的适配
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight
originScale = viewWidth / imageWidth.toFloat()
mScale = originScale
//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = (viewHeight / mScale).toInt()
这里设置Rect的right就是图片的宽度,因为原始图片的宽度可能比控件的宽度要宽,因此是将控件的宽度与图片的宽度对比获取了缩放比,那么Rect的bottom就需要等比缩放
这里的mRect可以看做是这张图片上的一个滑动窗口,无论是放大还是缩小,只要在屏幕上看到的区域,都可以看做是mRect在这张图片上来回移动截取的目标区域
1.3 BitmapRegionDecoder
在onMeasure
中,我们定义了需要加载的图片的Rect,这是一块区域,那么我们通过什么样的方式能够将这块区域的图片加载出来,就是通过BitmapRegionDecoder
区域解码器。
区域解码器,顾名思义,能够在某个区域进行图片解码展示
//创建区域解码器
try
BitmapRegionDecoder.newInstance(inputStream,false)
catch (e:Exception)
在传入图片流的时候,我们就已经创建了BitmapRegionDecoder
,同时将图片流作为参数构建了解码器,那么这个解码器其实已经拿到了整张图片的资源,因此任意一块区域,通过BitmapRegionDecoder
都能够解码展示出来
override fun onDraw(canvas: Canvas?)
super.onDraw(canvas)
mRegionDecoder ?: return
//复用bitmap
mOptions.inBitmap = mutableBitmap
mutableBitmap = mRegionDecoder?.decodeRegion(mRect, mOptions)
//画出bitmap
val mMatrix = Matrix()
mMatrix.setScale(mScale, mScale)
mutableBitmap?.let
canvas?.drawBitmap(it, mMatrix, null)
首先我们想要进行内存复用,需要调用BitmapFactory.Options
的inBitmap
,这个参数的含义就是,当我们在某块区域加载图片之后,如果图片上滑那么就需要重新加载,那么这个时候就不会重新开辟一块内存空间,而是复用之前的这块区域,所以调用BitmapRegionDecoder
的decodeRegion
方法,传入需要展示图片的区域,就能够给mutableBitmap
赋值,这样就达成了一块内存空间,多次复用的效果。
这样通过压缩之后,在屏幕中展示了这个长图的最上边部分,那么剩下就需要做的是手势事件的处理。
2 大图View的手势事件处理
通过前期的准备工作,我们已经实现了图片的区域展示,那么接下来关键在于,我们通过手势来查看完整的图片,对于手势事件的响应,在onTouch
方法中处理。
override fun onTouch(v: View?, event: MotionEvent?): Boolean
return mGestureDetector.onTouchEvent(event)
2.1 GestureDetector
通常来说,手势事件的处理都是通过GestureDetector
来完成,因此当onTouch
方法监听到手势事件之后,直接传给GestureDetector
,让GestureDetector
来处理这个事件。
override fun onDown(e: MotionEvent?): Boolean
return false
override fun onShowPress(e: MotionEvent?)
override fun onSingleTapUp(e: MotionEvent?): Boolean
return false
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean
return false
override fun onLongPress(e: MotionEvent?)
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean
return false
首先,我们先看下之前注册的GestureDetector.OnGestureListener
监听器中实现的方法:
(1)onDown
override fun onDown(e: MotionEvent?): Boolean
if(!mScroller.isFinished)
mScroller.forceFinished(true)
return true
当手指按下时,因为滑动的惯性,所以down
事件的处理就是如果图片还在滑动时,按下就停止滑动;
(2)onScroll
那么当你的手指按下之后,可能还会继续滑动,那么就是会回调到onScroll
方法,在这个方法中,主要做滑动的处理
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean
mRect.offset(0, distanceY.toInt())
//边界case处理
if (mRect.bottom > imageHeight)
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
if (mRect.top < 0)
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
postInvalidate()
return false
在onScroll
方法中,其实已经对滑动的距离做了计算(这个真的太nice了,不需要我们自己手动计算),因此只需要对mRect
展示区域进行变换即可;
但是这里会有两个边界case
,例如滑动到底部时就不能再滑了,这个时候,mRect
的底部很可能都已经超过了图片的高度,因此需要做边界的处理,那么滑动到顶部的时候同样也是需要做判断。
(3)onFling
惯性滑动。我们在使用列表的时候,我们在滑动的时候,虽然手指的滑动距离很小,但是列表划出去的距离却很大,就是因为惯性,所以GestureDetector
中对惯性也做了处理。
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean
mScroller.fling(0, mRect.top, 0, -velocityY.toInt(), 0, 0, 0, imageHeight - viewHeight)
return false
//计算惯性
override fun computeScroll()
super.computeScroll()
if (mScroller.isFinished)
return
if (mScroller.computeScrollOffset())
//正在滑动
mRect.top = mScroller.currY
mRect.bottom = mScroller.currY + (viewHeight / mScale).toInt()
postInvalidate()
这个还是比较好理解的,就是设置最大的一个惯性滑动距离,无论怎么滑动,边界值就是从顶部一划到底,这个最大的距离就是 imageHeight - viewHeight
设置了惯性滑动的距离,那么在惯性滑动时,也需要实时改变mRect
的解码范围,需要重写computeScroll
方法,判断如果是正在滑动(通过 mScroller.computeScrollOffset()
判断),那么需要改变mRect
的位置。
2.2 双击放大效果处理
我们在使用app时,双击某张图片或者双指拉动某张图片的时候,都会讲图片放大,这也是业内主流的两种图片放大的方式。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight
//缩放比
val radio = viewWidth / imageWidth.toFloat()
//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = viewHeight
我们先看一下不能缩放时,mRect
的赋值;那么当我们双击放大时,left
和top
的位置不会变,因为图片放大了,但是控件的大小不会变,因此left
的最大值就是控件的宽度,bottom
的最大值就是控件的高度。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight
originScale = viewWidth / imageWidth.toFloat()
mScale = originScale
//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = Math.min(imageWidth, viewWidth)
mRect.bottom = Math.min((viewHeight / mScale).toInt(), viewHeight)
这里就将onMeasure
进行改造;那么对于双击事件的处理,可以使用GestureDetector.OnDoubleTapListener
来处理,在onDoubleTap
事件中回调。
override fun onDoubleTap(e: MotionEvent?): Boolean
if (mScale < originScale * 2)
mScale = originScale * 2
else
mScale = originScale
postInvalidate()
return false
这里做了缩放就是判断mScale
的值,因为一开始进来不是缩放的场景,因此 mScale = originScale
,当双击之后,需要将mScale
扩大2倍,当重新绘制的时候,Bitmap
就放大了2倍。
那么当图片放大之后,之前横向不能滑动现在也可以滑动查看图片,所以需要处理,同时也需要考虑边界case
override fun onDoubleTap(e: MotionEvent?): Boolean
if (mScale < originScale * 2)
mScale = originScale * 2
else
mScale = originScale
//
mRect.right = mRect.left + (viewWidth / mScale).toInt()
mRect.bottom = mRect.top + (viewHeight / mScale).toInt()
if (mRect.bottom > imageHeight)
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
if (mRect.top < 0)
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
if(mRect.right > imageWidth)
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
if(mRect.left < 0)
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
postInvalidate()
return false
当双击图片之后,mRect
解码的区域也随之改变,因此需要对right
和bottom
做相应的改变,图片放大或者缩小,都是在控件宽高的基础之上
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean
mRect.offset(distanceX.toInt(), distanceY.toInt())
//边界case处理
if (mRect.bottom > imageHeight)
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
if (mRect.top < 0)
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
if(mRect.left < 0)
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
if(mRect.right > imageWidth)
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
postInvalidate()
return false
因为需要左右滑动,那么onScroll
方法也需要做相应的改动,mRect
的offset
需要加上x轴的偏移量。
2.3 手指放大效果处理
上一小节介绍了双击事件的效果处理,那么这一节就介绍另一个主流的放大效果实现 - 手指缩放,是依赖 ScaleGestureDetector
,其实跟GestureDetector
的使用方式一致,这里就不做过多的赘述。
mScaleGestureDetector = ScaleGestureDetector(context, ScaleGesture())
在初始化ScaleGestureDetector
的时候,需要传入一个ScaleGesture
内部类,集成ScaleGestureDetector.SimpleOnScaleGestureListener
,在onScale
方法中获取缩放因子来绘制
inner class ScaleGesture : ScaleGestureDetector.SimpleOnScaleGestureListener()
override fun onScale(detector: ScaleGestureDetector?): Boolean
var scale = detector?.scaleFactor ?: mScale//可以代替mScale
if (scale < originScale)
scale = originScale
else if (scale > originScale * 2)
scale = originScale * 2
//在原先基础上缩放
mRect.right = mRect.left + (viewWidth / scale).toInt()
mRect.bottom = mRect.top + (viewHeight / scale).toInt()
mScale = scale
postInvalidate()
return super.onScale(detector)
这里别忘记了别事件传递出来,对于边界case
可自行处理
override fun onTouch(v: View?, event: MotionEvent?): Boolean
mGestureDetector.onTouchEvent(event)
mScaleGestureDetector.onTouchEvent(event)
return true
下面附上大图治理的流程图
黄颜色模块: BitmapFactory.Options
配置,避免整张大图直接加载在内存当中,通过开启内存复用(inMutable
),使用区域解码器,绘制一块可见区域‘
浅黄色模块: View
的绘制流程
作者:Ghelper
链接:https://juejin.cn/post/7131385674694918180
来源:稀土掘金
以上是关于Android性能优化 -- 大图治理的主要内容,如果未能解决你的问题,请参考以下文章