美文网首页
Android面试题

Android面试题

作者: 奈文_摩尔 | 来源:发表于2020-02-10 20:01 被阅读0次

    一、图片压缩处理
    图片的存在形式有三种:
    1、文件形式(以二进制形式存在于硬盘上)
    2、流的形式(以二进制形式存在于内存中)
    3、Bitmap形式(位图图像,是由称作像素的单个点组成的)
    这三种形式的区别:文件形式和流的形式对图片大小没有影响,当以Bitmap形式存在时,其占用内存会瞬间变大。

    一张图片(Bitmap)占用的内存大小=图片长度x图片宽度x单位像素占用的字节数

    图片常用的压缩格式:
    ARGB A:透明度 R:红色 G:绿色 B:蓝色

    ALPHA_8:表示8位ALPHA位图,即A=8,一个像素占用一个字节,没有其他颜色,只有透明度。
    ARGB_4444:表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素占16位,两个字节。
    ARGB_8888:表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素占32位,四个字节。
    RGB_565:表示16位RGB位图,即R=5,G=6,B=5,一个像素占16位,他没有透明度,两个字节。
    我们在做压缩处理的时候,可以先通过改变Bitmap的图片编码格式,来达到压缩的效果,其实压缩最主要就是要么改变其宽高,要么就通过减少其单个像素占用的内存。

    常用的压缩方法:
    1、质量压缩:质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。
      我们可以看到有个参数:quality,可以调节你压缩的比例,但是还要注意一点就是,质量压缩对png格式这种图片没有作用,因为png是无损压缩。
    2、采样率压缩:采样率压缩其原理是缩放bitamp的尺寸,通过调节其inSampleSize参数,比如调节为2,宽高会为原来的1/2,内存变回原来的1/4。
    3、缩放法压缩:放缩法压缩使用的是通过矩阵对图片进行裁剪,也是通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样。
    4、RGB_565压缩:RGB_565压缩是通过改用内存占用更小的编码格式来达到压缩的效果。Android默认的颜色模式为ARGB_8888,这个颜色模式色彩最细腻,显示质量最高。一般不建议使用ARGB_4444,因为画质实在是辣鸡,如果对透明度没有要求,建议可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。
    5、createScaledBitmap:这里是将图片压缩成用户所期望的长度和宽度,但是这里要说,如果用户期望的长度和宽度和原图长度宽度相差太多的话,图片会很不清晰。

    以上就是5种图片压缩的方法,这里需要强调,他们的压缩仅仅只是对Android中的Bitmap来说的。如果将这些压缩后的Bitmap另存为sd中,他们的内存大小并不一样。

    二、RecyclerView缓存机制:
    四级缓存:
    Scrap:当前屏幕内的缓存数据
    Cache:刚刚移出屏幕的缓存数据,默认大小是2个,当其容量被充满同时又有新的数据添加的时候,会根据FIFO原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache里面的数据是干净的,也就是携带了原来的ViewHolder的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache是根据position来寻找数据的,这个postion是根据第一个或者最后一个可见的item的position以及用户操作行为(上拉还是下拉)。
    ViewCacheExtension:是google留给开发者自己来自定义缓存的,这个ViewCacheExtension我个人建议还是要慎用,没有找到具体的应用场景。
    RecycledViewPool:刚才说了Cache默认的缓存数量是2个,当Cache缓存满了以后会根据FIFO(先进先出)的规则把Cache先缓存进去的ViewHolder移出并缓存到RecycledViewPool中,RecycledViewPool默认的缓存数量是5个。RecycledViewPool与Cache相比不同的是,从Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的数据会被全部重置,相当于一个新的ViewHolder,而且Cache是根据position来获取ViewHolder,而RecycledViewPool是根据itemType获取的,如果没有重写getItemType()方法,itemType就是默认的。因为RecycledViewPool缓存的ViewHolder是全新的,所以取出来的时候需要走onBindViewHolder()方法。

    三、Handler机制
    由五部分组成:Handler、MessageQueue、Message、Looper、ThreadLocal。
    MessageQueue:消息队列,主要包含两个操作:enqueueMessage(插入)和next(读取并删除),他是通过一个单链表的数据结构来维护消息列表,在插入和删除上比较有优势。
    next方法是一个无限循环的方法,如果消息队列中没有消息,则方法处于阻塞状态。当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。
    Looper:消息循环,它会不停地从MessageQueue中查看是否有新消息,如果有就立马处理,否则就一直阻塞在那里。在构造方法中会创建一个MessageQueue,然后将当前线程对象保存起来,代码如下:
    private Looper(boolean quitAllowed){
    mQueue=new MessageQueue(quitAllowed);
    mThread=Thread.currentThread();
    }
    Handler工作需要Looper,创建Looper方式如下:
    Looper.prepare();
    Handler handler = new Handler();
    Looper.loop();
    Looper最重要的方法就是loop方法,只有调用了loop后,消息循环系统才会真正的作用起来。
    下面是Handler发构造方法:
    public Handler(Callback callback, boolean async) {

            ...// 仅贴出关键代码
    
            // 1. 指定Looper对象
                mLooper = Looper.myLooper();
                if (mLooper == null) {
                    throw new RuntimeException(
                        "Can't create handler inside thread that has not called Looper.prepare()");
                }
                // Looper.myLooper()作用:获取当前线程的Looper对象;若线程无Looper对象则抛出异常
                // 即 :若线程中无创建Looper对象,则也无法创建Handler对象
                // 故 若需在子线程中创建Handler对象,则需先创建Looper对象
                // 注:可通过Loop.getMainLooper()可以获得当前进程的主线程的Looper对象
    
            // 2. 绑定消息队列对象(MessageQueue)
                mQueue = mLooper.mQueue;
                // 获取该Looper对象中保存的消息队列对象(MessageQueue)
                // 至此,保证了handler对象 关联上 Looper对象中MessageQueue
    }
    

    由上述代码可知Handler的创建,必须建立在当前线程存在Looper的基础上才可以。
    loop方法是一个死循环,唯一跳出循环的方式是MessageQueue的next方法返回了null。当Looper的quit方法被调用时,Looper就会调用MessageQueue的quit或者quitSafely方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null。loop方法会调用MessageQueue的next方法来获取新消息,而next是一个阻塞操作,当没有消息时,next方法会一直阻塞在那里,这也就导致loop方法一直阻塞在那里。如果MessageQueue的next方法返回了新消息,Looper就会处理这条消息。
    msg.target.dispatchMessage(msg),这里的msg.target是发送这条消息的Handler对象,这样Handler发送的消息最终又交给它的dispatchMessage方法来处理了。

    四、ThreadLocal
    ThreadLocal创建有下面三种方式:
    // 1. 直接创建对象
    private ThreadLocal myThreadLocal = new ThreadLocal()

    // 2. 创建泛型对象
    private ThreadLocal myThreadLocal = new ThreadLocal<String>();

    // 3. 创建泛型对象 & 初始化值
    // 指定泛型的好处:不需要每次对使用get()方法返回的值作强制类型转换
    private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
    return "This is the initial value";
    }
    };

    // 特别注意:
    // 1. ThreadLocal实例 = 类中的private、static字段
    // 2. 只需实例化对象一次 & 不需知道它是被哪个线程实例化
    // 3. 每个线程都保持 对其线程局部变量副本 的隐式引用
    // 4. 线程消失后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)
    // 5. 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程只能访问到自己通过调用ThreadLocal的set()设置的值
    // 即 哪怕2个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。

    访问ThreadLocal变量:
    // 1. 设置值:set()
    // 需要传入一个Object类型的参数
    myThreadLocal.set("初始值”);

    // 2. 读取ThreadLocal变量中的值:get()
    // 返回一个Object对象
    String threadLocalValue = (String) myThreadLocal.get();

    核心原理:
    ThreadLocal类中有一个Map(称:ThreadLocalMap):用于存储每个线程&该线程设置的存储在ThreadLocal变量的值。
    1、ThreadLocalMap的key = 当前ThreadLocal的实例、值value = 该线程设置的存储在ThreadLocal变量的值。
    2、该key是ThreadLocal对象的弱引用;当要抛弃掉ThreadLocal对象时,垃圾收集器会忽略该key的引用而清理掉ThreadLocal对象。

    关于如何设置ThreadLocal的值请看原码:
    // ThreadLocal的源码

    public class ThreadLocal<T> {

    ...
    

    /**
    * 设置ThreadLocal变量引用的值
    * ThreadLocal变量引用 指向 ThreadLocalMap对象,即设置ThreadLocalMap的值 = 该线程设置的存储在ThreadLocal变量的值
    * ThreadLocalMap的键Key = 当前ThreadLocal实例
    * ThreadLocalMap的值 = 该线程设置的存储在ThreadLocal变量的值
    **/
    public void set(T value) {

        // 1. 获得当前线程
        Thread t = Thread.currentThread();
    
        // 2. 获取该线程的ThreadLocalMap对象 ->>分析1
        ThreadLocalMap map = getMap(t);
    
        // 3. 若该线程的ThreadLocalMap对象已存在,则替换该Map里的值;否则创建1个ThreadLocalMap对象
        if (map != null)
            map.set(this, value);// 替换
        else
            createMap(t, value);// 创建->>分析2
    }
    

    /**
    * 获取ThreadLocal变量里的值
    * 由于ThreadLocal变量引用 指向 ThreadLocalMap对象,即获取ThreadLocalMap对象的值 = 该线程设置的存储在ThreadLocal变量的值
    **/
    public T get() {

        // 1. 获得当前线程
        Thread t = Thread.currentThread();
    
        // 2. 获取该线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    
        // 3. 若该线程的ThreadLocalMap对象已存在,则直接获取该Map里的值;否则则通过初始化函数创建1个ThreadLocalMap对象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value; // 直接获取值
        }
        return setInitialValue(); // 初始化
    }
    

    /**
    * 初始化ThreadLocal的值
    **/
    private T setInitialValue() {

        T value = initialValue();
    
        // 1. 获得当前线程
        Thread t = Thread.currentThread();
    
        // 2. 获取该线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    
         // 3. 若该线程的ThreadLocalMap对象已存在,则直接替换该值;否则则创建
        if (map != null)
            map.set(this, value); // 替换
        else
            createMap(t, value); // 创建->>分析2
        return value;
    }
    

    /**
    * 分析1:获取当前线程的threadLocals变量引用
    **/
    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    /**
    * 分析2:创建当前线程的ThreadLocalMap对象
    **/
    void createMap(Thread t, T firstValue) {
    // 新创建1个ThreadLocalMap对象 放入到 Thread类的threadLocals变量引用中:
    // a. ThreadLocalMap的键Key = 当前ThreadLocal实例
    // b. ThreadLocalMap的值 = 该线程设置的存储在ThreadLocal变量的值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    // 即 threadLocals变量 属于 Thread类中 ->> 分析3
    }

    ...
    

    }

    /**
    * 分析3:Thread类 源码分析
    **/

    public class Thread implements Runnable {
       ...
    
       ThreadLocal.ThreadLocalMap threadLocals = null;
       // 即 Thread类持有threadLocals变量
       // 线程类实例化后,每个线程对象拥有独立的threadLocals变量变量
       // threadLocals变量在 ThreadLocal对象中 通过set() 或 get()进行操作
    
       ...
    

    }

    五、HashMap原理:
    HashMap 采用的数据结构 = 数组(主) + 单链表(副)
    数组下标 = key键的哈希值。
    数组元素 = 一个键值对(Entry) = 一个链表的头结点。

    相关文章

      网友评论

          本文标题:Android面试题

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