Android必知必会——Drawable

作者: 不正经的创造者 | 来源:发表于2020-05-20 16:55 被阅读0次

    Drawable概览

    如果需要在应用内显示静态图片,可以使用 Drawable 类及其子类绘制形状和图片。Drawable 是可绘制对象的常规抽象。不同的子类可用于特定的图片场景,可以对其进行扩展以定义行为方式独特的可绘制对象。

    Drawable的定义和实例化

    可以通过如下三种方式定义和实例化Drawable:

    • 构造函数

      使用现有的Drawable子类,如ShapeDrawable,用来绘制基本的物理图形;ColorDrawable,用来绘制特定的颜色;BitmapDrawable,用来绘制特定的位图等。

      当然还可以直接继承Drawable,自定义绘制行为:

      //此示例是一个用来绘制区域最大圆形的Drawable
      class MyDrawable : Drawable() {
          private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0) }
      
          override fun draw(canvas: Canvas) {
              // 获取可绘制区域的宽高,得到可绘制最大圆的半径
              val width: Int = bounds.width()
              val height: Int = bounds.height()
              val radius: Float = Math.min(width, height).toFloat() / 2f
      
              // 由中心画一个圆
              canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, redPaint)
          }
      
          override fun setAlpha(alpha: Int) {
              // 必须重写的方法,处理透明度
          }
      
          override fun setColorFilter(colorFilter: ColorFilter?) {
              // 必须重写的方法,处理颜色过滤器
          }
      
          override fun getOpacity(): Int =
              // 必须重写的方法,返回此Drawable的不透明度/透明度
              //返回值必须是如下几个值:
              //PixelFormat.UNKNOWN
              //PixelFormat.TRANSLUCENT 只有绘制的地方才覆盖底下的内容
              //PixelFormat.TRANSPARENT 透明,完全不显示任何东西
              //PixelFormat.OPAQUE 完全不透明,遮盖在它下面的所有内容
              PixelFormat.OPAQUE
      }
      
      
    • 通过资源图片创建可绘制对象

      最直接的方式,就在资源目录下存放特定类型的图片文件如PNG、JPG、GIF等。

      值得注意的一点是,res/drawable/目录下的图片资源可由aapt工具在构建过程中自动完成无损图片压缩优化。但是在res/raw/文件夹下的图片,appt不会对其进行修改。

      在通过Resources获取图片资源文件得到Drawable对象时,如果同一个资源实例化了多个Drawable对象,并更改其中一个对象的属性(如透明度),则其他对象也会受到影响。

    val myImage1: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)
    
    val myImage2: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)
    
    myImage1.setAlpha(1)//myImage2也会受到影响
    
    
    • 通过XML资源创建可绘制对象

      例如TransitionDrawable:

    <transition xmlns:android="http://schemas.android.com/apk/res/android">
            <item android:drawable="@drawable/image_expand">
            <item android:drawable="@drawable/image_collapse">
        </transition>
    
    

    VectorDrawable

    VectorDrawable 是一种矢量图形,在 XML 文件中定义为一组点、线条和曲线及其相关颜色信息。使用矢量可绘制对象的主要优势在于图片可缩放。可以在不降低显示质量的情况下缩放图片,也就是说,可以针对不同的屏幕密度调整同一文件的大小,而不会降低图片质量。这不仅能缩减 APK 文件大小,还能减少开发者维护工作。还可以对动画使用矢量图片,具体方法是针对各种显示屏分辨率使用多个 XML 文件,而不是多张图片。

    VectorDrawable 定义静态可绘制对象。与 SVG 格式类似,每个矢量图形定义为树状层次结构,由 path 和 group 对象构成。每个 path 都包含对象轮廓的几何图形,而 group 包含转换的详细信息。所有路径都是按照其在 XML 文件中显示的顺序绘制的。


    借助 Vector Asset Studio 工具,可轻松地将矢量图形作为 XML 文件添加到项目中。

    对于矢量图形,还有另外一个类AnimatedVectorDrawable,它 可以为矢量图形的属性添加动画。

    Android5.0开始支持使用矢量图形,如果要在更低版本使用,那么可以通过VectorDrawableCompat 和 AnimatedVectorDrawableCompat来进行兼容。在控件中,如ImageView,可以使用srcCompat属性,来引用矢量图形。

    Bitmap

    Bitmap是一种独立于显示器的位图数字图像文件格式。BMP文件通常是不压缩的,所以它们通常比同一幅图像的压缩图像文件格式要大很多。

    Bitmap存储的核心,在于图像的信息。即宽度上有多少像素点,高度上有多少像素点,然后每个像素点的具体信息。

    而针对每个像素点,通常保存的颜色深度有2(1位)、16(4位)、256(8位)、65536(16位)和1670万(24位)种颜色。

    那么当Bitmap中的像素越多,每个点可表达的颜色越多,那么这个图片就越清晰、颜色越丰富。

    Android中实例化Bitmap时可选择的质量类型:

    • ALPHA_8 (8 / 8 = 1)字节/像素

    只保存透明度信息,没有颜色信息

    • RGB_565 (红绿蓝 5+6+5=16 16 / 8 = 2)字节/像素

    保存红绿蓝信息,没有透明度

    • ARGB_4444 (透明度、红绿蓝 4+4+4+4=16 16 / 8 = 2)字节/像素

    透明度和红绿蓝都有,但是能使用的色彩数少

    • ARGB_8888 (透明度、红绿蓝 8+8+8+8=32 32 / 8 = 4)字节/像素

    透明度和红绿蓝都有,但是能使用的色彩数较多

    • RGBA_F16 (透明度、红绿蓝 16+16+16+16=48 48 / 8 = 6)字节/像素

    透明度和红绿蓝都有,但是能使用的色彩数多

    Bitmap使用内存的计算

    计算公式(针对内存中的Bitmap):

    使用内存 = 横向像素数 * 竖向像素数 * 每个像素字节数
    
    

    例如,对于像素数为1024 * 1024、质量为ARGB_8888的Bitmap来说,要将其加载到内存中,需要的内存为:

    1024 * 1024 * 4 = 4MB

    在Android应用中加载Bitmap比较复杂,原因有多种:

    • 位图很容易就会耗尽应用的内存预算。

    • 在界面线程中加载位图会降低应用的性能,导致响应速度变慢,甚至会导致系统显示 ANR 消息。因此,在使用位图时,必须正确地管理线程处理。

    • 如果应用将多个位图加载到内存中,需要娴熟地管理内存和磁盘缓存。否则,应用界面的响应速度和流畅性可能会受到影响。

    高效加载大图

    既然Bitmap是实打实的图片数据,占用内存巨大,那么在某些场景必须加载大图时(如加载相册中的高清大图),应该如何处理呢?

    可以按照如下几个步骤,高效的加载大图:

    • inJustDecodeBounds

      通过BitmapFactory加载Bitmap时,传入Config参数,并将其inJustDecodeBounds设置为true,那么在加载过程中,BitmapFactory不会为其自动申请内存,而是进读取位图的尺寸和类型。

        val options = BitmapFactory.Options().apply {
            inJustDecodeBounds = true
        }
        BitmapFactory.decodeResource(resources, R.id.myimage, options)
        val imageHeight: Int = options.outHeight
        val imageWidth: Int = options.outWidth
        val imageType: String = options.outMimeType
    
    • inSampleSize

      通过inJustDecodeBounds,可以获知位图的尺寸,这之后就可以在其他几个维度确定是否要降低图片的采样率:

      1.在内存中加载完整图片的估计内存使用量。

      2.根据应用的任何其他内存要求,可分配用于加载此图片的内存量。

      3.图片要载入到的目标 ImageView 或界面组件的尺寸。 例如,如果 1024x768 像素的图片最终会在 ImageView 中显示为 128x96 像素缩略图,则不值得将其加载到内存中。

      4.当前设备的屏幕大小和密度。

    例如,分辨率为 2048*1536 且以 4 作为 inSampleSize 进行解码的图片会生成大约 (2048/4)512 *(1536/4)384 的位图。将此图片加载到内存中需使用 0.75MB,而不是完整图片所需的 12MB(假设位图配置为 ARGB_8888)。

    inSampleSize的数值,应为2的幂。即1、2、4、8...

    那么,接下来要做的就是根据位图实际尺寸和实际需要尺寸,来计算实际的采用率:

      fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
          // 图像的原始高度和宽度
          val (height: Int, width: Int) = options.run { outHeight to outWidth }
          var inSampleSize = 1
    
          if (height > reqHeight || width > reqWidth) {
    
              val halfHeight: Int = height / 2
              val halfWidth: Int = width / 2
    
              // 计算最大的inSampleSize值,该值为2的幂,并且使height和width都大于请求的height和width。
              while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                  inSampleSize *= 2
              }
          }
    
          return inSampleSize
    

    综上,加载大图时,整体流程是这样的:

    fun decodeSampledBitmapFromResource(
               res: Resources,
               resId: Int,
               reqWidth: Int,
               reqHeight: Int
       ): Bitmap {
           // 先获取图片尺寸信息
           return BitmapFactory.Options().run {
               inJustDecodeBounds = true
               BitmapFactory.decodeResource(res, resId, this)
    
               // 计算 inSampleSize
               inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
    
               // 根据计算的采样率,最终加载实际的位图
               inJustDecodeBounds = false
    
               BitmapFactory.decodeResource(res, resId, this)
           }
       }
    

    缓存位图

    将单个位图加载到界面中非常简单,但如果需要同时加载较多的图片,情况就会变得复杂。在很多情况下(比如ListView、GridView或ViewPager等),屏幕上的图片与可能很快会滚动到屏幕上的图片加起来,数量是无限的。

    对于这类组件,系统会通过循环利用移出屏幕的子视图来限制其对内存的占用。垃圾回收器也会释放已加载的位图,但当用户又滑回之前被回收的条目时,可以通过内存和磁盘缓存,让组件可以快速重新加载经过处理的图片。

    • 内存缓存——LruCache

    配置LruCahce初始化相关参数

    private lateinit var memoryCache: LruCache<String, Bitmap>
    
       override fun onCreate(savedInstanceState: Bundle?) {
           ...
           // 获取最大可用VM内存(KB单位),超过此数量将抛出OutOfMemory异常。 
           val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    
           // 将可用内存的1/8用作此内存缓存。
           val cacheSize = maxMemory / 8
    
           memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
    
               override fun sizeOf(key: String, bitmap: Bitmap): Int {
                   // 缓存大小单位是KB
                   return bitmap.byteCount / 1024
               }
           }
           ...
       }
    

    使用内存缓存:

    fun loadBitmap(resId: Int, imageView: ImageView) {
           val imageKey: String = resId.toString()
           //内存缓存中有,那么直接用
           val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also {
               mImageView.setImageBitmap(it)
           } ?: run {
           //内存缓存中没有,那么异步加载
               mImageView.setImageResource(R.drawable.image_placeholder)
               val task = BitmapWorkerTask()
               task.execute(resId)
               null
           }
       }
    

    异步加载图片时,需要及时的将位图存入内存缓存:

    private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
           ...
           // 异步加载位图,加载成功后,及时将位图放入内存缓存
           override fun doInBackground(vararg params: Int?): Bitmap? {
               return params[0]?.let { imageId ->
                   decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap ->
                       addBitmapToMemoryCache(imageId.toString(), bitmap)
                   }
               }
           }
           ...
       }
    
    • 磁盘缓存

    内存缓存有助于加快对最近查看过的位图的访问,但不能依赖于此缓存中保留的图片。GridView 这样拥有较大数据集的组件很容易将内存缓存填满。应用可能被其他任务(如电话)中断,而在后台时,应用可能会被终止,而内存缓存则会销毁。

    在这些情况下,可以使用磁盘缓存来保存经过处理的位图,并在图片已不在内存缓存中时帮助减少加载时间。当然,从磁盘获取图片比从内存中加载缓慢,而且应该在后台线程中完成,因为磁盘读取时间不可预测。

    完整的图片存取方式:

    private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB的磁盘缓存空间
       private const val DISK_CACHE_SUBDIR = "thumbnails"
       ...
       private var diskLruCache: DiskLruCache? = null
       private val diskCacheLock = ReentrantLock()
       //即使是初始化磁盘缓存也需要执行磁盘操作,因此不应在主线程上执行。
       //不过,这也意味着可能会在初始化之前访问该缓存。
       //为了解决此问题,利用了一个 lock 对象来确保应用在磁盘缓存初始化之前不会从该缓存中读取数据。
       private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
       private var diskCacheStarting = true
    
       override fun onCreate(savedInstanceState: Bundle?) {
           ...
           // 初始化内存缓存
           ...
           // 异步初始化磁盘缓存
           val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
           InitDiskCacheTask().execute(cacheDir)
           ...
       }
    
       internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
           override fun doInBackground(vararg params: File): Void? {
               diskCacheLock.withLock {
                   val cacheDir = params[0]
                   diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
                   diskCacheStarting = false // 完成初始化
                   diskCacheLockCondition.signalAll() // 唤醒等待的线程
               }
               return null
           }
       }
    
       internal inner class  BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
           ...
    
           // 异步解码图像
           override fun doInBackground(vararg params: Int?): Bitmap? {
               val imageKey = params[0].toString()
    
               // 异步检查硬盘缓存
               return getBitmapFromDiskCache(imageKey) ?:
                       // 硬盘缓存中未找到
                       decodeSampledBitmapFromResource(resources, params[0], 100, 100)
                               ?.also {
                                   // 将最终位图添加到缓存
                                   addBitmapToCache(imageKey, it)
                               }
           }
       }
    
       fun addBitmapToCache(key: String, bitmap: Bitmap) {
           // 校验内存缓存中是否有可用缓存,没有则放入内存缓存
           if (getBitmapFromMemCache(key) == null) {
               memoryCache.put(key, bitmap)
           }
    
           // 同样放入磁盘缓存
           synchronized(diskCacheLock) {
               diskLruCache?.apply {
                   if (!containsKey(key)) {
                       put(key, bitmap)
                   }
               }
           }
       }
    
       fun getBitmapFromDiskCache(key: String): Bitmap? =
               diskCacheLock.withLock {
                   while (diskCacheStarting) {
                       try {
                           diskCacheLockCondition.await()
                       } catch (e: InterruptedException) {
                       }
    
                   }
                   return diskLruCache?.get(key)
               }
    
       // 创建指定的应用程序缓存目录的唯一子目录。尝试在外部使用,但如果未安装,则退回到内部存储。
       fun getDiskCacheDir(context: Context, uniqueName: String): File {
           // 检查是否安装了介质或内置了存储,如果是,尝试使用外部缓存目录,否则使用内部缓存目录
           val cachePath =
                   if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
                           || !isExternalStorageRemovable()) {
                       context.externalCacheDir.path
                   } else {
                       context.cacheDir.path
                   }
    
           return File(cachePath + File.separator + uniqueName)
       }
    

    管理Bitmap内存

    对于不同的Android版本,位图内存管理发生了如下的变更:

    • 在 Android Android 2.2(API 级别 8)及更低版本上,当发生垃圾回收时,应用的线程会停止。这会导致延迟,从而降低性能。Android 2.3 添加了并发垃圾回收功能,这意味着系统不再引用位图后,很快就会回收内存。

    • 在 Android 2.3.3(API 级别 10)及更低版本上,位图的像素数据存储在native内存中。它与存储在 Dalvik 堆中的位图本身是分开的。native内存中的像素数据并不以可预测的方式释放,可能会导致应用短暂超出其内存限制并崩溃。

    • 从 Android 3.0(API 级别 11)到 Android 7.1(API 级别 25),像素数据会与关联的位图一起存储在 Dalvik 堆上。

    • 在 Android 8.0(API 级别 26)及更高版本中,位图像素数据存储在原生堆中。

    因此在不同的Android版本中,应采用不同的管理方案:

    • 在 Android 2.3.3(API 级别 10)及更低版本上,建议使用 recycle(),可以尽快回收内存。

    • Android 3.0(API 级别 11)引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时尝试重复使用现有位图。这意味着位图的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过,inBitmap 的使用方式存在限制,要求需要重用的位图是可变的。特别是在 Android 4.4(API 级别19)之前,系统仅支持大小相同(像素数相同且采样率为1)的位图。

    关于BitmapFactory.Options

    这个参数的作用非常大,它可以设置Bitmap的采样率,通过改变图片的宽度、高度、缩放比例等,以达到减少图片的像素的目的。总的来说,通过设置这个值,可以更好地控制、显示,使用位图。

    以下是其个属性及其含义:

    属性 类型 含义
    inJustDecodeBounds boolean 是否只解析图片信息
    inSampleSize int 采样率(每隔多少个样本采样一次作为结果,比如4,代表没4个像素取1个作为结果返回,宽高都变为原来的1/4,总体为原来的1/16)
    inScaled boolean 在需要缩放时,是否对当前文件进行缩放。false则不进行缩放;true或不设置,则会根据文件夹分辨率和屏幕分辨率动态缩放
    inDensity int 设置文件所在资源文件夹的屏幕分辨率
    inTargetDensity int 表示真实显示的屏幕分辨率,缩放比 = inTargetDensity/inDensity
    inScreenDensity int 正在使用的实际屏幕的像素密度,目前没什么用
    inPreferredConfig enum 设置像素的存储格式。RGB_565,ARGB_8888等
    inMutable boolean 如果设置true,则解码方法将始终返回可变(可以修改像素信息)的位图,而不是不变(不可修改)的位图。
    inBitmap Bitmap 重用此Bitmap,需要此Bitmap是可修改
    outConfig Config 如果知道,解码位图将具有的配置
    outHeight int 位图的最终高度
    outWidth int 位图的最终宽度
    inDither boolean 是否抖动,如果设置true,解码器将尝试抖动解码图像。例如图片原本是100px200px,而实际需要150px300px,设置此参数后,会将原来的100像素平铺,多出来的空白利用相邻两个颜色生成“中间色”来过渡。

    关于Bitmap的可变和不可变

    对于可变的Bitmap来说,通过setPixel(int x,int y,int color)等函数可以设置其中的像素值,而不可变的Bitmap使用这些方法就会报错。

    那么什么情况下生成的Bitmap可变,什么时候不可变呢?答案就是:通过BitmapFactory加载的Bitmap都是不可变的;只有Bitmap中的几个函数创建的Bitmap才是像素可变的。这几个函数是:

    1.copy(Config config, boolean isMutable)//isMutable传入true
    2.createBitmap(@Nullable DisplayMetrics display, int width, int height,
            @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace)
    3.createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
            boolean filter)
    

    相关文章

      网友评论

        本文标题:Android必知必会——Drawable

        本文链接:https://www.haomeiwen.com/subject/pfunohtx.html