Android面试题

·  阅读 4308

计算机基础


原码反码补码

1.正数的原码反码补码相同,都是将数字转换为二进制形式,然后将高位补0。比如说对于8位来说:

  • 10所对应的原码反码补码都是 0000 1010

2.而对于负数,负数的原码是它的绝对值对应的二进制,而最高位是1。所以:

  • 10所对应的原码是 1000 1010

3.负数的反码是它原码最高位除外的其余所有位取反,所以:

  • 10所对应的反码是 1111 0101

4.而负数的补码是将其反码的数值+1,所以:

  • 10所对应的补码是 1111 0110

JAVA的8种基本数据类型所占位数

左移和右移

java中的移位运算符有三种:

  • 左移<< 左移就是将左边的操作数在内存中的二进制数据左移指定的位数,左边移空的部分补零。num << n, 相当于 num 乘以2的 n 次方

  • 右移>> 右移:右边超出截掉,左边补上符号位 (负的就填1 正的就填0)。num >> n, 相当于 num 除以2的 n 次方

  • 无符号右移>>> 无符号右移无论最高位是什么,空位都补零

三次握手

主要为了确认服务端和客户端接受和发送数据的能力是正常的。

  • 第一次:客户端向服务端发送数据
  • 第二次:服务端接收到数据后,向客户端发送数据。这两个证明:客户端发送能力,服务端发送和接收能力是好的
  • 第三次:客户端向服务端发送数据,告诉服务端接收到了数据,表明客户端接收能力是好的

网络相关


http协议和tcp协议区别

  • TCP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
  • Http协议是建立在TCP协议基础之上的,只有TCP先建立起一个到服务器的连接通道后,才能通过http协议发送数据

http连接和socket连接区别

  • Http和Socket都是基于TCP协议进行通信
  • Http每次请求结束后,连接就关闭。而Socket使用TCP协议的时候,只要网络没问题,我们不通过代码把连接关闭,这个连接就会一直存在,服务器还可以主动向客户端推消息

# 子网掩码与ip地址的关系

寻找IP地址,可以用快递寻找门牌号码类比

每栋楼有楼号,比如4号楼,5号楼等
每栋楼有门牌号,比如101号房,102号房等

IP地址同样有两部分地址组成,分别是"网络地址(网络号)"和“主机地址(主机号)”。我们怎么知道哪部分是楼号?哪部分是房间号呢?怎样划分呢?

答案是通过——Netmask 子网掩码 网络地址就是:把IP地址转成二进制和子网掩码进行与运算,结果就是网络地址

ConnectionPool连接池

用来管理 HTTP 和 HTTP/2 连接的重用,以减少网络延迟。在okhttp中,客户端与服务端的连接被抽象为一个个的Connection,实现类是RealConnection。而ConnectionPool就是专门用来管理Connection的类

一些共享一个地址(Address)的HTTP requests可能也会共享一个Connection。ConnectionPool设置这样的策略:让一些connections保持打开状态,以备将来使用。

  • maxIdleConnections 每个地址闲置的connections 的最大数量
  • keepAliveDurationNs 每个空闲连接的存活时间的纳秒数
  • connections : connection缓存池。Deque是一个双端列表,支持在头尾插入元素,这里用作LIFO(后进先出)堆栈,多用于缓存数据

Java基础


try,catch

  • 可以有try,没有catch,有finally就行了。但是出了异常系统会直接抛出,闪退

图片.png

  • try里或者catch代码块里有return,finally最终也会执行

什么情况下Java程序会产生死锁?如何定位、修复?

两个或多个线程之间,由于互相有对方需要的锁,而永久处于阻塞状态,造成死锁

死锁的四个条件
1.一个资源每次只能被一个线程使用
2.某个线程获得了资源,未使用完之前,不能强行剥夺
3.该线程因请求其他资源时发生阻塞,但对已获得的资源保持不放
4.若干线程之间形成了循环等待资源关系。你等我的资源,我等你的资源 图片.png

//死锁代码
public class DeadLock {
    public static void main(String[] args) {
        Object lockA = new Object();
        Object lockB = new Object();
 
        Thread t1 = new Thread(() -> {
            // 1.占有一把锁
            synchronized (lockA) {
                System.out.println("线程1获得锁A");
                // 休眠1s(让线程2有时间先占有锁B)
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取线程2的锁B
                synchronized (lockB) {
                    System.out.println("线程1获得锁B");
                }
            }
        });
        t1.start();
 
        Thread t2 = new Thread(() -> {
            // 占B锁
            synchronized (lockB) {
                System.out.println("线程2获得锁B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取线程1的锁A
                synchronized (lockA) {
                    System.out.println("线程2获得了锁A");
                }
            }
        });
        t2.start();
    }
}
复制代码

如何解决死锁问题 降低锁的颗粒度,尽量避免多个锁之间嵌套

public class UnDeadLock2 {
    public static void main(String[] args) {
        Object lockA = new Object();
        Object lockB = new Object();
 
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1得到锁A");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("线程1得到锁B");
                    System.out.println("线程1释放锁B");
                }
                System.out.println("线程1释放锁A");
            }
        }, "线程1");
        t1.start();
 
        Thread t2 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程2得到锁A");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("线程2得到锁B");
                    System.out.println("线程2释放锁B");
                }
                System.out.println("线程2释放锁A");
            }
        }, "线程2");
        t2.start();
    }
}
复制代码

使用Trace抓取日志解决死锁

sleep 和 wait 的区别

  • sleep 方法是 Thread 类中的静态方法,wait 是 Object 类中的方法
  • sleep 并不会释放同步锁,而 wait 会释放同步锁
  • sleep 可以在任何地方使用,wait()方法和notify()方法在使用时都有一个前提条件,必须都要获取当前对象的锁。也就是说如果wait()方法和notify()方法在使用时没有获取到锁时,程序就会直接抛出异常
  • sleep 中必须传入时间,而 wait 可以传,也可以不传,不传时间的话只有 notify 或者 notifyAll 才能唤醒,传时间的话在时间之后会自动唤醒
  • wait()方法在执行完成后,会立刻释放对象的锁,这时其它线程依然可以执行wait()方法所在的synchronized同步方法。而notify()方法在执行完成后不会立即释放对象的锁,直到这个线程的synchronized同步方法执行完时才会释放锁
  • wait()方法的本质是将当前线程添加到等待队列中。notify()方法的本质是将等待队列中某一个线程使它退出等待队列。但如果等待队列中的线程有很多,notify()方法也只是随机抽取一个线程让它退出等待队列
  • wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对 象调用的wait方法后的线程。
  • wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

线程的生命周期

在线程的生命周期中:
它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。

  • 当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态
  • 当线程对象调用了Thread.start()方法之后,该线程处于就绪状态(至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)
  • 如果OS调度选中了,就会进入到运行状态
  • 线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。比如执行了wait()或者sleep()方法
  • 线程会以以下三种方式之一结束,结束后就处于死亡状态:
    • run()方法执行完成,线程正常结束。
    • 线程抛出一个未捕获的Exception或Error。
    • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

Exception 和 Error的区别

  • Exception 和 Error 都继承于 Throwable
  • Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。Error 是指在正常情况下,不大可能出现的情况,所以不便于也不需要捕获

HashMap实现原理,如果hashCode冲突怎么办,为什么线程不安全,与Hashtable有什么区别

  • 主要通过计算数据的hash值来插入,hash值相同的元素插入同一个链表,采用数组+链表方式存储(插入到链表时才用头插法,后插入的Entry被查找的可能性更大)。当链表长度超过阈值(默认是8)的时候会触发树化,链表会变成树形结构
  • 如果数组长度达到阈值,会调用 resize 方法扩展容量。将原数组扩展为原来的 2 倍,重新计算 index 索引值,将原节点重新放到新的数组中。这一步可以将原先冲突的节点分散到新的桶中。
  • 可能会有多个线程同时put数据,若同时push了hashCode相同数据,后面的数据可能会将上一条数据覆盖掉 Hashtable几乎在每个方法上都加上synchronized(同步锁),实现线程安全
  • HashMap线程不安全,并发插入时可能会出现环链表,让下一次读操作出现死循环
  • ConcurrentHashMap是线程安全的,使用分段锁。当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。锁住链表,不同链表进行put时不加锁,而Hashtable不是
  • 什么是HashMap
  • 高并发下的HashMap
  • 什么是ConcurrentHashMap
HashMap的初始长度

从Key映射到HashMap数组的对应位置,会用到一个Hash函数:index = HashCode(Key) & (Length - 1).HashMap的初始长度为16或者其他2的幂,Hash算法的结果就是均匀的

synchronized 修饰实例方法和修饰静态方法有什么不一样

public synchronized void run()  {
    System.out.println(1);
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    System.out.println(2);
}
复制代码
  • synchronized修饰普通方法时锁对象是this对象,而使用两个对象去访问,不是同一把锁
Demo demo = new Demo();
new Thread(() -> demo.run()).start();
Demo demo2 = new Demo();
new Thread(() -> demo2.run()).start();
复制代码

结果为: 1 1 2 2

不同步。但如果使用同一对象访问,结果才是同步的

Demo demo = new Demo();
new Thread(() -> demo.run()).start();
new Thread(() -> demo.run()).start();
复制代码

输出结果:1 2 1 2

  • 当synchronized修饰静态方法时,锁对象为当前类的字节码文件对象。使用不同的对象访问,结果是同步的,因为当修饰静态方法时,锁对象是class字节码文件对象,而两个对象是同一个class文件,所以使用的是一个锁

final 、finally、finalize 区别

  1. final关键字用于基本数据类型前:这时表明该关键字修饰的变量是一个常量,在定义后该变量的值就不能被修改。
    final关键字用于方法声明前:这时意味着该方法时最终方法,只能被调用,不能被覆盖,但是可以被重载。
    final关键字用于类名前:此时该类被称为最终类,该类不能被其他类继承。

  2. 用try{ }catch(){} 捕获异常时,无论室友有异常,finally代码块中代码都会执行。

  3. finalize方法来自于java.lang.Object,用于回收资源。 可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用

Java中成静态内部类和非静态内部类特点

  • 静态内部类:和外部类没有什么"强依赖"上的关系,耦合程度不高,可以单独创建实例。由于静态内部类与外部类并不会保存相互之间的引用,因此在一定程度上,还会节省那么一点内存资源
  • 内部类中需要访问有关外部类的所有属性及方法

强引用、弱引用、软引用和虚引用

  • 强引用:当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误也不会回收,直接new出来的就是强引用
  • 软引用:内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。
    当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收
  if(JVM内存不足) {
        // 将软引用中的对象引用置为null
        str = null;
        // 通知垃圾回收器进行回收
        System.gc();
   }
复制代码
  • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象
  • 虚引用:虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收

原子变量Atomic

synchronized、volatile、Atomic区别和用法

多线程并发问题

多线程并发问题

volidate

  • 单例的双重判空原因
    • 1.第一个判空:提高效率,不需要每次都执行synchronized代码块
    • 2.第二个判空:防止new多个实例
  • volidate作用
    • 保证可见性。不过多个线程同时对volidate修饰的变量进行写操作时,volidate也不能保证线程安全(比如多个线程同时操作volidate修饰的a,让a++,最后的数字可能与预期的不一样,假如此时a=1,a++两次后刷新成2,因为a++其实是多个操作的 )
    • 保证有序性,防止实例初始化时指令重排,new操作时只完成一半就有别的线程拿到实例导致实例的成员变量值有可能不对
    • 不保证原子性。可以用原子变量来保证原子性

线程的sleep方法

  • 不会释放锁 通过synchronized同步块实现锁机制。线程tv1睡眠三秒后,线程tv2里的run()方法代码才执行,因为它获取不到锁发生了阻塞
        var t1: Thread = object : Thread() {
        override fun run() {
              synchronized(any){
                  println("线程一开始睡眠")
                  Thread.sleep(3000)
                  println("线程一睡眠结束")
               }
            }
        }
        var t2: Thread = object : Thread() {
            override fun run() {
               synchronized(any){
                   println("线程2开始睡眠")
                   Thread.sleep(3000)
                   println("线程2睡眠结束")
               }
            }
        }
        t1.start()
        t2.start()
        
        //日志输出
        14:13:24.660 1805-1836/ I/System.out: 线程一开始睡眠
        14:13:27.663 1805-1836/ I/System.out: 线程一睡眠结束
        14:13:29.555 1805-1837/ I/System.out: 线程2开始睡眠
        14:13:32.556 1805-1837/ I/System.out: 线程2睡眠结束
    复制代码
  • 线程在sleep时是可以被中断的
    线程1在sleep时候被2中断
        var t1: Thread = object : Thread() {
            override fun run() {
                println("线程一开始睡眠")
                try {
                    Thread.sleep(30000)
                } catch (e: InterruptedException) {
                    println("===error=== ")
                }
                println("线程一睡眠结束")
            }
        }
        var t2: Thread = object : Thread() {
            override fun run() {
                Thread.sleep(3000)
                t1.interrupt()
            }
        }
        t1.start()
        t2.start()
    复制代码
  • sleep(0)
    • 在线程没退出之前,线程有三个状态,就绪态,运行态,等待态
    • sleep(n)之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,当n秒后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争
    • 所谓的cpu调度,就是根据一定的算法(优先级,FIFO等。。。),从就绪队列中选择一个线程来分配cpu时间。
    • sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争

主线程是否可以直接捕获子线程的异常?

try{
    new Thread(){
        public void run(){
            if (...) throw new RuntimeException(); 
        }
    }.start();
}catch(Exception e){
}
复制代码

答案不能。

  • 线程代码不能抛出任何checked异常。所有的线程中的checked异常都只能被线程本身消化掉
  • 子线程代码抛出运行级别异常之后,线程会中断。主线程不受这个影响,不会处理这个RuntimeException,而且根本不能catch到这个异常。会继续执行自己的代码

线程池

线程池解析

线程池是如何重复利用空闲的线程来执行任务的?:没有任务时核心线程会阻塞,不会关闭

TreeMap和HashMap区别

  • TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的;TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)。
  • HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;数据结构主要是桶(数组),链表或红黑树。适用于在Map中插入、删除和定位元素
  • 如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap

synchronized和ReentrantReadWriteLock区别

  • synchronized 内部锁与 ReentrantLock 锁都是独占锁(排它锁), 同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低。 ReentrantReadWriteLock 读写锁 是一种改进的排他锁,也可以称作 共享/排他锁。ReentrantLock的读取锁是共享的。有线程写操作时,其他线程不可写,不可读;该线程读操作时,其他线程不可写,但可读。
  • ReentrantLock可以实现公平锁机制,synchronized是非公平锁机制
  • synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象;Lock发生异常,若没有主动释放,极有可能造成死锁,故需要在finally中用unLock()方法释放锁

注解

根据运行机制分类:

  • 源码注解(RetentionPolicy.SOURCE):注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。比如Override
  • 编译时注解(RetentionPolicy.CLASS):注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。比如bufferkrnif
  • 运行时注解(RetentionPolicy.RUNTIME):注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。比如EventBus,自定义的代替findViewById注解

根据获取注解对象分为运行时注解和编译时注解

  • 运行时注解:通过反射机制获取注解对象
  • 编译时注解:通过APT方式获取注解对象

通过反射来获取注解信息会对性能造成影响,而编译时注解就不一样了。编译时注解,是在 java 编译生成 .class 文件这一步进行的操作,性能问题也就无从说起了


Kotlin


进程,线程,协程

Coroutine 协程

  • 进程:打开一个软件,就是开启了一个进程。有独立的内存空间。
  • 线程:是操作系统进行运算的最小单位,拥有独立的栈。它被包含在进程中。一个进程可以拥有多个线程

线程之间可以共享资源,进程之间不可以

  • 协程:正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程

协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换,协程的挂起不会阻塞线程。因此,协程之间的切换不会像线程切换那样消耗资源。

Java怎么调用Kotlin中的扩展函数

借助AndroidStudio看看这个扩展函数被编译成什么样的java代码(Tools > Kotlin > Show Kotlin Bytecode),就知道怎么调用了

图片.png 调用

图片.png

创建协程launch和runBlocking区别

runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会

     GlobalScope.launch {
            delay(1000) //不会阻塞
            println("111111111111111")
        }
        Thread.sleep(200)
        println("2222222222")


        runBlocking {
            delay(1000) //阻塞
            println("33333333")
        }
        Thread.sleep(200)
        println("44444444444")
     
    输出结果
    2020-12-11 18:09:41.178 29211-29211/ I/System.out: 2222222222
    2020-12-11 18:09:41.980 29211-29279/ I/System.out: 111111111111111
    2020-12-11 18:09:42.181 29211-29211/ I/System.out: 33333333
    2020-12-11 18:09:42.383 29211-29211/ I/System.out: 44444444444
复制代码

async()和launch()都可以获取返回值,有什么不同

假如一个页面需要请求两个接口,用两个接口返回的数据才能渲染出页面。launch()是串行的写法。耗时

mainScope.launch {
    //获取token
    val token = getToken()
    //通过token,获取userInfo
    val userInfo = getUserInfo(token)
    //登录成功
    Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
---------------------------------------
suspend fun getUserInfo(token: String): UserInfo {
    return withContext(Dispatchers.Default) {
        Logger.i("get userInfo, token: $token")
        val userInfo = api.getUserInfo(token)
        userInfo
    }
}
suspend fun getToken(): String {
    return withContext(Dispatchers.Default) {
        Logger.i("get token")
        val token = api.getToken()
        token
    }
复制代码

用async()可以同时发出两个请求,效率更高

suspend关键字

用于修饰函数,提醒调用者此函数要放在一个协程里调用。一般耗时方法会用到这个关键字


Android基础知识


Activity屏幕旋转,为什么viewModel数据不丢失

图片.png 以上是ViewModel的存储路线图,以键值对存储在HashMap数据模型中,而这个Map数据模型存储在ViewModelStore内,而ComponentActivity持有ViewModelStore引用,这个ComponentActivity就是我们编写Activity时会继承。

getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            if (!isChangingConfigurations()) {
                getViewModelStore()返回的是ViewModelStore
                getViewModelStore().clear();
            }
        }
    }
});
复制代码

Activity销毁时,执行onDestroy会自动调用ViewModelStore的clear方法清除掉ViewModel,这也就是我们不用担心ViewModel生命周期的问题,因为Activity会自动帮我们处理掉; 横竖屏切换,onDestroy也执行了,ViewModel和ViewModelStore却沿用了切换之前的实例

####Activity四种启动模式

Bitmap的压缩方式

  • 尺寸压缩。尺寸压缩会改变图片的尺寸,即压缩图片宽度和高度的像素点。先预读bitmap的宽高,然后计算出合适的采样率inSampleSize进行压缩,当采样率为2时,即宽、高均为原来的1/2,像素则为原来的1/4.其占有内存也为原来的1/4。当设置的采样率小于1时,其效果与1一样。当设置的inSampleSize大于1,不为2的指数时,系统会向下取一个最接近2的指数的值
  • 质量压缩。它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。有个参数:quality,可以调节你压缩的比例,质量压缩对png格式这种图片没有作用,因为png是无损压缩。quality值与最后生成的图片的大小并不是线性关系,比如大小为 300k的图片,当quality为90时,得到的图片大小并不是为270K

Android中的动画有哪几类,它们的特点和区别是什么

  • 帧动画:通过xml配置一组图片,动态播放
  • 补间动画是父容器不断的绘制 view,看起来像移动了效果,其实 view 没有变化,还在原地
  • 属性动画是通过不断改变 view 内部的属性值,真正的改变 view

Android轻量级数据SparseArray详解

  • key是用int[] mKeys数组存储,value是用Object[] mValues数组存储。基于二分查找,因此查找的时间复杂度为O(LogN)

  • 由于SparseArray中Key存储的是数组形式,因此可以直接以int作为Key。避免了HashMap的装箱拆箱操作,性能更高且int的存储开销远远小于Integer;

  • 采用了延迟删除的机制,当一个键值对被remove后,会在对应key的value下放置成员变量DELETED,同时将成员变量mGarbage设为true

    public class SparseArray<E> implements Cloneable {
       private static final Object DELETED = new Object();
       ......
    复制代码
  • 不管删除还是添加,都需要先查找key,如果没找到就返回~low(二叉查找的左边界取反)。这样通过看返回值,如果大于0说明找到了,如果小于0说明没找到,直接取绝对值后就是应该插入的位置

  • 插入的时候,如果该位置有无效元素,就不需要扩容,否则如果mGarbage为true,就先移除无效元素后再插入

  • SparseArray 内部是通过二分查找来寻址的,效率很明显要低于 HashMap 的常数级别的时间复杂度。SparseArray 所占空间优于 HashMap,而效率低于 HashMap,是典型的时间换空间,适合较小容量的存储。所以在数据量大的情况下性能并不明显,将降低至少50%。满足下面两个条件我们可以使用SparseArray代替HashMap:

    • 数据量不大,最好在千级以内
    • key必须为int类型,这中情况下的HashMap可以用SparseArray代替
  • remove() SparseArray 的 remove() 方法并不是直接删除之后再压缩数组,而是将要删除的 value 设置为 DELETE 这个 SparseArray 的静态属性,这个 DELETE 其实就是一个 Object 对象,同时会将 SparseArray 中的 mGarbage 这个属性设置为 true,这个属性是便于在合适的时候调用自身的 gc()方法压缩数组来避免浪费空间。这样可以提高效率,如果将来要添加的key等于删除的key,那么会将要添加的 value 覆盖 DELETE。

  • gc()。 SparseArray 中的 gc() 方法跟 JVM 的 GC 其实完全没有任何关系。``gc()` 方法的内部实际上就是一个for循环,将 value 不为 DELETE 的键值对往前移动覆盖value 为DELETE的键值对来实现数组的压缩,同时将 mGarbage 置为 false,避免内存的浪费。

  • put()。 put 方法是这么一个逻辑,如果通过二分查找 在 mKeys 数组中找到了 key,那么直接覆盖 value 即可。如果没有找到,会拿到与数组中与要添加的 key 最接近的 key 索引,如果这个索引对应的 value 为 DELETE,则直接把新的 value 覆盖 DELET 即可,在这里可以避免数组元素的移动,从而提高了效率。如果 value 不为 DELETE,会判断 mGarbage,如果为 true,则会调用 gc()方法压缩数组,之后会找到合适的索引,将索引之后的键值对后移,插入新的键值对,这个过程中可能会触发数组的扩容。

布局优化

  • 为什么多了层级,性能可能会差很多? 因为布局层级的增加,可能会导致测量时间呈指数级增长。ViewGroup可能会对子View进行多次测量
  • include。用于重复的布局,写成一个xml文件,多处include
  • merge。配合include使用,用于消除重复的根布局
  • ViewSub。延迟加载,当调用inflate()或setVisibility()后会被remove掉,然后在将其中的layout加到当前view hierarchy中。ViewStub的inflate只能被调用一次,第二次调用会抛出异常

Looper总结

图片.png

  • Looper通过prepare方法进行实例化,先从他的成员变量sThreadLocal中拿取,没有的话就new 一个Looper,然后放到sThreadLocal中缓存。每个线程只能创建一个Looper实例。实例化时会创建一个MessageQueue,后面handle发消息时候就发到looper的MessageQueue中
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}
复制代码
  • Looper通过loop方法开启循环队列,里面开启了死循环,没有msg时候会阻塞
  • 在ActivityThread的main方法中也就是Activity启动的时候,已经调用了Looper.prepareMainLopper()方法
  • 在子线程中直接new Handle()来用就不行了,因为没有事先调用Looper.prepare(),所以这里拿到的是null

图片.png

ThreadLocal在Looper中的使用

ThreadLocal的奇思妙想 为了解决多个线程访问同一个数据问题,同步锁的思路是线程不能同时访问一片内存区域.而ThreadLocal的思路是,干脆给每个线程Copy一份一摸一样的对象,线程之间各自玩自己的,互相不影响对方 常见ThreadLocal应用场景:确保在每一个线程中只有一个Looper的实例对象

  • ThreadLocal的set方法
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}
复制代码
  • ThreadLocal的get方法
 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();
 }
复制代码

简而言之:先拿到当前线程,再从当前线程中拿到ThreadLocalMap,通过ThreadLocalMap来存储数据。(ThreadLocalMap是Thread的成员变量)

  • handler postDelay这个延迟是怎么实现的:handler.postDelay直接将消息插入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。
  • 每一个线程中都有一个ThreadLocalMap类型的threadLocals变量,这个map中key为ThreadLocal的实例引用,value为对应的本地变量。如果这个线程不消亡,开发者也没有采用remove操作及时清除掉不再使用的变量,这些变量就会一直存在map中,直到撑爆你的内存,造成内存溢出问题

IdleHandler

Q:IdleHandler 有什么用?

  1. IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机;
  2. 当 MessageQueue 当前没有立即需要处理的消息时,会执行 IdleHandler;

Q:MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成对使用?

  1. 不是必须;
  2. IdleHandler.queueIdle() 的返回值,可以移除加入 MessageQueue 的 IdleHandler;

Q:当 mIdleHanders 一直不为空时,为什么不会进入死循环?

  1. 只有在 pendingIdleHandlerCount 为 -1 时,才会尝试执行 mIdleHander;
  2. pendingIdlehanderCount 在 next() 中初始时为 -1,执行一遍后被置为 0,所以不会重复执行;

Q:是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?

  1. 不建议;
  2. IdleHandler 的处理时机不可控,如果 MessageQueue 一直有待处理的消息,那么 IdleHander 的执行时机会很靠后;

Q:IdleHandler 的 queueIdle() 运行在那个线程?

  1. 陷进问题,queueIdle() 运行的线程,只和当前 MessageQueue 的 Looper 所在的线程有关;
  2. 子线程一样可以构造 Looper,并添加 IdleHandler;

Service 和 IntentService

Activity对事件响应不超过5秒,BroadcastReceiver执行不超过10秒,Service耗时操作为20秒。否则系统会报ANR

  • 使用startService()方法启用服务后,调用者与服务之间没有关连。调用者直接退出而没有调用stopService的话,Service会一直在后台运行
  • 使用bindService()方法启用服务,调用者与服务绑定在一起了,调用者一旦退出,服务也就自动终止
  • IntentService是Service的子类,会创建子线程来处理所有的Intent请求,其onHandleIntent()方法实现的代码,无需处理多线程问题

FragmentPageAdapter和FragmentPageStateAdapter的区别

  • FragmentPageAdapter在每次切换页面的的时候,没有完全销毁Fragment,适用于固定的,少量的Fragment情况。默认notifyDataSetChanged()刷新是无效的

  • FragmentPageStateAdapter在每次切换页面的时候,是将Fragment进行回收,适合页面较多的Fragment使用,这样就不会消耗更多的内存

Sqlite数据库,什么是事务

事务是由一个或多个sql语句组成的一个整体,如果所有语句执行成功那么修改将会全部生效,如果一条sql语句将销量+1,下一条再+1,倘若第二条失败,那么销量将撤销第一条sql语句的+1操作,只有在该事务中所有的语句都执行成功才会将修改加入数据库中
sqlite数据库相关操作,主要包括创建和增删改查,事务

怎么做Sqlite数据库升级

  1. 直接删除老数据库,但会造成数据丢失,一般不采用
  2. 对数据库进行升级,参考SQLite数据库版本升级

invalidate与requestLayout区别

  • view调用invalidate将导致当前view的重绘,viewGroup调用invalidate会使viewGroup的子view调用draw
  • requestLayout方法只会导致当前view的measure和layout,而draw不一定被执行。只有当view的位置发生改变才会执行draw方法

View和ViewGroup区别

  • ViewGrouponInterceptTouchEvent默认返回false,即不拦截事件,View没有拦截事件方法,View默认时消耗事件的
  • ViewGroup默认不会调用onDraw方法,View默认会调用onDraw方法。可以通过setWillNotDraw(boolean willNotDraw)来指定是否调用onDraw方法
    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
复制代码

android版本新特性

  • 5.0
    • 引入Material Design主题
  • 6.0
    • 运行时权限
  • 7.0
    • 文件读写权限适配FileProvider
    • 移除了对 Apache HTTP 客户端的支持,建议使用 HttpURLConnection 代替。继续使用 Apache HTTP API,必须先在 build.gradle 文件中配置:
        android {
            useLibrary 'org.apache.http.legacy'
        }
    复制代码
  • 8.0
  • 9.0
  • 10.0

Android中一张图片占据的内存大小是如何计算

参考Android中一张图片占据的内存大小是如何计算

  • 图片来源是 res 内的不同资源目录时,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,做一次分辨率转换,规则如下:新分辨率 = 原图横向分辨率 * (设备的 dpi / 目录对应的 dpi ) * 原图纵向分辨率 * (设备的 dpi / 目录对应的 dpi )
  • 其他图片的来源,如磁盘,文件,流等,均按照原图的分辨率来进行计算图片的内存大小
  • 一张图片占用的内存大小的计算公式:分辨率 * 像素点大小;但分辨率不一定是原图的分辨率,需要结合一些场景来讨论,像素点大小就几种情况:ARGB_8888(4B)、RGB_565(2B) 等等

APP启动速度优化

  • 用adb命令可以检测启动时间,示例如下:
adb shell am start -W [packageName]/[.MainActivity]
./adb shell am start -W "com.hchstudio.dict"/".MainActivity"
复制代码

WaitTime为我们所关注的启动时间

  • app的启动流程,主要需要减少Application和启动界面的onCreate方法
  • 的app首页主题样式加上android:windowBackground,放一下app的背景图片,这样即使app启动慢,也会首先加载背景,这样就会给用户造成一种假象,认为是app已经启动
<!--AppTheme.Launcher为启动界面的主题样式-->
<style name="AppTheme.Launcher">
    <item name="android:windowBackground">@color/colorLauncher</item>
</style>
复制代码

APP体积优化

  • 使用混淆
  • 不复杂图片使用svg代替png,利用着色器实现换肤
  • 在使用了 SO 库的时候优先保留 v7 版本的 SO 库,删掉其他版本的SO库。原因是在 2018 年,v7 版本的 SO 库可以满足市面上绝大多数的要求.最多再加个64位的so库

内存抖动

内存抖动是由于短时间内有大量对象进出新生区导致的,它伴随着频繁的GC,gc会大量占用ui线程和cpu资源,会导致app整体卡顿。

避免发生内存抖动的几点建议:

  • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
  • 当需要大量使用Bitmap的时候,试着把它们缓存在数组或容器中实现复用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

Android中ClassLoader的种类&特点:

  • BootClassLoader(Java的BootStrap ClassLoader): 用于加载Android Framework层class文件。
  • PathClassLoader(Java的App ClassLoader): 只能加载已经安装过的apk的dex文件
  • DexClassLoader(Java的Custom ClassLoader): 可以从一个jar包或者未安装的apk中加载dex文件
  • BaseDexClassLoader: 是PathClassLoader和DexClassLoader的父类。

SharePreference为什么不能存储较大value

  • 获取数据时会卡主线程(比如getString()方法中会调用wait()方法)
    public String getString(String key, @Nullable String defValue) {
    	synchronized (this) {
        		awaitLoadedLocked();
        		String v = (String)mMap.get(key);
        		return v != null ? v : defValue;
    	}
    }
    
    private void awaitLoadedLocked() {
    	while (!mLoaded) {
        	try {
            	wait();
        	} catch (InterruptedException unused) {
    	}
    }
    复制代码
  • SharePreference存值的时候,内部会有一个静态的map保存了你所有的key和value
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    	if (sSharedPrefsCache == null) {
        		sSharedPrefsCache = new ArrayMap<>();
    	}
    
    	final String packageName = getPackageName();
    	ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    	if (packagePrefs == null) {
        		packagePrefs = new ArrayMap<>();
        		sSharedPrefsCache.put(packageName, packagePrefs);
    	}
    
    	return packagePrefs;
    }
    复制代码

实现View滑动的几个办法

  • View自身提供的scrollTo()和scrollBy方法。但只适合对View内容的滑动
  • 使用动画。但滑动后的View点击没有效果,所以适用于没有交互的View
  • 改变布局参数,比如layoutParams.left。比动画稍微复杂,适合有交互的View

Scroller使用。调用startScroll方法,然后invidate() --> View会调用onDraw(),里面会调用computeScroll(),此方法默认空实现,需要自行实现 --> 重写computeScroll(),实现滑动,如果没有结束,postInvalidate()重绘

事件分发

  • 源码
// 1. Activity的dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //交给PhoneWindow处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}

// 2. PhoneWindow的superDispatchTouchEvent()方法
public boolean superDispatchTouchEvent(MotionEvent event) {
		//交给DecorView处理
        return mDecor.superDispatchTouchEvent(event);
}
// 3. 由于DecorView是FrameLayout子类,所以事件会被传递到DecorView的子View也就是,setContentView设置的View中
复制代码
  • View的事件分发

View的测量

View的展示过程

  • 先创建一个Window
  • 创建DecorView
  • 通过 WindowManager.addView()方法将Window展示到屏幕上,该方法中会创建会为每个 Window 创建 ViewRootImpl 对象用于连接 WindowManager 和 DecorView ,并且所有 Window 想要对 View 的进行的操作都是通过 ViewRootImpl 来完成的
  • 调用 DecorView 的measure 方法,measure 方法中又会调用 onMeasure 方法将测量过程传递到子 View。调用 layout 方法,layout 方法中会调用 onLayout 方法将布局过程传递到子 View。调用 darw 方法,draw 方法中调用 dispatchDraw 方法将绘制过程传递到子 View 中
  • DecorView 的 MeasureSpc 单纯的由其 LayoutParams
    • match_parent 精确模式,大小就是窗口的大小
    • wrap_content 最大模式,大小不定,最大不能超过窗口大小
    • 固定值 精确模式,大小为 LayoutParams 中指定的宽高大小
  • 子View的测量是根据父 View 的 MeasureSpc 和子 View 的 LayoutParams 来共同确定子 View 的 MeasureSpec ,其逻辑可以用一下图片总结
  • onMeasure() 的任务就是计算准确的 measuredWidth 和 measuredHeight。所以子 View 需要根据需求重写 onMeasure 方法来保存自己想要的测量值

getWidth()与getMeasuredWidth()的区别

内存泄露

小题大做 | 内存泄漏简单问,你能答对吗

产生内存泄露的原因:某个对象应该销毁,但因为被其他对象持有无法销毁就会产生内存泄漏。
比如:Handler 引起的内存泄漏。Activity已经销毁,但销毁后handleMessage方法被调用,内部类还持有外部类Activity的引用。解决办法:
1.将内部类声明为静态(kotlin内部类默认就是静态)
2.或者用弱引用
3.用生命周期更长的比如applicationContext代替activity

  • 为什么内部类会持有外部类引用 因为编译时候,内部类构造方法中会传入外部类的引用
//原代码
class InnerClassOutClass{
    class InnerUser {
       private int age = 20;
    }
}

//class代码
class InnerClassOutClass$InnerUser {
    private int age;
    InnerClassOutClass$InnerUser(InnerClassOutClass var1) {
        this.this$0 = var1;
        this.age = 20;
     }
}
复制代码

ANR

  • 应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作“应用程序无响应”(ANR:Application Not Responding)对话框
  • 默认情况下,在Android中Activity的最长执行时间是5秒(主要类型),BroadcastReceiver的最长执行时间的则是10秒,ServiceTimeout的最长执行时间是20秒(少数类型)
  • 使用Traceview,可以看到各个线程的各个方法的执行时间,耗时比较长的方法就应该重点关注
  • 主线程所有的方法也是通过Looper轮训消息执行的。通过设置logging,监听打印的字符串,可以判断坚挺到每个方法的耗时

public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}


if (logging != null) {
    logging.println(">>>>> Dispatching to " + msg.target + " "
            + msg.callback + ": " + msg.what);
}
...
msg.target.dispatchMessage(msg);
...
if (logging != null) {
    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

复制代码

Activity,View,Window三者关系

  • 一个Activity包含了一个Window对象,这个对象是由PhoneWindow来实现的
  • PhoneWindow将DecorView作为整个应用窗口的根View
  • 而DecorView又将屏幕划分为两个区域:一个是TitleView,另一个是ContentView,我们平时所写的就是展示在ContentView中的

Fragment生命周期

图片.png


Android框架知识


buttnife实现原理

通过注解处理器动态生成java文件,在java文件中进行findViewById和setOnClickListener操作

EventBus实现原理

  • 注册的时候传入this,获取class,再通过反射获取class的所有方法,从中找到订阅方法
  • 将不同事件类型的订阅方法分别保存在一起,在同一个list(事件类型决定订阅方法收到通知后怎么执行,MAIN主线程调用,POSTING默认模式,发布者和订阅者在同一个线程执行)
  • 如何找到订阅方法?先从缓存中获取订阅方法列表,如果缓存中不存在则通过反射获取到订阅者所有的函数,遍历再通过权限修饰符
  • 如何在子线程发布消息后在主线程处理?通过主线程的Looper(mainLooper)
  • 收到通知后通过反射调用
  • 如何确定优先级?2中添加方法时候,会根据优先级来添加,优先级越高的,添加在最前面
  • 黏性事件如何保存和发送?发送一个 sticky 事件后,该事件会被 缓存起来,当订阅该事件的类调用 register() 方法时,就会收到保存的事件

LiveData原理

LiveData通知其他组件原理主要是观察者设计模式。在android里用的比较多的是MutableLiveData

 public class MutableLiveData<T> extends LiveData<T> {
    //非主线程中使用
    @Override
    public void postValue(T value) {
        super.postValue(value);
    }

    //主线程中使用
    @Override
    public void setValue(T value) {
        super.setValue(value);
    }
}
复制代码

通过LiveData的postValue或者setValue方法,通知观察者Observer数据的变化并请可观察的变化数据通过Observer的onChanged传导出来

其优点有

  • 遵从应用程序的生命周期,如在Activity中如果数据更新了但Activity已经是destroy状态,LivaeData就不会通知Activity(observer)
  • 不会造成内存泄漏(LiveData仅通知活跃的Observer去更新UI。 非活跃状态的Observer,即使订阅了LiveData,也不会收到更新的通知)

LiveDataobserve方法

    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        //将LifecycleBoundObserver和activity或者fragment的lifeCycle相关联
        owner.getLifecycle().addObserver(wrapper);
    }
复制代码

装饰器LifecycleBoundObserver

 class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
            super(observer);
            mOwner = owner;
        }

        @Override
        boolean shouldBeActive() {
            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
        }

        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
                //数据发生变化,如果activity或者fragment已经销毁,就解除订阅,避免了内存泄露
            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            activeStateChanged(shouldBeActive());
        }
......
复制代码
public interface Observer<T> {
    /**
     * Called when the data is changed.
     * @param t  The new data
     */
    void onChanged(@Nullable T t);
}

复制代码

第一个参数可以直接传activity,如果activity已经销毁直接return,否则owner.getLifecycle().addObserver(wrapper);,LiveData调用postValue或者setValue方法就会回调ObserveronChanged方法

  • LiveData的observe方法接收activity的getLifeCycle(可感知activity生命周期)和Observer(添加一个观察者到LiveData的内部集合里)。
  • LiveData的setValue方法将数据通知到所有观察者,如果actiivty已经销毁则之间解除订阅。否则所有观察者的onChange(T t)方法会被执行

这数据处理逻辑放在ViewModel里,需要更新UI时给观察者发送通知即可,同时不用关心内存泄漏

ViewModel

  • 处理逻辑
  • 存储数据,ViewModel储存的数据activity旋转后数据不会丢失
  • 数据共享

Lifecycle

app优化

  • 内存优化:使用leakcanary抓取内存泄露,或者使用android studio抓取内存信息,通过Profiler分析内存泄露情况
  • 体积优化
    • 不复杂图片使用svg代替png。换肤时使用着色器,可减少图片资源
    • build文件配置
      • 保留指定语言
      • 保留指定so库架构
      • 开启混淆压缩

Glide

  • Glide的缓存
    • Glide缓存机制大致分为三层:Lru算法缓存、弱引用缓存、磁盘缓存
    • 读取的顺序是:Lru算法缓存、弱引用缓存、磁盘缓存(据说glide最新版改了,先从弱引用中取,没有的话再从Lru中取,再放进弱引用中)
  • Glide.with(this) .load("http://www.baidu.com/img/bdlogo.png") .into(imageView);
    • with()方法是对RequestManager进行配置
    public static RequestManager with(FragmentActivity activity) {
         return getRetriever(activity).get(activity);
    }
    复制代码
    • load()方法是对RequestBuilder进行配置
    public RequestBuilder<Drawable> load(@Nullable Object model) {
         return asDrawable().load(model);
    }
    复制代码
    • into()方法是通过线程池给imageView进行图片的加载设置
  • 占用内存较小
    • 默认使用RGB_565格式
  • 支持gif
  • 与Activity生命周期绑定,不会出现内存泄露
      1. Glide绑定Activity时,生成一个无UI的Fragment
      1. 将无UI的Fragment的LifeCycle传入到RequestManager中
      1. 在RequestManager的构造方法中,将RequestManager存入到之前传入的Fragment的LifeCycle,在回调LifeCycle时会回调到Glide的相应方法

Android串口

参考Android串口通信:抱歉,学会它真的可以为所欲为
通过串口编程可以让Android应用和外设进行通信,通过谷歌提供的一个来开发。
通过那个库,打开串口。获取输入输出流,就可以利用串口接收数据和发送数据了
核心参数:

path:为串口的物理地址,一般硬件工程师都会告诉你的例如ttyS0、ttyS1等,或者通过SerialPortFinder类去寻找得到可用的串口地址。
baudrate:波特率,与外接设备一致
flags:设置为0,原因较复杂,见文章最底下
复制代码

断点上传下载实现

断点下载

  • 在本地下载过程中要使用数据库实时存储到底存储到文件的哪个位置了
  • 下次继续传递时,才能通过HTTP的GET请求中的setRequestProperty("Range","bytes=startIndex-endIndex");方法可以告诉服务器,数据从哪里开始,到哪里结束
  • 同时在本地的文件写入时,RandomAccessFile的seek()方法也支持在文件中的任意位置进行写入操作
  • 最后通过广播或事件总线机制将子线程的进度告诉Activity的进度条。关于断线续传的HTTP状态码是206

断点上传

  • 对文件进行分块,每次上传前从服务器获知哪些块还未上传,上传这些块即可
  • 所有块上传上传完毕后通知服务器将所有块合并

热修复

  • 什么是dex分包
    • 把一个apk解压后,会有一个classes.dex的文件,它包含了我们项目中所有的class文件
    • dvm中存储方法id用的是short类型,所以就导致dex中方法不能超过65535个
  • 分包的原理
    • 就是将编译好的class文件,拆分打包成多个dex
    • 除了第1个dex文件外(正常apk中存在的唯一的dex文件),其他的所有dex文件都以资源的形式放到apk里面,并在Application的onCreate回调中通过系统的ClassLoader加载它们。
    • 注意:在注入之前就已经引用到的类,则必须放到第一个dex文件中,否则会提示找不到该文件
  • ClassLoader动态加载
    • 每个dex文件是一个Element,多个dex文件排列成一个有序的数组就是dexElements
    • 将我们修复Bug后的dex文件,通过反射加入到dexElements数组最前面,就可以了

消息机制

一文读懂 Handler 机制全家桶

  • MessageQueue的数据结构 --> 链表(因为要不停的插入和取出消息,链表插入和删除是O(1),数组是O(n))
  • Message插入的位置 --> 遍历MessageQueue,比较插入消息和MessageQueue中消息的when,插入后保证MessageQueue的when从小到大排列即可
  • 什么时候取出消息 --> 会有个循环从MessageQueue中取消息,会根据第一个消息的when进行阻塞,直到阻塞时间到,就会唤醒next方法取消息
  • Handle怎么发送Runnable --> 封装成Message,Message有一个Runnable类型的成员变量

图片.png

图片.png

image.png

图片.png

图片.png

图片.png

  • 主线程怎么向子线程中发消息 --> 在子线程中Looper.prepare(),Looper.loop(),开启子线程中的MessageQueue
  • 为什么子线程向主线程发消息不需要Looper.prepare(),Looper.loop() --> Activity启动时调用了prepare()和loop(),开启过了主线程的MessageQueue
  • 不同线程怎么单独维护一个Looper的 --> 每个线程都有ThreadLocal成员变量, Looper.prepare()的时候用他来存储Loop实例。这样不同的线程就有了不同的Looper,不同的MessageQueue(MessageQueue就是Loop的成员变量)
  • Handle使用
    • ActivityThread的main方法中,Looper.prepareMainLopper() (子线程调用Looper.prepare()),Looper.loop()在当前线程中存了Looper实例,同时开始轮训自己的MessageQueue
    • Handler handle = new Handler(),取出主线程中的Looper,取出Looper中MessageQueue(sendMessage时需要将messgae放到MessageQueue)

OkHttp

图片.png

  • Interceptor类
    • RetryAndFollowUpInterceptor:重试和失败重定向拦截器
    • BridgeInterceptor:桥接拦截器,处理一些必须的请求头信息的拦截器
    • CacheInterceptor:缓存拦截器,用于处理缓存
    • ConnectInterceptor:连接拦截器,建立可用的连接,是CallServerInterceptor的基本
    • CallServerInterceptor:请求服务器拦截器将 http 请求写进 IO 流当中,并且从 IO 流中读取响应 Response
  • OkHttp对于网络请求都有哪些优化,如何实现的
    • 在okhttp中,我们每次的request请求都可以理解为一个connection,而每次发送请求的时候我们都要经过tcp的三次握手,然后传输数据,最后在释放连接。在高并发或者多个客户端请求的情况下,多次创建就会导致性能低下。如果能够connection复用的话,就能够很好地解决这个问题了。能够复用的关键就是客户端和服务端能够保持长连接,并让一个或者多个连接复用。怎么保持长连接呢?在BridgeInterceptor的intercept()方法中requestBuilder.header("Connection", "Keep-Alive"),我们在request的请求头添加了("Connection", "Keep-Alive")的键值对,这样就能够保持长连接。而连接池ConnectionPoll就是专门负责管理这些长连接的类。需要注意的是,我们在初始化 ConnectionPoll的时候,会设置 闲置的connections 的最大数量为5个,每个最长的存活时间为5分钟
    • 无缝支持GZIP来减少数据流量,在request 头中加入 "Accept-Encoding", "gzip"

Leakcanary

  • 垃圾回收机制分为「引⽤计数法」和「可达性分析法」

    • 「引⽤计数法」 Python , Object-C , Swift⽤⼀个计数器记录⼀个对象被引⽤的次数,如果引⽤的次数被减少到 0 那么说明这个对象是垃圾对象。(引⽤计数有循环引⽤的问题)

    • 「可达性分析法」 Java。Jvm 通过⼀些 GC Roots 向下搜索,如果可以被 Gc Roots 引⽤到的对象,说明这个对象不是垃圾 对象,反之这个对象就算互相引⽤了也是垃圾对象 那些对象

    GC Roots:GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区,正在运⾏的线程不被GC所管理,因而选择这些区域内的对象作为GC roots

  • 通过注册 Application 和 Fragment 上的⽣命周期回调来完成在 Activity和 Fragment 销毁的时候开始观察

  • 监控原理 图片.png

  • 检测到内存泄露后通过分析dump文件来找到发生内存泄露的地方

BlockCanary

  • 原理:主线程的UI绘制最终都在ActivityThread的Handle的handleMessage()回调方法中执行。统计dispatchMessage方法执行的耗时来判断应用是否卡顿
  • 缺点:定位不准。会拼接字符串

谷歌两部验证原理

  • 客户端请求服务器,服务器随机生成一个密钥,把这个密钥保存在数据库中,同时返给客户端。

图片.png

  • 客户端拿到密钥后,去谷歌身份验证器客户端。它会每30秒使用密钥和时间戳通过一种『算法』生成一个6位数字的一次性密码,如『684060』

图片.png

  • 下次需要做安全验证时候,就拿着验证码提交给服务器获取验证结果。只要密钥,时间戳和算法都相同。就可以生成同一种一次性密码。

以前银行的U盾也是这个原理,密钥就存在U盾里

但是这里比较重要的是用户手机时间要准确,因为从算法原理来讲,身份验证服务器会基于同样的时间来重复进行用户手机的运算。进一步来说,服务器会计算当前时间前后几分钟内的令牌,跟用户提交的令牌比较。所以如果时间上相差太多,身份验证过程就会失败。

双亲委派机制

根据双亲委派模式,在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,如果没有加载的话则请求继续向上传递直Bootstrap ClassLoader。如果请求向上委托过程中,如果始终没有检测到该类已经加载,则Bootstrap ClassLoader开始尝试从其对应路劲中加载该类文件,如果失败则由子类加载器继续尝试加载,直至发起加载请求的子加载器为止

优点:避免了类的重复加载


设计模式


1.装饰设计模式

  • 当不适合采用生成子类的方式对已有类进行扩充时,采用装饰设计模式可以扩展一个对象的功能,可以使一个对象变得越来越强大
  • 不适合采用生成子类的方式对已有类进行扩充原因:会使类更加臃肿。子类会继承父类所有非private的变量和方法,然后再进行扩充。而使用装饰设计模式扩充的类,只需要增加扩种那部分功能即可
  • 使用场景:RecyclerView本身是不支持添加底部和头部的,那么采用装饰设计模式可以对其进行功能扩展。装饰设计模式 RecyclerView添加头部和底部

2.MVC、MCP、MVVP 的区别

1.MVC Android传统就是用MVC模式,Modle(逻辑)和V(View)直接交互,耦合度太高,MVC中是允许Model和View进行交互的

2.MVP Model与View之间的交互由Presenter完成。还有一点就是Presenter与View之间的交互是通过接口的。当View 需要更新数据时,首先去找 Presenter,然后 Presenter 去找 Model 请求数据,Model 获取到数据之后通知 Presenter,Presenter 再通知 View 更新数据,这样 Model 和 View就不会直接交互了,所有的交互都由 Presenter 进行,Presenter 充当了桥梁的角色。很显然,Presenter 必须同时持有 View 和 Model 的对象的引用,才能在它们之间进行通信 存在问题:

  • 内存泄露:由于Presenter经常性的需要执行一些耗时操作那么当我们在操作未完成时候关闭了Activity,会导致Presenter一直持有Activity的对象,造成内存泄漏
  • 随着业务逻辑的增加,UI的改变多的情况下,这样就会造成View的接口会很庞大。而MVVM就解决了这个问题

解决办法: 在Presenter中使用弱引用,将view的引用加到弱引用中去 每个Activity都有BaseActivity,BaseActivity中

3.MVVM通过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层 MvvmMvp比较相似,不同的是Mvvm中可以用DataBinding或者LiveData动态绑定数据,进一步降低耦合

  • Activity负责UI操作,ViewModel负责数据逻辑。两者通过LiveData进行关联。ViewModel返回LiveData实例和Activity绑定,ViewModel有变化时可以自动更新UI,Activity也可以通过ViewModel发起数据请求
  • 问题: 看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致

以登录为例,登录时只需要拿着用户名和密码请求服务器即可

  • MVC中,需要先找到输入框,再从输入框中拿到用户名和密码,再进行登录操作
  • MVP中,通过Presenter拿到用户名和密码,进行登录。相当于通过引入了PresenterM层和V层分离,降低耦合
  • MVVM中,用户在输入框输入完用户名和密码后,这种UI的变化直接同步到数据,直接登录

3.策略设计模式

适用场景: 某些业务中,某一个行为,会有多个实现类,并且当前业务只会选择一种实现类
参考用漫画的方式讲策略设计模式,一看就懂

4.Double Check Lock 实现单例

public static TestInstance getInstance(){ //1
    if (mInstance == null){ //2
        synchronized (TestInstance.class){ //3
            if (mInstance == null){ //4
                mInstance = new TestInstance(); //5
            }
        }
    }
    return mInstance;
}
复制代码

第一层判断主要是为了避免不必要的同步
第二层的判断则是为了在 null 的情况下创建实例。mInstance = new TestInstance(); 这个步骤,其实在jvm里面的执行分为三步:
1.在堆内存开辟内存空间;
2.初始化对象;
3.把对象指向堆内存空间;
由于在 JDK 1.5 以前 Java 编译器允许处理器乱序执行。不过在 JDK 1.5 之后,官方也发现了这个问题,故而具体化了 volatile ,即在 JDK 1.6 以后,只要定义为 private volatile static DaoManager3 sinstance ; 就可解决 DCL 失效问题

5.OkHttp中的责任链

public interface Interceptor {
    //每一层的拦截器接口,需要进行实现 Chain:串连拦截器的链
    Response intercept(Chain chain) throws IOException;

    //链主要有两个方法:拿到Request;通过Request拿到Response
    interface Chain {
        Request request();

        Response proceed(Request request):Response throws IOException;//负责往下执行
    }
}
复制代码

public class RealInterceptorChain implements Interceptor.Chain {
    final List<Interceptor> interceptors;//节点的列表
    final int index;//当前节点的index,通过index和interceptors就可以拿到所有节点
    final Request request;//请求

    public RealInterceptorChain(List<Interceptor> interceptors, int index, Request request){
        this.interceptors = interceptors;
        this.index = index;
        this.request = request;
    }
    @Override
    public Request request() {
        return request;
    }

    @Override
    public Response proceed(Request request) throws IOException {
        RealInterceptorChain next = new RealInterceptorChain(interceptors,  index + 1, request);

        //next传到当前节点,当前节点处理好request后就可以通过next执行proceed方法,将request传递到下一节点
        Interceptor interceptor = interceptors.get(index);
        Response response = interceptor.intercept(next);
        return response;
    }
}
复制代码

拦截器

public class BridgeInterceptor implements Interceptor{

    @Override
    public Response intercept(Chain chain) throws IOException {
        Log.e("TAG","BridgeInterceptor");
        Request request = chain.request();
        // 添加一些请求头
        request.header("Connection","keep-alive");
        // 做一些其他处理
        if(request.requestBody()!=null){
            RequestBody requestBody = request.requestBody();
            request.header("Content-Type",requestBody.getContentType());
            request.header("Content-Length",Long.toString(requestBody.getContentLength()));
        }
        Response response = chain.proceed(request);//这里的chain就是传进来的next,next的index已经加1

        return response;
    }
}
复制代码

RealCall中excute()方法

protected void execute() {
            final Request request = orignalRequest;
            try {
                List<Interceptor> interceptors = new ArrayList<>();
                interceptors.add(new BridgeInterceptor());
                interceptors.add(new CacheInterceptor());
                interceptors.add(new CallServerInterceptor());

                Interceptor.Chain chain = new RealInterceptorChain(interceptors,0,orignalRequest);
                Response response = chain.proceed(request);

                callback.onResponse(RealCall.this,response);
            } catch (IOException e) {
                callback.onFailure(RealCall.this,e);
            }
        }
复制代码

其他


Https

20 张图彻底弄懂 HTTPS 的原理!

  • 使用对称密钥:加密和解密使用的是同一个密钥。弊端:最开始的时候怎么将这个对称密钥发送出去呢?如果对称密钥在发送的时候就已经被拦截,那么发送的信息还是会被篡改和窥视
  • 使用非对称密钥:双方必须协商一对密钥,一个私钥一个公钥。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据, 只有对应的私钥才能解密。A将自己的公钥发给B,B以后给A发消息时候用公钥加密后发送,A收到消息后用自己的私钥解密。弊端:非对称密钥(RSA)加密和解密速度慢
  • 非对称密钥+对称密钥:A将自己的公钥发给B,B用公钥将对称密钥加密发给B,这样双方就安全地传递了对称加密的密钥,既解决了密钥的传递问题, 又解决了RSA速度慢的问题。弊端:第一次传递公钥时有风险
  • 数字证书。假如一开始A将自己的公钥发给B时,中间被拦截,拦截者替换成自己的公钥,这样还是会有安全问题。解决办法:数字证书。server向CA申请证书,也就是server的一些信息和公钥使用CA的私钥进行加密,传递给客户端,客户端使用CA的公钥进行解密,如果能解密,就说明这个公钥没有被中间人调包。因为如果中间人使用自己的私钥加密后的东西传给客户端,客户端是无法使用第三方的公钥进行解密的。公钥内置在操作系统上
  • 如果中间人也向CA申请了证书再掉包怎么办?客户端解密后,还需要验证证书上的域名与自己的请求域名是否一致,中间人中途虽然可以替换自己向 CA 申请的合法证书,但此证书中的域名与 client 请求的域名不一致,client 会认定为不通过!
  • 总结:HTTPS 采用证书来加强非对称加密的安全性,通过非对称加密,客户端和服务端进行通信传输的对称密钥,后续的所有信息都通过该对称秘钥进行加密解密,完成整个HTTPS 的流程
  • charles是怎么拦截请求看到明文的?安装 charles 的证书,这个证书里有 charles 的公钥,这样的话 charles 就可以将 server 传给 client 的证书调包成自己的证书,client 拿到后就可以用你安装的 charles  证书来验签等,验证通过之后就会用 charles 证书中的公钥来加密对称密钥了

Https和Http

  • HTTP 协议以明文方式发送内容,数据都是未加密的,安全性较差。HTTPS 数据传输过程是加密的,安全性较好。
  • HTTP 和 HTTPS 用的端口不一样,前者是 80 端口,后者是 443 端口。
  • HTTPS 协议需要到数字认证机构(Certificate Authority, CA)申请证书。
  • HTTP 页面响应比 HTTPS 快,主要因为 HTTP 使用 3 次握手建立连接,客户端和服务器需要握手 3 次,而 HTTPS 除了 TCP 的 3 次握手,还需要经历一个 SSL 协商过程

三次握手

其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常

  • 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

3.OAuth 2.0

用一个短期的令牌(token)允许用户让第三方应用访问他在某一网站上存储的私密的资源,而不需要用户名和密码

app登录成功后服务器返回一个token,下次app就通过这个token来直接登录,token如果过期就需要跳到登录页面重新登录
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异

  • 令牌是有时间限制,到期会自动失效。密码一般长期有效,除非用户修改。

  • 令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。

  • 令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。

4.ReactNative原理

在 React Native 框架中,JSX 源码通过 React Native 框架编译后,通过对应平台的 Bridge 实现了与原生框架的通信。如果我们在程序中调用了 React Native 提供的 API,那么 React Native 框架就通过 Bridge 调用原生框架中的方法。 如果是 UI 层的变更,那么就映射为虚拟 DOM 后进行 diff 算法,diff 算法计算出变动后的 JSON 映射文件,最终由 Native 层将此 JSON 文件映射渲染到原生 App 的页面元素上,最终实现了在项目中只需要控制 state 以及 props 的变更来引起 iOS 与 Android 平台的 UI 变更。 编写的 React Native代码最终会打包生成一个 main.bundle.js 文件供 App 加载,此文件可以在 App 设备本地,也可以存放于服务器上供 App 下载更新,后续章节讲解的热更新就会涉及到 main.bundle.js 位置的设置问题

5.对比Java,Kotlin的优点和缺点

1.kotlin语法糖熟练后用起来比java更顺手
2.空指针安全。对于一个可能是 null 的变量,在用之前,需要加上?
3.支持方法扩展。可以给类加上他没有的方法

6.别人面试题分享

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改