Android:窗口、自定义view、bitmap

1、ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联


image

2、自定义View-绘制流程概述

  • 一个 Activity 包含一个Window,Window是一个抽象基类,是 Activity 和整个 View 系统交互的接口,只有一个实现子类PhoneWindow,提供了一系列窗口的方法,比如设置背景,标题等。一个PhoneWindow 对应一个DecorView 和一个 ViewRootImpl,DecorView 是ViewTree 里面的顶层布局,是继承于FrameLayout,包含两个子View,一个id=statusBarBackground 的 View 和 LineaLayout,LineaLayout 里面包含 title 和content,title就是平时用的TitleBar或者ActionBar, content是FrameLayout,activity通过 setContent()加载布局的时候加载到这个View上。ViewRootImpl就是建立 Window 和DecorView 之间的联系。
  • Android View 的绘制流程
  • MeasureSpec的数值来源:Android中View内部类MeasureSpec研究
// 参数size和mode是我们在xml文件中设置的
public static int makeMeasureSpec(int size, int mode) 方法

4、Android Handler
6、Android Bitmap

  • 子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的。

  • 绘制视图所对应方法的作用:

  • measure过程:测量控件的大小
  • layout过程:根据measure测量得到的控件大小,确定控件在屏幕的位置(上下左右的位置)
  • draw过程:根据控件的大小和位置,对控件进行绘制
  • view的measure过程:
  • 在单一view的测量中:measure() 方法是final方法,子类不能重写
  • measure -> onMeasure -> setMeasuredDimension + getDefaultSize
  • getDefaultSize():根据View宽/高的测量规格计算View的宽/高值
  • setMeasuredDimension():存储测量后的View的宽/高值
  • viewGroup的measure过程:
  • 遍历 测量所有子View的尺寸,合并所有子View的尺寸,最终得到ViewGroup父视图的测量值。
  • 单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有。
  • measure方法调用onMeasure方法;在onMeasure方法中:1、遍历所有子view的测量:measureChildren(),在measureChildren()方法中会调用子view的measure方法;2、合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:measureCarson(),这个方法需要自己实现;3、存储测量后View宽/高的值:调用setMeasuredDimension()
  • 单个view的layout过程:
  • 当个view的onLayout方法是空实现,所以只有layout方法,在改方法中调用setFrame()或setOpticalFrame()确定view的位置
  • viewGroup的layout过程:
  • 在layout方法中,调用onLayout方法和setFrame()或setOpticalFrame();onLayout方法会遍历子控件的测量,调用子控件的layout方法
  • 单个view的draw过程:
    1. 绘制view背景: drawBackground
    1. 绘制view内容: onDraw 空实现,需要我们自己复写
    1. 绘制子View:dispatchDraw()
      // 单一View无子View,故View 中:默认为空实现
      // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
    1. 绘制装饰(渐变框,滑动条等等):onDrawForeground()
  • viewGroup的draw过程;
  • 在dispatchDraw方法中,调用在调用drawChild方法,在drawChild方法中调用子控件的draw方法
  • getWidth / getHeight 与 getMeasuredWidth / getMeasuredHeight 获取的宽/高有什么区别?

getWidth / getHeight:获得View最终的宽 / 高
getMeasuredWidth / getMeasuredHeight:获得 View测量的宽 / 高
但是一般是相等的

  • 注:
  • 在调用该方法之前必须要完成 layout 过程
  • 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
  • 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制(因为draw方法包括所有的绘制,但onDraw方法只是绘制view的内容)
  • 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制

2、MeasureSpec:

  • UNSPECIFIED不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
  • EXACTLY精确测量模式,视图宽高指定为 match_parent 或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值
  • AT_MOST最大值测量模式,当视图的宽高指定为 wrap_content 时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸
  • 注意:

① 在某些情况下,需要多次测量Measure才能确定View的最终宽高
② 因此,在上述情况下,Measure过程后得到的宽高不准确。
③ 因此,建议在Layout过程中onLayout去获取最终的宽高。

  • 单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有。

3、一般来说,使用多进程会造成以下几个方面的问题:

  • 静态成员和单例模式完全失效
  • 线程同步机制完全失效
  • SharedPreferences 的可靠性下降
  • Application 会多次创建

5、Window 概念与分类:
Window 是一个抽象类,它的具体实现是 PhoneWindow。WindowManager 是外界访问 Window 的入口,Window 的具体实现位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一个 IPC 过程。Android 中所有的视图都是通过 Window 来呈现,因此 Window 实际是 View 的直接管理者。

Window 类型 说明 层级
Application Window 对应着一个 Activity 1~99
Sub Window 不能单独存在,只能附属在父 Window 中,如 Dialog 等 1000~1999
System Window 需要权限声明,如 Toast 和 系统状态栏等 2000~2999

6、window的三大操作:addView、upView、removeView
7、Bitmap 中有两个内部枚举类:

Config 是用来设置颜色配置信息
CompressFormat 是用来设置压缩方式

Config 单位像素所占字节数 解析
Bitmap.Config.ALPHA_8 1 颜色信息只由透明度组成,占8位
Bitmap.Config.ARGB_4444 2 颜色信息由rgba四部分组成,每个部分都占4位,总共占16位
Bitmap.Config.ARGB_8888 4 颜色信息由rgba四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置
Bitmap.Config.RGB_565 2 颜色信息由rgb三部分组成,R占5位,G占6位,B占5位,总共占16位
RGBA_F16 8 Android 8.0 新增(更丰富的色彩表现HDR)
HARDWARE Special Android 8.0 新增 (Bitmap直接存储在graphic memory)

通常我们优化 Bitmap 时,当需要做性能优化或者防止 OOM,我们通常会使用 Bitmap.Config.RGB_565 这个配置,因为 Bitmap.Config.ALPHA_8 只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444 显示图片不清楚, Bitmap.Config.ARGB_8888 占用内存最多。

CompressFormat 解析
Bitmap.CompressFormat.JPEG 表示以 JPEG 压缩算法进行图像压缩,压缩后的格式可以是 .jpg 或者 .jpeg,是一种有损压缩
Bitmap.CompressFormat.PNG 颜色信息由 rgba 四部分组成,每个部分都占 4 位,总共占 16 位
  • 操作:
Matrix matrix = new Matrix();  
// 缩放 
matrix.postScale(0.8f, 0.9f);  
// 左旋,参数为正则向右旋
matrix.postRotate(-45);  
// 平移, 在上一次修改的基础上进行再次修改 set 每次操作都是最新的 会覆盖上次的操作
matrix.postTranslate(100, 80);
// 裁剪并执行以上操作
Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);

保存图片资源:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
File file = new File(getFilesDir(),"test.jpg");
if(file.exists()){
   file.delete();
}
try {
   FileOutputStream outputStream=new FileOutputStream(file);
   bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream);
   outputStream.flush();
   outputStream.close();
} catch (FileNotFoundException e) {
   e.printStackTrace();
} catch (IOException e) {
   e.printStackTrace();
}
//释放bitmap的资源,这是一个不可逆转的操作
bitmap.recycle();

图片压缩:

   public static Bitmap compressImage(Bitmap image) {
    if (image == null) {
        return null;
    }
    ByteArrayOutputStream baos = null;
    try {
        baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        byte[] bytes = baos.toByteArray();
        ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
        Bitmap bitmap = BitmapFactory.decodeStream(isBm);
        return bitmap;
    } catch (OutOfMemoryError e) {
        e.printStackTrace();
    } finally {
        try {
            if (baos != null) {
                baos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return null;
}

Option类

常用方法 说明
boolean inJustDecodeBounds 如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息
int inSampleSize 图片缩放的倍数
int outWidth 获取图片的宽度值
int outHeight 获取图片的高度值
int inDensity 用于位图的像素压缩比
int inTargetDensity 用于目标位图的像素压缩比(要生成的位图)
byte[] inTempStorage 创建临时文件,将图片存储
boolean inScaled 设置为true时进行图片压缩,从inDensity到inTargetDensity
boolean inDither 如果为true,解码器尝试抖动解码
Bitmap.Config inPreferredConfig 设置解码器这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes
String outMimeType 设置解码图像
boolean inPurgeable 当存储Pixel的内存空间在系统内存不足时是否可以被回收
boolean inInputShareable inPurgeable为true情况下才生效,是否可以共享一个InputStream
boolean inPreferQualityOverSpeed 为true则优先保证Bitmap质量其次是解码速度
boolean inMutable 配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段
int inScreenDensity 当前屏幕的像素密度

基本使用:

try {
    FileInputStream fis = new FileInputStream(filePath);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    // 设置inJustDecodeBounds为true后,再使用decodeFile()等方法,并不会真正的分配空间,即解码出来的Bitmap为null,但是可计算出原始图片的宽度和高度,即options.outWidth和options.outHeight
    BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;

    if (srcHeight > height || srcWidth > width) {
        if (srcWidth > srcHeight) {
            inSampleSize = Math.round(srcHeight / height);
        } else {
            inSampleSize = Math.round(srcWidth / width);
        }
    }

    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;

    return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
} catch (Exception e) {
    e.printStackTrace();
}

Bitmap 类的构造方法都是私有的,所以开发者不能直接 new 出一个 Bitmap 对象,只能通过 BitmapFactory 类的各种静态方法来实例化一个 Bitmap。仔细查看 BitmapFactory 的源代码可以看到,生成 Bitmap 对象最终都是通过 JNI 调用方式实现的。所以,加载 Bitmap 到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java 部分的,一部分是 C 部分的。这个 Bitmap 对象是由 Java 部分分配的,不用的时候系统就会自动回收了,但是那个对应的 C 可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用 recycle() 方法来释放 C 部分的内存。从 Bitmap 类的源代码也可以看到,recycle() 方法里也的确是调用了 JNI 方法了的。

8、Context 本身是一个抽象类,是对一系列系统服务接口的封装,包括:内部资源、包、类加载、I/O操作、权限、主线程、IPC 和组件启动等操作的管理。ContextImpl, Activity, Service, Application 这些都是 Context 的直接或间接子类

9、SharedPreferences 采用key-value(键值对)形式, 主要用于轻量级的数据存储, 尤其适合保存应用的配置参数, 但不建议使用 SharedPreferences 来存储大规模的数据, 可能会降低性能

10、SharedPreferences源码有用synchronize进行加锁同步

11、Handler 有两个主要用途:
(1)安排 Message 和 runnables 在将来的某个时刻执行;
(2)将要在不同于自己的线程上执行的操作排入队列。(在多个线程并发更新UI的同时保证线程安全。)
只有主线程能对UI进行操作,所以在对UI进行跟改之前,ViewRootImpl 对UI操作做了验证,这个验证工作是由 ViewRootImpl的 checkThread 方法完成:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

12、ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,其他线程则无法获取。Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。当不同线程访问同一个ThreadLocal 的 get方法,ThreadLocal 内部会从各自的线程中取出一个数组,然后再从数组中根据当前 ThreadLcoal 的索引去查找对应的value值:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

···
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

13、Android 提供了几种途径来从其他线程访问 UI 线程:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)

Android单线程模式必须遵守的规则:

  • 不阻塞UI线程
  • 不在主线程之外访问UI工具包

14、HandlerThread 集成了 Thread,却和普通的 Thread 有显著的不同。普通的 Thread 主要用于在 run 方法中执行一个耗时任务,而 HandlerThread 在内部创建了消息队列,外界需要通过 Handler 的消息方式通知 HanderThread 执行一个具体的任务。

15、IntentService 可用于执行后台耗时的任务,当任务执行后会自动停止,由于其是 Service 的原因,它的优先级比单纯的线程要高,所以 IntentService 适合执行一些高优先级的后台任务。在实现上,IntentService 封装了 HandlerThread 和 Handler。IntentService 第一次启动时,会在 onCreatea 方法中创建一个 HandlerThread,然后使用的 Looper 来构造一个 Handler 对象 mServiceHandler,这样通过 mServiceHandler 发送的消息最终都会在 HandlerThread 中执行。每次启动 IntentService,它的 onStartCommand 方法就会调用一次,onStartCommand 中处理每个后台任务的 Intent,onStartCommand 调用了 onStart 方法。可以看出,IntentService 仅仅是通过 mServiceHandler 发送了一个消息,这个消息会在 HandlerThread 中被处理。mServiceHandler 收到消息后,会将 Intent 对象传递给 onHandlerIntent 方法中处理,执行结束后,通过 stopSelf(int startId) 来尝试停止服务。(stopSelf() 会立即停止服务,而 stopSelf(int startId) 则会等待所有的消息都处理完毕后才终止服务)。

16、RecyclerView 优化

  • 数据处理和视图加载分离:数据的处理逻辑尽可能放在异步处理,onBindViewHolder 方法中只处理数据填充到视图中。
  • 数据优化:分页拉取远端数据,对拉取下来的远端数据进行缓存,提升二次加载速度;对于新增或者删除数据通过 DiffUtil 来进行局部刷新数据,而不是一味地全局刷新数据。
public class AdapterDiffCallback extends DiffUtil.Callback {
    
    private List mOldList;
    private List mNewList;
    
    public AdapterDiffCallback(List oldList, List newList) {
        mOldList = oldList;
        mNewList = newList;
        DiffUtil.DiffResult
    }
    
    @Override
    public int getOldListSize() {
        return mOldList.size();
    }

    @Override
    public int getNewListSize() {
        return mNewList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldList.get(oldItemPosition).getClass().equals(mNewList.get(newItemPosition).getClass());
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
    }
}
  • 布局优化:减少布局层级,简化 ItemView

  • 升级 RecycleView 版本到 25.1.0 及以上使用 Prefetch 功能

  • 通过重写 RecyclerView.onViewRecycled(holder) 来回收资源

  • 如果 Item 高度是固定的话,可以使用 RecyclerView.setHasFixedSize(true); 来避免 requestLayout 浪费资源

  • 对 ItemView 设置监听器,不要对每个 Item 都调用 addXxListener,应该大家公用一个 XxListener,根据 ID 来进行不同的操作,优化了对象的频繁创建带来的资源消耗

  • 如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool),来共用一个 RecycledViewPool。

你可能感兴趣的:(Android:窗口、自定义view、bitmap)