likes
comments
collection
share

Android面试知识点总结(四)—— 框架原理/Android常用组件原理篇

作者站长头像
站长
· 阅读数 5

OkHttp原理

为什么设置两个队列runningAsyncCalls & readyAsyncCalls?,为什么使用SynchronousQueue

  1. 使用2个Deque在外围维护整个请求链路,方便管理请求与操作
  2. SynchronousQueue为无容量队列,在线程池中能直接去运行,不需要进行等待,因为外围已经针对请求队列进行了相应的维护

为什么访问同一个目标机器请求数量小于5 ,运行队列最大上限为64?

  1. 因为Http协议对于同一协议最大请求并发数限制导致,Android = 4,web 6,9
  2. 因为Deque内存占用默认为16,运行队列相当于4次扩容

队列为什么是ArrayDeque ?

  1. 用作栈时,性能优于Stack,当用于队列时,性能优于LinkedList
  2. 两端都可以操作,方便进行管理,支持双向迭代器

ThreadPoolExecutor线程池主要的创建参数有哪些

//OkHttp的配置
public synchronized ExecutorService executorService() {
  if (executorService == null) {
    executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
  }
  return executorService;
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
        //...
}
  • corePoolSize :核心线程数量

    • 即使没有任务执行,核心线程也会一直存活
    • 线程数小于核心线程时,即使有空闲线程,线程沲也会创建新线程执行任务
    • 设置allowCoreThreadTimeout=true时,核心线程会超时关闭
  • maximumPoolSize: 最大线程数

    • 当所有核心线程都在执行任务,且任务队列已满时,线程沲会创建新线程执行任务
    • 当线程数 = maxPoolSize,且任务队列已满,此时添加任务时会触发RejectedExecutionHandler进行处理
  • keepAliveTime 、TimeUnit:线程空闲时间

    • 如果线程数 > corePoolSize,且有线程空闲时间达到keepAliveTime时,线程会销毁,直到线程数量 = corePoolSize
    • 如果设置allowCoreThreadTimeout = true时,核心线程执行完任务也会销毁直到数量 = 0
  • workQueue: 任务队列

    • ArrayBlockingQueue 有界队列,需要指定队列大小
    • LinkedBlockingQueue 若指定大小则和ArrayBlockingQueue类似,若不指定大小则默认能存储Integer.MAX_VALUE个任务,相当于无界队列,此时maximumPoolSize值其实是无意义的
    • SynchronousQueue 同步阻塞队列,当有任务添加进来后,必须有线程从队列中取出,当前线程才会被释放,newCachedThreadPool就使用这种队列
  • threadFactory: 创建线程的工厂

    • 通过他可以创建线程时做一些想做的事,比如自定义线程名称

OkHttp缓存机制

Http协议下的缓存机制

强制缓存:通过http协议所传送的数据,会被保存到缓存数据库中

对比缓存:通过返回值(304使用对比缓存,200使用服务器最新数据)确认是否使用

Http自身缓存机制

  1. 基于文件进行磁盘缓存
  2. 内部维护基于LRU算法的缓存清理线程

OkHttp在CacheInterceptor拦截器中进行缓存的判定机制:

  1. 以Request为key从Cache中读取候选缓存

  2. 根据「当前时间,Request,候选缓存」构建一个缓存策略,用于判断当前请求是否需要使用网络,是否存在缓存

  3. 根据缓存策略,如果当前请求不使用网络且没有缓存,直接报错并返回状态码504

  4. 根据缓存策略,如果当前请求不使用网络且存在缓存,直接返回缓存数据

  5. 进行网络操作,将请求交给下面的拦截器处理,同时获得返回的Response

  6. 若通过网络返回的Response的状态码为304,混合缓存Response和网络返回的Response的请求头,更新缓存并返回缓存Response

  7. 读取网络返回的Response,判断是否需要缓存,如果需要则对Response进行缓存

缓存策略主要是根据CacheStrategy中的networkRequest和cacheResponse来决定的:

networkRequestcacheResponse对应处理
nullnull直接报错,状态码返回504
nullnon-null直接返回缓存Response
non-nullnull请求最新数据,并满足缓存条件则缓存Response
non-nullnon-null网络Response状态码为304,则混合请求头后更新缓存,并返回缓存;若为200,直接返回网络Response,满足缓存条件则缓存Response

五大拦截器是那几个?对应的作用是什么?

参考:OKHhttp请求流程-五大拦截器

  1. RetryAndFollowUpInterceptor 重试重定向拦截器

    负责判断用户是否取消了请求;请求失败根据条件判断是否重试,在获得了结果之后会根据相应码判断是否需要重定向

  2. BridgeInterceptor 桥接拦截器

    负责将http协议规范的请求头补全,并添加一些默认的行为

  3. CacheInterceptor 缓存拦截器

    负责读取并判断是否使用缓存,同一个host的请求,如果存在缓存就先读出缓存,否则就去请求服务器,拿到结果后讲缓存存到磁盘,供下次使用

  4. ConnectInterceptor 链接拦截器

    负责判断连接池里面是否存在创建好的socket流,判断当前的连接是否可以使用,流是否已经被关闭,是否已经被限制创建新的流,如果当前的连接无法使用,就从连接池中获取一个连接,如果连接池中也没有发现可用的连接,创建一个新的连接,并进行握手,然后将其放到连接池中。省去了重复性的TCP/TL握手挥手过程,提升网络访问效率

  5. CallServerInterceptor 请求服务器拦截器

    负责调用上层拦截器传过来的RealConnection 和 HttpCodec,发送Request,并接收 Response 然后返回给上层拦截器。

总结:

retryAndFollowUpInterceptor 负责请求的重试和重定向

BridgeInterceptor 负责补全请求头字段

CacheInterceptor 负责Response的缓存

ConnectInterceptor 负责建立Http连接和连接的复用

CallServerInterceptor 发送请求数据和读取响应数据

OKHttp有哪些拦截器,分别起什么作用

OKHTTP的拦截器是把所有的拦截器放到一个list里,然后每次依次执行拦截器,并且在每个拦截器分成三部分:

  • 预处理拦截器内容
  • 通过proceed方法把请求交给下一个拦截器
  • 下一个拦截器处理完成并返回,后续处理工作。

这样依次下去就形成了一个链式调用,看看源码,具体有哪些拦截器:

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

根据源码可知,一共七个拦截器:

  • addInterceptor(Interceptor),这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。
  • RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,以及请求失败的充实工作,重定向的后续请求工作。跟他的名字一样,就是做重试工作还有一些连接跟踪工作。
  • BridgeInterceptor,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。
  • CacheInterceptor,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
  • ConnectInterceptor,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
  • networkInterceptors,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,所以用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。
  • CallServerInterceptor,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。

应用拦截器和网络拦截器有什么区别?

从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别。

  1. 首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
  2. 其次,除了CallServerInterceptor之外,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。
  3. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。

OkHttp怎么实现连接池

为什么需要连接池?

频繁的进行建立Sokcet连接和断开Socket是非常消耗网络资源和浪费时间的,所以HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。keepalive机制是什么呢?也就是可以在一次TCP连接中可以持续发送多份数据而不会断开连接。所以连接的多次使用,也就是复用就变得格外重要了,而复用连接就需要对连接进行管理,于是就有了连接池的概念。

  • OkHttp中使用ConectionPool实现连接池,默认支持5个并发KeepAlive,默认链路生命为5分钟。

怎么实现的?

1)首先,ConectionPool中维护了一个双端队列Deque,也就是两端都可以进出的队列,用来存储连接。

2)然后在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。

RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

也就是遍历了双端队列,如果连接有效,就会调用acquire方法计数并返回这个连接。

如果没找到可用连接,就会创建新连接,并会把这个建立的连接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了ConectionPool的put方法。

public final class ConnectionPool {
    void put(RealConnection connection) {
        if (!cleanupRunning) {
        	//没有连接的时候调用
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
}

3)其实这个线程池中只有一个线程,是用来清理连接的,也就是上述的cleanupRunnable

private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                //执行清理,并返回下次需要清理的时间。
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        //在timeout时间内释放锁
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

这个runnable会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,然后进入wait等待。

怎么清理的呢?

看看源码:

long cleanup(long now) {
    synchronized (this) {
      //遍历连接
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //检查连接是否是空闲状态,
        //不是,则inUseConnectionCount + 1
        //是 ,则idleConnectionCount + 1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      //如果超过keepAliveDurationNs或maxIdleConnections,
      //从双端队列connections中移除
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {      
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {      //如果空闲连接次数>0,返回将要到期的时间
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 连接依然在使用中,返回保持连接的周期5分钟
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

也就是当如果空闲连接maxIdleConnections超过5个或者keepalive时间大于5分钟,则将该连接清理掉

怎样属于空闲连接?

其实就是有关刚才说到的一个方法acquire计数方法

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

RealConnection中,有一个StreamAllocation虚引用列表allocations。每创建一个连接,就会把连接对应的StreamAllocationReference添加进该列表中,如果连接关闭以后就将该对象移除。

总结:

连接池的工作就这么多,主要就是管理双端队列Deque<RealConnection>,可以用的连接就直接用,然后定期清理连接,同时通过对StreamAllocation的引用计数实现自动回收。

OkHttp里面用到了什么设计模式

  • 责任链模式

这个不要太明显,可以说是okhttp的精髓所在了,主要体现就是拦截器的使用,具体代码可以看看上述的拦截器介绍。

  • 建造者模式

在Okhttp中,建造者模式也是用的挺多的,主要用处是将对象的创建与表示相分离,用Builder组装各项配置。 比如Request:

public class Request {
  public static class Builder {
    @Nullable HttpUrl url;
    String method;
    Headers.Builder headers;
    @Nullable RequestBody body;
    public Request build() {
      return new Request(this);
    }
  }
}
  • 工厂模式

工厂模式和建造者模式类似,区别就在于工厂模式侧重点在于对象的生成过程,而建造者模式主要是侧重对象的各个参数配置。 例子有CacheInterceptor拦截器中又个CacheStrategy对象:

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;
      //...
    }
  • 观察者模式

之前我写过一篇文章,是关于Okhttp中websocket的使用,由于webSocket属于长连接,所以需要进行监听,这里是用到了观察者模式:

  final WebSocketListener listener;
  @Override public void onReadMessage(String text) throws IOException {
    listener.onMessage(this, text);
  }
  • 单例模式:

    每个OkHttpClient 对象都管理自己独有的线程池和连接池,使用单例共享OkHttpClient对象

  • 享元模式:

    在Dispatcher的线程池中,所用到了享元模式,一个不限容量的线程池 , 线程空闲时存活时间为 60 秒。线程池实现了对象复用,降低线程创建开销,从设计模式上来讲,使用了享元模式。(享元模式:尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象,主要用于减少创建对象的数量,以减少内存占用和提高性能)

使用OkHttp发送网络请求并根据请求结果刷新UI有哪几种方式

  1. 使用AsyncTask + OkHttp的同步请求
  2. 使用OkHttp的异步请求+runOnUiThread方法(或者通过Handler发送到UI线程)

可否介绍一下OkHttp的整个异步请求流程

通过OkHttpClient对象和Request对象创建Call 对象,使用Call调用enqueue方法,通过该方法调用分发器Dispather的enqueue方法,将AsyncCall入队并提交给线程池执行,线程池中的线程会调用Call的execute()方法,Call的execute()方法会调用getResponseWithInterceptorChain()方法通过一系列拦截器对请求进行处理之后发出该请求并读取响应

注意:异步调用请求时,存在2个队列(运行,等待),需要满足运行队列小于64并且同一个host小于5,才会加入运行队列进行请求,否则会加入等待队列进行等待

OkHttp对于网络请求都有哪些优化,如何实现的

  1. 分发器线程池的引入

    异步2个队列加上线程池处理无容量队列

  2. 缓存 okhttp 的缓存策略是,key 为 Request的 url 的 MD5 值,value 为 response

    拦截器实现缓存效果,无网络状态,判断有无缓存,有则返回,有网络状态,无缓存,则缓存本次请求,有则在满足返回值304时,混合请求头后更新缓存并返回

  3. 多路复用

    连接池,socket复用机制,5分钟内保存5个连接,有就直接复用,没有创建后放入

  4. 支持gzip压缩

    当 response 通过 bridgeInterceptor 处理的时候会进行 gzip 压缩,这样可以大大减小我们的 response ,他不是什么情况下都压缩的,只有在Encoding == null 并且Range == null进行压缩

    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
          transparentGzip = true;
          requestBuilder.header("Accept-Encoding", "gzip");
    }
    

OkHttp源码中用到的核心类有哪些,简单讲一下

OkhttpClient :

对外的API,OkHttp的很多功能模块,全部包装进这个类;创建分为两种:一种是new OkHttpClient()的方式;另一种是使用建造者(Builder)模式 – new OkHttpClient.Builder()…Build()。 那么这两种方式有什么区别呢? 第一种:new OkHttpClient(),OkHttp做了很多工作,很多我们需要的参数在这里都获得默认值,也就是默认值设定。 第二种:默认的设置和第一种方式相同,但是我们可以利用建造者模式单独的设置每一个属性; 注意事项:OkHttpClient强烈建议全局单例使用,因为每一个OkHttpClient都有自己单独的连接池和线程池,复用连接池和线程池能够减少延迟、节省内存。

RealCall:

集成Call类,从源代码中,可看到使用Call类,发送出(同步/异步)请求.RealCall的主要作用:发送请求,当中还有拦截器的建立过程,异步回调。

Dispatcher(分发器,调度器,多线程):

保存同步和异步Call的地方,并负责执行异步AsyncCall

Interceptor:

有用户自定义的Interceptor、RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor、 CallServerInterceptor。拦截器之所以可以依次调用,并最终再从后向前返回Response,都依赖于RealInterceptorChain的proceed方法.

RealInterceptorChain(拦截器链):

getResponseWithInterceptorChain()方法中,先创建了一个拦截器列表interceptors,当拦截器列表组装完成,就会实例化拦截器链对象RealInterceptorChain:

 Response getResponseWithInterceptorChain() throws IOException {
    //...
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    return chain.proceed(originalRequest);
  }

然后调用该对象的proceed方法进行发送请求并接收响应。

为什么OkHttp好用呢?OkHttp有什么特点呢?

  1. 支持http2,对一台主机的所有请求共享同一个socket 连接
  2. 内置连接池,支持连接复用,减少延迟
  3. 支持透明的gzip压缩响应体
  4. 通过缓存避免不必要的网络请求
  5. 请求失败时自动重试主机的其他ip,自动重定向
  6. 好用的API,比如提供配置dns的api,可以配置httpdns

OkHttp怎么实现断点续传流程,用什么保存

获取本地已下载文件的长度(无文件为0),通过获取简要下载文件的长度(HTTP相应头部content-Length),对比长度后,在下载请求的头部文件中配置Range范围

okhttp实现带进度上传下载

OkHttp把请求和响应分别封装成了RequestBody和ResponseBody,下载进度自定义ResponseBody,重写source()方法,上传进度自定义RequestBody,重写writeTo()方法

为什么response.body().string() 只能调用一次

第一次拿到字节流后就默认关闭了这个流

Handler原理

面试题

  • 用途

    1. 安排 Message 和 runnables 在将来的某个时刻执行
    2. 在不同的线程上执行操作并排入队列。(在多个线程并发更新UI的同时保证线程安全)
  • Message:接受和处理消息对象

  • MessageQueue:管理Message的队列,先进先出,每一个线程最多存在一个

    MessageQueue主要包含两个操作:插入读取,MessageQueue 内部通过一个单链表数据结构维护消息列表

    next:一个无限循环的方法,如果消息队列中没有消息,那么 next 方法会一直阻塞。当有新消息到来时,next 方法会返回这条消息并将其从单链表中移除

  • Looper:消息泵,是 MessageQueue 的管理者,会不断从 MessageQueue 中取出消息,并将消息分给对应的 Handler 处理,每个线程只有一个 Looper

    Looper 会不停地从 MessageQueue 中 查看是否有新消息,如果有新消息就会立刻处理,否则会一直阻塞

    拓展:可通过 Looper.prepare() 为当前线程创建一个 Looper,除了 prepare 方法外,Looper 还提供了 prepareMainLooper 方法,Looper 提供了 quitquitSafely 来退出一个 Looper

    1.prepareMainLooper:给 ActivityThread(主线程) 创建 Looper 使用,本质也是通过 prepare 方法实现的

    2.quit与quitSafely区别quit 会直接退出 Looper,而 quitSafly 只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出

    注意:loop 方法是一个死循环,唯一跳出循环的方式是 MessageQueue 的 next 方法返回了null,只有使用quit方法通知消息退出队列,使队列next标记为null来达到阻塞目的,当没有消息时,next会一直阻塞,导致loop方法一直阻塞

handler的请求发送到哪里去了?使用post()跟sendMessage()有没有什么区别

  1. handler最后的请求都发送到了MessageQueue中enqueueMessage()方法中了,该方法中会执行一个死循环不停的处理Message消息

  2. 不论是post(),还是sendXX的其他方法,最后都会进入到enqueueMessage()方法中,本质上没有区别,就是post方法会传入一个Runnable,接受消息会在run()方法中,send如果不主动给Message中的callback**Runnable对象赋值的话,接受消息会在handlerMessage中进行返回(handlerMessage可以通过实现Handler.CallBack实现,也可以直接实现重写handler的handlerMessage方法实现**)

    注意:当Message中Runnable不为空时,只会执行闭包(无参无返回)函数run不会回传,为null时,会将包体数据通过handlerMessage回传回来

Looper什么时候被创建的?,如何消费的?

在主线程ActivityThread的main()中调用prepareMainLooper()创建,随后调用loop方法,进行死循环,不断的调用MessageQueue中的next方法死循环获取Message对象,然后通过Message对象根据target拿到handler对象调用dispatchMessage()进行消息的分发

一个线程有几个Handler ?一个线程有几个Looper ?如何保证?

  1. Handler个数与所在线程无关,可以在线程中实例化任意个数的Handler。
  2. Looper的构造方法被声明为了private,我们无法通过new关键字来实例化Looper,唯一开放的可以实例化Looper的方法是prepare()
  3. 实例化Looper并将实例化后的Looper保存到ThreadLocal中,而如果ThreadLocal中已经保存了Looper,则会抛出一个RuntimeException的异常。那么意味着在一个线程中最多只能调用一次prepare()方法,这样就保证了Looper的唯一性。

Looper、Handler、MessageQueue、Message之间的关系

主线程创建Looper时,同时会创建一个MessageQueue对象,然后通过Looper的loop方法去对MessageQueue中的Message进行分发,Message得到处理后会调用Handler中的dispatchMessage调用run发送数据

Looper是消息泵,是 MessageQueue 的管理者,MessageQueue是管理Message的队列,Message是接受和处理消息的载体,Handler是消息通讯的桥梁用来发送和接受处理

Handler、Thread和HandlerThread、IntentService的区别

  1. Handler是消息通讯的桥梁,主要用来发送消息和处理消息
  2. Thread是一个普通的线程
  3. HandlerThread是一个带有Looper的线程,在其run()方法中调用Looper.prepare()创建了Looper实例,并且开启了loop()
  4. IntentService是 Service 的一个子类,它的内部有一个 Handler 和 HandlerThread。所以 IntentService 与 Service 最大的不同就是 IntentService 在后台开启了一个子线程,而 Service 并没有,它还是在 UI 线程里。IntentService 通过 Handler 和 HandlerThread 来开启一个线程

Handler线程是如何实现切换的?

当在A线程中创建handler的时候,同时创建了MessageQueue与Looper,Looper在A线程中调用loop进入一个无限的for循环从MessageQueue中取消息,当B线程调用handler发送一个message的时候,会通过消息发送时存入的一个Handler对象去调用dispatchMessage()方法,将message插入到handler对应的MessageQueue中,因为Looper.loop()是在A线程中启动的,所以则回到了A线程,达到了从B线程切换到A线程的目的。

Handler内存泄漏的原因是什么?如何解决?

通过匿名内部类的方式来实例化Handler,而非静态的匿名内部类默认持有外部类的引用,即匿名内部类Handler持有了外部类。因为Handler的生命周期与宿主的生命周期不一致从而触发了内存泄漏

比如说在Activity中实例化了一个非静态的匿名内部类Handler,然后通过Handler发送了一个延迟消息,但是在消息还未执行时结束了Activity,此时由于Handler持有Activity,就会导致Activity无法被GC回收,也就是出现了内存泄漏的问题。

解决方式:将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并在 AcitivityonDestroy()中调用 handler.removeCallbacksAndMessages(null)及时移除所有消息。

Message数据结构是啥?为什么这样设计?,每次创建时都是创建新的对象吗?回收机制是什么?

  1. Message是一种单链表结构,是一种非线性、非连续性物理结构,由n个独立节点连接组成
  2. 因为loop跟MessageQueue中存在死循环,为了方便合理的去利用内存空间
  3. Message内部存在一个对象池,每次创建时会优先从对象池中进行拿去,如果不存在才会创建新的对象。
  4. Message在每次回收会调用recycle方法,recycle会将该对象进行初始化并存入对象池中,供后续使用

MessageQueue数据结构?怎么存储数据?

满足先进先出,在队尾增加数据,队首读取数据或者删除,MessageQueue是一个用于存储消息、用链表实现的特殊队列结构

通过死循环,使用快慢指针p和prev,每次向后移动一格,直到找到某个节点p的when大于我们要插入消息的when字段,则插入到p和prev之间。 或者遍历到链表结束,插入到链表结尾。

延迟是怎么实现的?

handler.postDelay并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以Message的时间顺序排列和唤醒的方式结合实现的。

Looper 的 loop() 死循环为什么不卡死?

当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制,会在 MessageQueue.next()中调用nativePollOnce()方法,此时线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作

总结:在没有消息时会处于休眠状态,有消息才会被换新,并不会消耗大量CPU资源

**epoll机制:**是一种I/O多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒。

  • 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过epoll机制进入阻塞状态。
  • 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程(enqueueMessage方法needWake`字段执行唤醒操作)。

loop() 后的处理为什么不可执行

因为 loop() 是死循环,直到 quit 前后面的处理都无法得到执行,所以避免将处理放在 loop() 的后面。

异步 Message 或同步屏障

异步 Message:设置了 isAsync 属性的 Message 实例

可以用异步 Handler 发送 也可以调用 Message#setAsynchronous() 直接设置为异步 Message

同步屏障:在 MessageQueue 的某个位置放一个 target 属性为 null 的 Message,确保此后的非异步 Message 无法执行,只能执行异步 Message

原理:当 MessageQueue 轮循 Message 时候发现建立了同步屏障的时候,会去跳过其他 Message,读取下个 async 的 Message 并执行,屏障移除之前同步 Message 都会被阻塞

总结:异步消息一般是系统内部使用的,当handler收到异步消息时,会优先处理异步消息,等到异步消息处理完后,才会处理同步消息

IdleHandler 空闲 Message

适用于期望空闲时候执行,但不影响主线程操作的任务

系统应用: Activity destroy 回调就放在了 IdleHandler 中,ActivityThread 中 GCHandler 使用了 IdleHandler,在空闲的时候执行 GC 操作 App应用: 发送一个返回 true 的 IdleHandler,在里面让某个 View 不停闪烁,这样当用户发呆时就可以诱导用户点击这个 View ,将某部分初始化放在 IdleHandler 里不影响 Activity 的启动

RecyclerView原理-缓存策略

参考:真正带你搞懂 RecyclerView 的缓存机制

  1. ViewHolder是View的容器,一个ViewHolder中包含一个View,一个View也就对应一个ViewHolder
  2. Recycler是RecyclerView的一个内部类,主要负责ViewHolder的回收和复用

四级缓存机制:

层级缓存变量缓存属性容量数据结构缓存用途
1mChangeScrap与mAttachedScrap可见缓存nArrayList用于布局过程中屏幕可见表项的回收和复用
2mCachedViews缓存列表2ArrayList用于移除屏幕表项的回收和复用,不会清空数据
3mViewCacheExtension自定义n
4RecyclerViewPool缓存池5SparseArray用于移除表项的回收和服用,会将ViewHolder的数据重置

mAttachedScrap:存放分离但未remove的ViewHolder,不会做数据的修改,且不走Adapter的绑定方法

mChangedScrap:存放分离但未remove且发生了变化的ViewHolder,使用这里的缓存,需要重新走Adapter绑定方法

mCacheViews:存放分离且remove的ViewHolder,最大容量为2,不需要重新绑定

RecycleViewPool:本身是一个内部类,保存的ViewHolder不仅仅是removed掉的视图,而且是恢复了出厂设置的视图,任何绑定过的痕迹都没有了,需要走Adapter绑定方法

  • 根据不同的itemType来对该itemType下的ViewHolder进行缓存,每个不同的itemType默认缓存5个(该值可以通过setMaxRecycledViews方法修改)
  • 根据不同的itemType使用SparseArray缓存对应的Scrap,每个Scrap中又用ArrarList缓存对应的ViewHolder(默认大小 = 5,可修改)

mViewCacheExtension:自定义缓存,官方未实现,本身为空

回收流程:

在滑动的过程中,当一个ViewHolder从可见变成不可见时,走到scrapView方法,判断是否是修改过的(此时的ViewHolder仅仅只是被分离,没有被review),如果未被修改,添加到mAttachedScrap中,如果被修改了则添加到mChangedScrap中;继续继续滑动过程中,上述列表中被review的ViewHolder会走到recycleViewHolderInternal方法中,(此时已经被review)如果符合条件mCacheViews容量为2则存入mCacheViews中,如果满了不满足,则会将最久的(index = 0)的数据存放到RecyclerViewPool中,将新的ViewHolder放入index = 1的位置(原index = 1顺位到index = 0位置),如果此时直接在mCacheViews中取视图是可以直接展示,不需要重新绑定的,一旦进入到了RecyclerViewPool中被初始化了,就需要重新绑定了

复用流程:

首先会通过getChangedScrapViewForPosition方法从mChangedScrap中寻找,如果没找到通过getScrapOrHiddenOrCachedHolderForPosition方法从mAttachedScrap中查找,如果没找到会从ChildHelper类中的mHiddenViews中查找(LayoutManager中会动态添加一组消失的视图),如果未找到则从mCahceViews中寻找,如果还未找到则从RecycleViewPool中寻找。最后还未找到的话则调用Adapter.CreateViewHolder创建

Glide源码

加载请求发送到了哪里?

请求发送进去的时候,存在2个队列WeakHashMap,一个正在执行队列,一个等待队列,新接收的请求会直接存入运行队列,如果请求request处于暂定状态的话,会存入等待列表,反之直接调用begin进行运行

请求是怎么被处理的?

调用bejin()方法后,会优先从内存中读取对应的缓存,如果不存在则去磁盘中进行查找,都没有的话,会执行SingleRequest,进行网络获取(网络工具使用是,HttpUrlConnection,会得到一个InptStream流),根据获取的到资源进行新的缓存与展示

怎么维护的?

通过生命周期进行请求的处理与回收(内部会使用Fragment进行生命周期的管理,如果不存在则会重新去创建一个带固定tag的Fragment,并且该Fragment是无UI的),内部request会在使用的时候优先从内存池中获取,如果不存在则重新创建,在回收时,会将其初始化后放入内存池中供下次使用

Glide与Picasso的区别,Glide的优势

  1. 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
  2. Glide加载图片的格式占用内存小(Glide:RGB_565,Picasso:ARGB_8888
  3. 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
  4. 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
  5. 高效的缓存策略(活动内存、内存、磁盘),灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小

Glide内存管理体现在哪?

LRU算法:根据时间戳进行排序,最新的在最上层,有新的会去回收掉最旧的那个,新的放在最上面

  1. 资源缓存在活动内存ActiveResourceCache中(缓存当前正在使用的资源(注意是弱引用)),可以直接拿来使用

  2. 资源缓存在内存LruResourceCache中(缓存最近使用过但是当前未使用的资源,基于LRU算法

  3. 缓存在磁盘中BitmapPool中(缓存所有被释放的图片,内存复用,基于LRU算法

    注意:

    • LruResourceCache和ActiveResourceCache设计是为了尽可能的资源复用
    • BitmapPool的设计目的是为了尽可能的内存复用

资源加载流程?(三级缓存是如何实现的?)

参考:跟着源码学设计:Glide框架及源码解析

  • 当我们需要显示某个资源时,Glide会先去查找ActiveResourceCache,如果找不到资源则查找LruResourceCache,如果在LruResourceCache也找不到合适的资源,则会根据加载策略从硬盘或者网络加载资源
  • 获取数据后Glide会从BitmapPool中找寻合适的可供内存复用的废弃recycled bitmap(找不到则会重新创建bitmap对象),然后刷新bitmap的数据。
  • bitmap被转换封装为Resource缓存入ActiveResourceCacheRequest对象中,Request根据target获取resource中引用的bitmap并展示。
  • 当target的资源需要释放时,resource会根据缓存策略被缓存到LruResourceCache,同时ActiveResourceCache中的弱引用会被删除。如果,该资源不能缓存到LruResourceCache,则资源将被回收到磁盘缓存中。
  • 当需要回收内存时(比如系统内存不足或者生命周期结束),LruResourceCache将根据LRU算法回收一些resource到磁盘缓存
  • 磁盘缓存会根据LRU算法和缓存池的尺寸来释放一些老旧资源。当系统GC时,则会回收可回收的资源释放内存

注意:如果是根据网络进行的请求,则在网络请求成功之后,获得对应的图片流,然后会显示在对应的视图上,同时此时也会在磁盘中进行相应的缓存,因为已经显示在对应视图上了,ActiveResourceCache中也会存储一份

Glide为什么不担心内存泄漏?

在用户需要时进行缓存,不需要时进行合理的回收

  • 当系统内存不足时,LruResourceCache会根据LRU算法移除一些内存中的缓存资源到BitmapPool

  • 到BitmapPool会根据LRU算法移除一些资源为新缓存的提供位置

  • 当应用再次需要资源时,会优先复用到BitmapPool中的bitmap对象(复用其内存),只需刷新bitmap的像素数据 1)这样能有效地降低内存抖动; 2)由于很多情况下可以复用废弃bitmap的内存,因此避免了内存分配等造成的性能损耗,系统比较流畅 3)降低了系统GC的频率 4)LruResourceCache和BitmapPool中都是当前不在使用的资源,做整体的资源回收性能会更好。

页面间的生命周期是怎么实现的?

Glide会传递一个上下文context,Glide会在内部通过context创建一个无UI的Fragment,通过监听Fragment的生命周期来监听外部Activity的生命周期Activtiy与Fragment生命周期相互绑定

注意:Glide内部通过context创建时,会创建2个队列(分为v4,非v4),通过Tag去RequestManagerFragment对象去查找,如果没有找到RequestManagerRetriever(Glide管理Fragment的中间件,内部含有2个相同的队列,一个管理非v4-Fragment,一个管理v4-Fragment)中的 requestManagerFragment队列中查找(HashMap),如果都没有找到则创建一个新的无视图的Fragment,并添加到队列中去,为下次提供使用

Glide怎么做大图加载

对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等 首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中 所以这种情况的优化思路一般是局部加载,通过BitmapRegionDecoder来实现 这种情况下通常Glide只负责将图片下载下来,图片的加载由我们自定义的ImageView来实现

Glide异步加载线程池有多少个?

缓存一般有三级,内存缓存、硬盘、网络。

由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

Glide 必然也需要多个线程池,看下源码是不是这样

public final class GlideBuilder {
  //...
  private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
  private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
  //...
  private GlideExecutor animationExecutor; //动画线程池
复制代码

Glide使用了三个线程池,不考虑动画的话就是两个。

LiveData原理

具有生命周期感知能力,支持黏性事件,采用了观察者模式,某种程度上也可以用作事件总线。

LiveData 源码中主要用到的类:

  • Observer:观察者接口
  • LiveData:发送已经添加观察的逻辑都在其中
  • ObserverWrapper :抽象的观察者包装类,提供了mLastVersion 和判断以及更新观察者是否活跃的方法
  • LifecycleBoundleObserver:继承 ObserverWrapper,可以感知生命周期,会在页面活跃的时候更新观察者
  • AlwaysActiveObserver:继承 ObserverWrapper ,无法感知生命周期,可以在任意时刻接收到通知。

LiveData 怎么感知生命周期感知?需要取消注册吗?

调用 observe 方法时,会调用 owner.getLifecycle().addObserver 来达到感知生命周期的目的,不需要进行额外的注册,observe内部帮忙处理了remove跟add相关的场景

setValue 和 postValue 有什么区别

setValue 只能在主线程使用,而 postValue 不限制线程。本质上postValue最后使用的还是postValue,只不过在内部使用handler将数据分发到了主线程之后来进行调用

设置相同的值,订阅的观察者们会收到同样的值吗

会,LiveData内部只有判断调用时Version的判断,并没有判断值之间的差异性

粘性事件原理,怎么防止数据倒灌

  • 工作机制:每次改变LiveData数据都会对数据版本号加1,并触发版本号小于数据版本号的观察者监听,触发后观察者的版本号与数据版本号一致

  • 粘性事件:更新数据后,观察者再订阅,新注册的观察者版本号为-1小于数据版本号,所以注册时会触发一次数据监听。

  • 数据粘连:LiveData的激活状态标识,会在对应的LifecyOwner执行onStart后设置为true,执行onDestroy后设置为false,在未激活状态下无论发生多少次改变,只有最后一次数据会发送给观察者

  • 数据倒灌:由于LiveData的激活状态标识先变为false,再变为true,导致触发小于数据版本号的所有观察者的监听。

常见场景为:使用ViewModel持有LivaData,并在生命周期内创建监听对象,则在Activity由于屏幕翻转等配置变化引发onDestroy时,ViewModel不会执行clear,因此保留了内部的LiveData,而在生命周期内重新创建监听对象的版本号为-1,所以在onStart之后会触发观察者监听

常见场景为:使用ViewModel持有LivaData,并在生命周期内创建监听对象,则在Activity由于屏幕翻转等配置变化引发onDestroy时,ViewModel不会执行clear,因此保留了内部的LiveData,而在生命周期内重新创建监听对象的版本号为-1,所以在onStart之后会触发观察者监听

  • 解决方案(防止数据倒灌):
    1. 修改version,通过反射方式控制version的值,让version大于lastVersion
    2. 复写LiveData,控制observe分发机制

observeForever怎么用

LiveData 作为事件总线机制或者配置之类

AlwaysActiveObserver 不依赖生命周期了,所以不会像 LifecycleBoundObserver 在生命周期变为 DESTROYED 时调用 LiveData#removeObserver 从 LiveData#mObservers Map 中移除自身,所以我们在使用 LiveData#observeForever 时应在不需要的时候调用 LiveData#removeObserver ,否则可能会发生内存泄露呢

总结一下有几种情况 LiveData 会分发值

  1. 调用 setValue 和 postValue 并且 LifecycleOwner 处于活跃状态时
  2. LiveData 有值,并且处于活跃状态时,调用 LiveData#observe 订阅观察者
  3. LiveData 有新值,也就是 ObserverWrapper 的 mLastVersion 小于 LiveData 的 mVersion,LifecycleOwner 从不活跃状态转为活跃状态时

RxJava

Rx主要操作符

Android面试知识点总结(四)—— 框架原理/Android常用组件原理篇

所有的操作都是操作符,除了 Subscribe 每个操作符都会生成新的 Observable,每个操作符的左边就是上游(upstream),右边就是下游(downstream)

  • Map:Map 对上游的数据项进行简单的变换(映射),返回新的 ObservableMap。实现就是把下游的 Observer 包装成 MapObserver 订阅给上游

  • FlatMap:把上游的数据项 Map 化成 Observable,然后把这些 Observable flatten 拍平,这个拍平指的就是不保证顺序的 merge

    简单点说就是把上游的数据项都变换成 Observable,把这些 Observable 都订阅给下游,但是对于只发射一个值的 Observable 做一下特殊处理,对于并发操作也要进行处理,所以要复杂一点

    如果想要保证按顺序 merge 可以使用 concatMap 操作符

  • SubscribeOn 和 ObserveOn:RxJava 不会直接使用线程或线程池,而是使用 Scheduler 调度器和 SubscribeOnObserveOn 这两个操作符来完成异步操作

    • 常用的线程调度器有:

      • 适合执行计算密集型任务的 Schedulers.computation()

      • 适合执行 I/O 密集型任务的 Schedulers.io()

      • 适合串行任务的 Schedulers.single()

        如果不指定调度器的话,那么在哪个线程订阅subscribe()的,就在哪个线程上执行操作符的逻辑,就在哪个线程通知观察者

  • Mergemerge() 可以合并多个 Observable,其实就是把多个 Observable 变成集合后调用 flatMap()。所以 merge 是不保证顺序的

    如果想要按顺序合并多个 Observable,可以使用 concat() 如果不想因为其中一个 Observable 的错误导致流中断,可以使用 mergeDelayError() 等到所有数据项都发射完再发射和处理错误 一个 Observable 实例可以使用 mergeWith() 来 merge 另一个实例

  • Concatconcat() 可以将多个 Observable 按顺序拼接起来,一个 Observable 发射完了再发射下一个 Observable

    一个 Observable 实例可以使用 concatWith() 来 concat 其他的实例

  • Zipzip() 可以按顺序压缩所有 Observable 的第 i 个数据项。最少的那个 Observable 发射完了就算完成了,其他 Observable 可能会被马上 dispose 并且接收不到 complete 的回调doOnComplete()

    一个 Observable 实例可以使用 zipWith() 来 zip 其他的实例

  • Timer、Interval、Delaytimer() 固定的时间延迟后发射,Interval固定的时间间隔发射,Delay发射每个数据前都延迟固定时间后再发射

  • SkipUntil 和 SkipWhile

    • skipUntil()让源Observable放弃发射数据,直到给定的Observable发射了数据它才可以正常发射数据
    • skipWhile()让源Observable放弃发射数据,直到给定条件变成false
  • TakeUntil 和 TakeWhile

    • takeUntil() 让源 Observable 在给定的 Observable 发射了数据后马上 complete
    • takeWhile()会镜像发射源Observable的数据,直到给定条件变成false时马上 complete
  • Catch

    • Catch 操作符可以拦截 onError 通知,并且可以把它替换成数据项或数据项序列
      • onErrorReturn()Observable 遇到 error 时不调用 ObserveronError() 方法,而是发射一个给定的数据项,然后 complete
      • onErrorResumeNext()Observable 遇到 error 时不调用 ObserveronError() 方法,而是将控制权交给给定的 Observable
      • onErrorComplete()Observable 遇到 error 时不调用 ObserveronError() 方法,错误通知被丢弃并直接 complete
  • Retry:Retry 操作符可以在 onError 时重新订阅(也就是重试)

EventBus原理

观察者模式又可称为发布 - 订阅模式,它定义了对象间的一种一对多的依赖关系,每当这个对象的状态改变时,其它的对象都会接收到通知并被自动更新。

  • 观察者模式有以下角色:

    • 抽象被观察者:将所有已注册的观察者对象保存在一个集合中。

    • 具体被观察者:当内部状态发生变化时,将会通知所有已注册的观察者。

  • 抽象观察者:定义了一个更新接口,当被观察者状态改变时更新自己。

    • 具体观察者:实现抽象观察者的更新接口。

为什么要使用事件总线机制来替代广播呢

  1. 广播:耗时、容易被捕获,不安全
  2. 事件总线:更节省资源、更高效、能将信息传递给原生以外的各种对象

对于事件总线EventBus而言,它的优缺点?

  1. 优点:开销小、代码更优雅、简洁、解耦发送者和接受者,可动态设置事件处理线程和优先级
  2. 缺点:每个事件必须自定义一个事件类,增加维护成本

在得知了EventBus的原理之后,我们注意到,每次我们在register之后,都必须进行一次unregister,这是为什么呢?

因为register是强引用,它会让对象无法得到内存回收,导致内存泄露。所以必须在unregister方法中释放对象所占的内存

EventBus2.x的版本,那么它又与EventBus3.x的版本有哪些区别呢?

  1. EventBus2.x使用的是运行时注解,它采用了反射的方式对整个注册的类的所有方法进行扫描来完成注册,因而会对性能有一定影响。
  2. EventBus3.x使用的是编译时注解,Java文件会编译成.class文件,再对class文件进行打包等一系列处理。在编译成.class文件时,EventBus会使用EventBusAnnotationProcessor注解处理器读取@Subscribe()注解并解析、处理其中的信息,然后生成Java类来保存所有订阅者的订阅信息。这样就创建出了对文件或类的索引关系,并将其编入到apk中。
  3. EventBus3.0开始使用了对象池缓存减少了创建对象的开销。
  • 除了EventBus,其实现在比较流行的事件总线还有RxBus,它与EventBus相比又如何呢?RxBus是基于RxJava开源基础上的

    1. RxJava的Observable有onError、onComplete等状态回调

    2. Rxjava使用组合而非嵌套的方式,避免了回调地狱

    3. Rxjava的线程调度设计的更加优秀,更简单易用

    4. Rxjava可使用多种操作符来进行链式调用来实现复杂的逻辑

    5. Rxjava的信息效率高于EventBus2.x,低于EventBus3.x

      如果项目中使用了RxJava,则使用RxBus,否则使用EventBus3.x

EventBus.getDefault()分析

内部通过双重判断的方式创建一个EventBus对象

在EventBus的构造方法中,创建一个保存注册对象和订阅信息对象(包含:注解订阅方法集,event类型等)的队列(HashMap),同时也创建了一个队列用于储存被注册过的对象,还会创建一个保存粘连性注册对象的队列

EventBus.getDefault().register(this)分析

通过注册的对象,讲该对象超类中以及自身存在的所有订阅过的方法进行存储,并且还会处理相关粘连性时间的发送

  1. 先从FindState池中取对象,如果不存在就创建一个并且放入其中,然后根据FindState对象获取对应订阅的消息集合
  2. 对于订阅方法的查找,优先使用索引查找,当找不到索引类时,继续使用反射的方式查找

EventBus.getDefault().post(Object())

ThreadMode执行线程
POSTING在发送事件的线程中执行直接调用消息接收方
MAIN在主线程中执行如果事件就是在主线程发送的,则直接调用消息接收方,否则通过 mainThreadPoster 进行处理
MAIN_ORDERED在主线程中按顺序执行通过 mainThreadPoster 进行处理,以此保证消息处理的有序性
BACKGROUND在后台线程中按顺序执行如果事件是在主线程发送的,则提交给 backgroundPoster 处理,否则直接调用消息接收方
ASYNC提交给空闲的后台线程执行将消息提交到 asyncPoster 进行处理

拿到在register方法中的订阅信息队列,通过反射的方式method.invoke进行数据的分发工作,如果存在不同的线程,会通过handler进行相关线程的切换

EventBus.getDefault().unregister(this)

  1. 移除该订阅对象
  2. 移除该订阅对象下对应的订阅方法和时间

EventBus.getDefault.postSticky(Object())

将注册粘连性事件的注册对象添加到对应的粘连性时间队列中,最后通过post进行事件发送

提问

1.有这么一种情况:A类实现了B接口,Test1Activity注册了EventBus,并且其中test方法被Subscribe注释

问题一:如果方法的参数为A类,Test2Activity中进行了post操作,事件类型为B,Test1Activity的test方法可以接收到事件吗

接收不到事件

问题二:如果方法的参数为B类,Test2Activity中进行了post操作,事件类型为A,Test1Activity的test方法可以接收到事件吗

可以接收到事件

发送方的数据类型需要是接收方的子集或本身,否则无法接收到

原理:在post()事件时,会调用lookupAllEventTypes()方法,去查找注册过的当前事件类型和所有父类类型、接口类型的观察者并进行触发

2.一个父类Parent,其中test方法被Subscribe注解,Son继承了Parent并且重写了test方法,请问如果Son被注册,如果post事件后,Son可以接收到事件,那么Parent类可以接收到事件吗?

Son可以收到,Parent不可以收到,子类中重写了父类的注解方法,就不会再去查找父类中的注解方法,就只触发子类的重写方法不会去触发父类的方法

子类覆盖了父类的方法,父类的注册事件指向了子类,就会出现在子类中重写后,只有子类会进行触发;反之子类不对父类方法进行重写时,会触发父类的注册事件

3.描述一下EventBus是如何进行线程切换的?

继承Handler实现了HandlerPoster,本质上通过Handler进行相应的线程切换

LeakCanary原理

  1. 监测Activity / Fragment的生命周期的 onDestroy() 的调用
  2. 首先创建WeakReference包装对象(需要传入引用队列),然后将WeakReference缓存至集合(ReferenceQueue)中
  3. 若期间发生GC,WeakReference包装的对象不再被引用即会被回收,同时WeakReference自身加入引用队列。此时通过获取引用队列中的WeakReference,去移除WeakReference集合中的对应元素。若WeakReference集合还残留元素,则说明对应WeakReference没有加入引用队列,也意味着WeakReference没有被回收
  4. 切换子线程再最终判定。主动触发一次GC,等待100ms后,再次检查集合。若仍发现对象被引用未被释放,则判定这些缓存集合中的对象存在内存泄漏,将进行dump heap操作
  5. 发生内存泄露之后,dump heap生成hprof文件并解析文件,生成泄漏引用链 (依赖另一个专门分析hprof的库来解析文件和生成分析结果。在1.X是依赖haha库,2.X改成依赖shark库)

setContentView()原理

Activity 与 AppCompatActivity的区别

AppCompatActivity最终是继承至Activity,AppcompaActivity带ActionBar标题栏,Activity 则不带,AppCompatActivity为兼容方式

AppCompatActivity -> FragmentActivity -> ComponentActivity -> Activity

  1. Activity 和 AppCompatActivity 加载布局前都会创建一个 DecorView,并将系统布局加载到 DecorView 中,通过 DecorView 找到 id 为 android.id.content 的FrameLayout,最后通过 LayoutInflater 加载我们的 xml 布局。
  2. Activity 没有设置Factory ,AppCompatActivity 设置了 Factory。
  3. Activity 不会拦截 View,而 AppCompatActivity 会拦截 View,并将部分 View 转换成对应的 AppCompatView。

ThreadLocal、AsyncTask原理

ThreadLocal

使用一个map保存所有线程的局部数据,map的key是线程的id(此处的id就是对应Thread中包含的ThreadLocalMap对象),value就是所要存储的数据

每一个Thread对象内部会存在一个ThreadLocalMap引用(默认是null),然后ThreadLocal通过create()的方式将当前的ThreadLoaclMap存储到对应的Thread中

ThreadLoaclMap在初始创建时,以ThreadLoacl为key,value为空创建一个相关对象

AsyncTask

AsyncTask是线程池和Handler的封装

关于线程池,AsyncTask内置有两个线程池:

  • THREAD_POOL_EXECUTOR(ThreadPoolExecutor)

    核心线程数CORE_POOL_SIZE = 1,最大线程数MAXIMUM_POOL_SIZE = 20

  • SERIAL_EXECUTOR(SerialExecutor)

    • 基于第一个线程池THREAD_POOL_EXECUTOR的二次包装,并且加上了同步锁,保证每次我们new AsyncTask并调用execute()时执行的任务是串行的,以及保证操作一些共享变量时线程安全

      因为AsyncTask调用execute方法,默认使用SerialExecutor这个线程池,SerialExecutor通过同步关键词synchronized来保证线程执行任务队列里头的任务是串行的

  • 关于AsyncTask的使用,还有需要注意的就是容易导致内存泄露的情况:

    1. 非静态内部类引用了外部变量导致的内存泄露

      如果用到了内部类,给内部类添加静态修饰符即可

    2. 未执行的AsyncTask导致的内存泄露 需要在界面的生命周期结束的时候,设置任务取消标记

  • 内部线程的切换机制

    • 通过外部传入Handler、Looper或者自身根据主线程Looper创建一个Handler,通过Handler进行线程间的切换

线程池的基本原理

线程池具体的参数说明 - okHttp线程池

  • 在开发过程中,合理地使用线程池能够带来3个好处。

    1. 降低资源消耗。
    2. 提高响应速度
    3. 提高线程的可管理性。
  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。

  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。

转载自:https://juejin.cn/post/7154310311850278919
评论
请登录