面试总结-Android

2,327 阅读32分钟

面试总结-Java基础

面试总结-Java多线程

面试总结-Kotlin

面试总结-Android

service两种启动模式的区别

Service有两种启动方式,分别是startService和binderService。

  1. startService方式启动服务,调用者和服务之间没关联,即使调用者退出了,服务也还是会继续运行。多次调用startService() 并不会创建多个服务,但是onStartCommand()会被多次调用,Service会经历 onCreate -> onStart,当调用 stopService或者Service 自己调用 stopSelf()方法时,Service会直接执行 onDestory()。
  2. bindService方式启动服务,调用者和服务之前绑定在一起,生命周期一致。即调用者退出了,服务也就停止了。通过bindService,Service会经历onCreate->onBind,这时候服务的调用者与服务绑定在一起。当调用者退出了,Service就会调用 onUnbind -> onDestoryed,也就是绑定之后一起共存亡
  3. bindService方式启动服务,调用者与服务之间绑定在一起,即调用者退出了,服务也就停止了。通过bindService,Service会经历onCreate-onBind,这个时候服务的调用者与服务绑定在一起。当调用者退出了,Service就会调用onUnbind->onDestoryed,所谓绑定在一起就共存亡。

什么是IntentSerivce?

Service默认运行在UI线程中,所以里面不能做耗时操作,要做耗时操作就必须开启一个子线程操作。

IntenService 其实是继承自Service,只不过内部默认开启了一个子线程处理所有的Intent请求。

而多次调用 startService 只会执行onStartCommand方法,但是启动线程的动作友只在 onCreate 中,所以并不会再次启动新的线程,每次执行 onStartService 时会通过 Message 的 sendMessage 发送一条消息到消息队列中。也就是说通过唯一的一个子线程来处理各个请求,并按照 Looper 的队列顺序一个接一个执行。

view的绘制流程

我们都知道,我们自定义View的时候需要实现三个方法,分别是:onMeasure(测量)、onLayout(布局)、onDraw(绘制)。

  • onMeasure(测量):主要用于确定 View 的宽和高。
  • onLayout(布局):主要用于确定 View 在父容器中放置的位置。
  • onDraw(绘制):根据前两步确定好View,通过onDraw将View绘制到屏幕上 在Android中,主要有两种视图,分别是:View和ViewGroup。 View是一个独立的视图,该容器可容纳多个子视图,即 ViewGroup 可以容纳多个View 和 ViewGroup。

此时我们会想到一个问题: View到底是如何测量的,如何布局的,最终是如何绘制到界面上的?

我们需要从底层知道自定义View的时候是如何触发这三个函数的以及是如何管理的,由谁来管理的。

我们知道,我们所有的界面显示都是在Activity上进行的,每个Activity都对应一个窗口Window,这个窗口在Android里就是PhoneWindow

image.png

image.png

image.png

Glide内部原理

image.png

Glide加载一个一兆的图片(100 * 100),是否会压缩后再加载,放到一个300 * 300的view上会怎样,800*800呢,图片会很模糊,怎么处理?

当我们调整imageview的大小时,Picasso会不管imageview大小是什么,总是直接缓存整张图片,而Glide就不一样了,它会为每个不同尺寸的Imageview缓存一张图片,也就是说不管你的这张图片有没有加载过,只要imageview的尺寸不一样,那么Glide就会重新加载一次,这时候,它会在加载的imageview之前从网络上重新下载,然后再缓存。

举个例子,如果一个页面的imageview是300 * 300像素,而另一个页面中的imageview是100 * 100像素,这时候想要让两个imageview像是同一张图片,那么Glide需要下载两次图片,并且缓存两张图片。

复制代码
public <R> LoadStatus load() {
    // 根据请求参数得到缓存的键
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);
}

看到了吧,缓存Key的生成条件之一就是控件的长宽。

简单说一下内存泄漏的场景,如果在一个页面中使用Glide加载了一张图片,图片正在获取中,如果突然关闭页面,这个页面会造成内存泄漏吗?

因为Glide 在加载资源的时候,如果是在 Activity、Fragment 这一类有生命周期的组件上进行的话,会创建一个透明的 RequestManagerFragment 加入到FragmentManager 之中,感知生命周期,当 Activity、Fragment 等组件进入不可见,或者已经销毁的时候,Glide 会停止加载资源。

但是如果,是在非生命周期的组件上进行时,会采用Application 的生命周期贯穿整个应用,所以 applicationManager 只有在应用程序关闭的时候终止加载。

如何设计一个大图加载框架?

image.png

GLide如何感知生命周期?

主要是通过 RequestManager 类和 Lifecycle 接口来实现的。

  1. RequestManager

    • 在 Glide 中,RequestManager 负责管理图片加载请求。我们可以通过 Glide.with() 方法获取一个 RequestManager 实例。
    • 当你使用 Glide.with() 方法时,你可以传入一个 ActivityFragmentView 作为参数,Glide 会根据传入的参数自动感知对应组件的生命周期。
  2. Lifecycle: Glide 通过监听传入的 ActivityFragmentView 对象的生命周期回调方法来感知生命周期的变化。 在 RequestManager 中,通过 Lifecycle 接口注册对应的生命周期监听器,以便在生命周期发生变化时执行相应的操作。

  3. 生命周期回调: 一旦 RequestManager 注册了生命周期监听器,它会在传入的 ActivityFragmentView 对象的生命周期发生变化时,接收到相应的生命周期回调。

    • Glide 利用这些生命周期回调方法,例如 onStart()onStop(),来执行图片加载请求的开始和取消操作。

通过这种机制,Glide 能够感知到关联组件的生命周期变化,并根据生命周期的状态来启动、暂停或取消对应的图片加载请求,从而确保了图片加载操作与组件生命周期的同步,避免了潜在的内存泄漏和资源浪费问题。

Glide 感知生命周期的目的是什么?

Glide 感知生命周期的主要目的是确保在图片加载过程中,及时地根据组件的生命周期状态进行加载操作的开始和取消,以提高应用的性能和用户体验,具体目的包括:

  1. 优化资源利用: 在 Android 应用中,图片加载是一个可能消耗大量资源的操作。通过感知生命周期,Glide 可以在适当的时候启动和取消图片加载请求,避免不必要的资源浪费。

  2. 避免内存泄漏: 如果在 Activity 或 Fragment 销毁后仍然存在未取消的图片加载请求,可能会导致内存泄漏。通过及时取消未完成的请求,可以避免这种问题的发生。

  3. 减少网络流量: 在用户退出应用或切换页面时,如果仍然在后台进行图片加载请求,可能会消耗用户的流量。通过感知生命周期,可以在合适的时候取消这些请求,减少不必要的网络流量。

  4. 提高用户体验: 及时启动图片加载请求可以确保图片能够在用户需要时立即显示,提高了用户体验。同时,在用户切换页面或退出应用时,及时取消图片加载请求可以加快页面切换和应用退出的速度,减少卡顿和等待时间。

综上所述,Glide 感知生命周期的目的是为了提高应用的性能和用户体验,确保在图片加载过程中,能够及时地根据组件的生命周期状态进行加载操作的开始和取消,从而避免不必要的资源消耗和性能问题。

Handler原理

Handler很重要,它和Binder进程间通信可以说是组成Android最重要的两部分。为什么说Hanlder很重要,我觉着主要有以下三个方面:

  • 1: 我们做Android开发的都知道,经常要做一些数据加载,比如:网络请求,文件读取,图片加载等,加载完的数据要返回到主线程更新UI。
  • 2: 很多第三方框架内部都是或多或少的有Hanlder的影子,比如我们常用的Glide图片加载库,Retrofit请求网络,Rxjava的线程切换以及kotlin协程线程切换。
  • 3: Android启动一个APP进程后,App就一直不会退出去,这其实和android的AMS的管理有关系,其实在这里面围绕着让我们Android系统一直能运行,其实就是一个“心跳机制”,这个“心跳机制”其实就是Handler来维持的。这个Handler里维持了一个自己的Looper,而这个Looper就是一个死循环,这个死循环是一直不会退出去的,这其实就是我们的“心跳机制”。

image.png

Handler 是标准的事件驱动模型,是由Handler,Message,MessageQueue和Looper四个组件组成,存在一个消息队列 MessageQueue,它是一个基于消息触发时间的优先级队列,还有一个基于此消息队列的事件循环 Looper,Looper 通过循环,不断的从 MessageQueue 中取出待处理的 Message,再交由对应的事件处理器 Handler/callback 来处理。

其中 MessageQueue 被 Looper 管理,Looper 在构造时同步会创建 MessageQueue,并利用 ThreadLocal 这种 TLS,将其与当前线程绑定。而 App 的主线程在启动时,已经构造并准备好主线程的 Looper 对象,开发者只需要直接使用即可。

Handler 类中封装了大部分「Handler 机制」对外的操作接口,可以通过它的 send/post 相关的方法,向消息队列 MessageQueue 中插入一条 Message。在 Looper 循环中,又会不断的从 MessageQueue 取出下一条待处理的 Message 进行处理。

IdleHandler 使用相关的逻辑,就在 MessageQueue 取消息的 next() 方法中。

IdleHandler 是什么?怎么用?

IdleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。

IdleHandler 被定义在 MessageQueue 中,它是一个接口。

// MessageQueue.java
public static interface IdleHandler {
    boolean queueIdle();
}

既然 IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么何时出现空闲?

MessageQueue 是一个基于消息触发时间的优先级队列,所以队列出现空闲存在两种场景。

  1. MessageQueue 为空,没有消息;
  2. MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行;

一些面试问题:

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;

Retrofit内部原理

Retrofit并不是网络请求框架,严格说只是对网络请求的一种封装,我们只需要定义一个接口类,在请求方法上加上相应的注解,甚至都不需要实现,就可以实现网络请求

Retrofit最重要的就是内部的动态代理模式

静态代理: Java中的静态代理要求代理类(ProxySubject)和委托类(RealSubject)都实现同一个接口(Subject)。静态代理中代理类在编译期就已经确定,而动态代理则是JVM运行时动态生成,但是静态代理代码冗余大,一旦需要修改接口,代理类和委托类都需要修改。这也是静态代理的缺陷。

举个例子:假如我有一套房子需要出售,我(真实对象)需要将房子委托给中介公司(代理对象),中介来帮助我出售房子(抽象对象)

动态代理: -JDK自带的java.lang.reflect.Proxy,只能代理接口类 我们的类一般由Java源文件编译出Java字节码.class文件,然后经过类加载器ClassLoader加载使用。

我(真实对象)要出售房子,委托给某个中介公司(代理对象),出售房屋 由于市场行情不行,出售了一段时间发现市场反馈不行,那我的房子也不能一直放着, 此时想到可以在出售的同时也出租,这样还能收取租金。两全其美 出售:中介公司1(代理对象1) 出租:中介公司2(代理对象2)

  •      出租和出售都有共同点:1:平台上传房屋信息,2: 中介带客户上门带看
         不同点:模块变化部分通过占位符代替
    
//抽象主题(房屋出售)
interface ISaleHouse {
    void sale();
}
//抽象主题(房屋出租)
interface IHireHouse {
    void hire();
}

//真实主题(出售房子)
public class SaleAgent implements ISaleHouse {
    @Override
    public void sale() {
        System.out.println("我要出售房屋");
    }
}
//真实主题(出租房子)
public class SaleHire implements IHireHouse {
    @Override
    public void hire() {
        System.out.println("我要出租房屋");
    }
}
//动态代理
public class ProxyHandler implements InvocationHandler {
    private Object obj;
    ProxyHandler(Object target) {
        this.obj = target;
    }
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        System.out.println("平台上传房屋信息"); //公共方法
        Object object = method.invoke(obj, objects);
        System.out.println("中介带客户看房子"); //公共方法
        return object;
    }
}

void proxy() {
    ProxyHandler proxyHandler = new ProxyHandler(new SaleAgent());
    ISaleHouse saleHouse = (ISaleHouse) Proxy.newProxyInstance(ISaleHouse.class.getClassLoader(), new Class[]{ISaleHouse.class}, proxyHandler);
    saleHouse.sale();

    ProxyHandler proxyHandler1 = new ProxyHandler(new SaleHire());
    IHireHouse hireHouse = (IHireHouse) Proxy.newProxyInstance(IHireHouse.class.getClassLoader(), new Class[]{IHireHouse.class}, proxyHandler1);
    hireHouse.hire();
}

public static void main(String[] args) {
    DynamicProxyTest dynamicProxyTest = new DynamicProxyTest();
    dynamicProxyTest.proxy();
}

回到Retrofit

查看Retrofit源码我们可以发现,Retrofit通过Builder创建Retrofit对象,在create方法中,通过JDK动态代理的方式,生成实现类,在调用接口方法时,会触发InvocationHandler的invoke方法,将接口的空方法转换成ServiceMethid, 然后生成okhttp请求,通过callAdapterFactory找到对应的执行器,比如RxJava2CallAdapterFactory,最后通过ConverterFactory将返回数据解析成JavaBena,使用者只需要关心请求参数,内部实现由retrofit封装完成,底层请求还是基于okhttp实现的。

Rxjava内部原理

我们知道 Rxjava 是一种链式调用的方式实现我们的一次网络请求

Observable
  .create(...) // 在 io 调度器上执行
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(...) // 在 Android 主线程上执行

Rxjava 通过 subscribeOn 实现切换到工作线程,创建一个ObservableObserveOn对象。会执行subscribeActual方法

image.png

Android Binder进程间通信(C/S架构)

https://juejin.cn/post/7242227037241737274
https://juejin.cn/post/7248853134785052709

Android内存泄漏和优化

内存泄漏指的是应用程序中存在一些对象或者资源无法被垃圾回收器回收,导致内存占用不断增加,最终导致设备性能下降。

内存泄漏的原因

对象未被正确回收

单例模式导致的内存泄漏

Handler 导致的内存泄漏

如果在使用Handler时,未正确处理消息队列和对外部类弱引用,可能导致外部类无法被回收。

Context 的错误引用

在Android开发中,Context引用是非常常见的内存泄漏原因。当将一个长生命周期的对象与Context关联时,如果未正确解除引用,将导致Context无法被回收。

LeakCanary原理

关于LeakCanary的原理,官网上已经给出了详细的解释。翻译过来就是:

  1. LeakCanary使用ObjectWatcher来监控Android的生命周期。当Activity和Fragment被destroy以后,这些引用被传给ObjectWatcher以WeakReference的形式引用着。如果gc完5秒钟以后这些引用还没有被清除掉,那就是内存泄露了。
  2. 当被泄露掉的对象达到一个阈值,LeakCanary就会把java的堆栈信息dump到.hprof文件中。
  3. LeakCanary用Shark库来解析.hprof文件,找到无法被清理的引用的引用栈,然后再根据对Android系统的知识来判定是哪个实例导致的泄露。
  4. 通过泄露信息,LeakCanary会将一条完整的引用链缩减到一个小的引用链,其余的因为这个小的引用链导致的泄露链都会被聚合在一起。

Android ANR

juejin.cn/post/684490…

ANR:全称:application no response(应用程序无响应),Android设计ANR的用意主要是系统通过与之交互的组件,比如 Activity,Service以及用户的操作(触摸、滑动、点击)对它们进行监控,用于判断我们的应用进程是不是存在卡死或者是响应过慢的一个状态,这其实是很多应用当中一种WatchDog(看门狗)的一种设计,当我们的应用程序发生了ANR的时候,我们的应用程序会为我们采集内存、CPU等一些信息,同时会在我们的/Data/ANR目录下生成对应的Trace文件。而我们要去分析ANR的原因以及解决问题,那我们就需要结合这个Trace文件,分析Trace文件来分析原因。

我们在开发过程中出现了各种各样的问题,我们可以通过异常信息或者日志快速区跟踪问题发生的原因,但是当我们的应用上线后,这些问题就很难监控了,那我们的应用程序线上发生ANR时,我们要如何监控呢?我们可以监听我们Trace文件的改变(Android 6.0之前),6.0以后我们业界有两种方式监听ANR。

第一种就是WatchDog(看门狗的方式监听),比如:微信的开源的性能监测工具Marchs,它其实就是利用了这种思想,它是怎么做的呢:当我们要去执行Handler的message的消息发送时,我们的activity,fragment,onCreate,生命周期等等都是通过Handler来驱动的,所以当我们在执行一个Message之前,我们可以使用一个字线程的Handler发送一个延迟消息(比如延迟15秒),当我们执行完了字线程的Handler消息时,我们可以把子线程的Hanlder消息remove掉。如果说我们的主线程的Message执行超时或耗时超过了我们设定的15秒的话,字线程的Handler就会执行对应的Message,此时我们就可以通过子线程的Hanlder判断我们的应用程序可能发生了ANR。此时我们就可以通过ActivityManagerService获取到当前应用进程的一个错误状态,具体的方式就是:ActivityMangerService.getProcessErrorState(),来获取应用进程的一个错误状态,进一步去判断应用程序是否发生了ANR。

第二种就是通过 信号机制 来监听的,信号机制类似于Android的广播,当我们在ANR发生的时候,我们的操作系统会给我们发生一个信号。我们Android系统在接到底层发出这个信号时,就会判断发生了ANR,去采集ANR的信息,那我们也可以通过这种方式来监控,爱奇艺的性能监控工具(xCrash)就是通过这种方式实现的。这块主要是通过C/C++的代码,去进行信号的注册,当我们的ANR的信号发生时,就会执行我们注册的函数,用来监控。

APK 的打包流程

image.png

事件分发

  1. 什么是事件分发?

是指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数个MOVE事件。

image.png

  1. 事件分发的本质

将点击事件(MotionEvent)传递到某一个具体的View 处理的全过程。

即 事件传递的过程 = 事件分发过程

  1. 事件在哪些对象之间进行传递?

Activity、ViewGroup、ViewAndroidUI界面由ActivityViewGroupView 及其派生类组成

即:1个点击事件发生后,事件先传到`Activity`、再传到`ViewGroup`、最终到 `View`

4. 事件分发过程由哪些方法协作完成?

由三种方式协作完成:dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent()

image.png

  1. 事件分发流程

Android事件分发流程 = Activity -> ViewGroup -> View

即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终到 View

image.png

即要想充分理解Android分发机制,本质上是要理解:

    1. Activity对点击事件的分发机制
    1. ViewGroup对点击事件的分发机制
    1. View对点击事件的分发机制

流程1。  Activity对点击事件的分发机制;当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent()),流程总结如下:

image.png

核心方法:dispatchTouchEvent 和 onTouchEvent总结如下:

image.png

流程2。  ViewGroup的事件分发机制

从上面 Activity 的事件分发机制可知,在 Activity.dipatchTouchEvent()实现了将事件从 Activity -> ViewGroup 的传递, ViewGroup 的事件分发机制从 dispatchTouchEvent() 开始。

image.png

核心方法:dispatchTouchEvent()onTouchEvent()onInterceptTouchEvent()总结如下:

image.png

流程3。  View的事件分发机制

从上面 ViewGroup 事件分发机制知道,View 的事件分发机制从 dispatchTouchEvent() 开始。流程总结如下:

image.png

这里需要特别注意的是,onTouch()的执行 先于 onClick()

核心方法:dispatchTouchEvent() 、 onTouchEvent() 总结如下:

image.png

Android事件分发工作流程-总结

Android事件分发流程 = Activity -> ViewGroup -> View,即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到View。

image.png

内存过小怎么办?内存泄漏如何分析?

内存过小解决办法:

  1. 关闭后台应用:在Android设备上,有许多应用程序会在后台运行,占用内存资源。通过关闭一些不必要的后台应用程序,可以释放内存资源,提高设备的运行效率。
  2. 清除缓存:应用程序在使用过程中会产生缓存文件,这些文件会占用一定的内存空间。定期清除应用程序的缓存文件,可以释放一些内存资源。
  3. 卸载不必要的应用程序:一些应用程序会占用大量的内存资源,如果不需要这些应用程序,可以考虑卸载以释放内存。
  4. 使用内存优化软件:市场上有一些内存优化软件可以帮助管理设备的内存资源,通过优化可以释放一些内存空间。
  5. 恢复出厂设置:如果设备的内存问题比较严重,可以考虑恢复出厂设置,这将删除设备上的所有数据和应用程序,从而释放大量的内存空间。

内存泄漏如何分析?

在Android开发中,内存泄漏是一个常见问题,可能会导致应用程序性能下降,甚至在严重情况下导致应用程序崩溃。以下是一些解决Android内存泄漏问题的步骤:

  1. 确定内存泄漏的位置: 使用Android Studio自带的 Android Profiler工具可以帮助你检测内存泄漏。这个工具会显示一个内存使用情况的图表,以及哪些对象占用了最多的内存。通过这个工具,你可以确定哪些对象占用了大量内存并且没有被垃圾回收。
  2. 分析内存泄漏的原因: 确定内存泄漏后,需要分析内存泄漏的原因。可能是由于某个对象持有对其他对象的引用,导致其他对象无法被垃圾回收。也可能是由于在Activity或Fragment中持有对Context的引用,导致Context无法被释放。
  3. 修复内存泄漏: 根据分析的结果,采取相应的措施修复内存泄漏。例如,如果某个对象持有对其他对象的引用,可以尝试减少对该对象的引用或者在合适的时候手动释放引用。如果是因为持有Context的引用导致的内存泄漏,可以考虑使用Application Context替代Activity Context。
  4. 测试修复: 修复内存泄漏后,需要再次运行Profile工具来测试内存泄漏是否已经解决。如果内存使用情况正常,并且垃圾回收器能够正常回收不再使用的对象,那么内存泄漏问题就解决了。
  5. 避免内存泄漏: 在开发过程中,应该采取一些措施来避免内存泄漏。例如,避免在Activity或Fragment中持有对Context的引用,避免在静态变量中持有对Activity的引用,以及在不再需要的时候手动释放对对象的引用。

LeakCanary是一个用于检测Android和Java应用程序内存泄漏的工具。它是由Square公司开发的,是一个开源库,可以帮助开发者在开发测试阶段更容易地发现内存泄漏的情况。

LeakCanary的工作原理是在不影响程序正常运行的情况下,动态收集程序存在的内存泄漏问题。它会在Activity的onDestroy方法执行时,对Activity创建一个带ReferenceQueue的弱引用,并检查这个引用是否被清除。如果没有被清除,就认为存在内存泄漏,然后通过另一个进程分析内存泄漏的信息并展示出来。

使用LeakCanary可以极大地方便Android应用程序的开发,因为开发者不需要每次在开发流程中都抽出专人来进行内存泄漏问题检测。将LeakCanary集成到自己的程序中也非常简单,只需要引入LeakCanary提供的jar包即可。一旦检测到内存泄漏,LeakCanary就会dump内存信息,并通过另一个进程分析内存泄漏的信息并展示出来。

界面卡顿怎么办?怎么分析?

Android开发中卡顿问题一直是个比较棘手又重要的问题,严重影响用户体验;卡顿是人的一种视觉感受,比如我们滑动界面时,如果滑动不流畅我们就会有卡顿的感觉,这种感觉我们需要有一个量化指标,在编程时如果开发的程序超过了这个指标我们认为其是卡顿的

开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms≈1000/60的时间来处理所有的任务;Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps

过度绘制会导致界面消耗性能,严重还会出现卡顿

我们在写RecyclerView的时候,如果RecyclerView的父布局、RecyclerView、item三者的背景只要其中一个设置就可以了,没有设置背景就不会渲染,否则就会有过度绘制的情况

● 父布局套子布局也是尽量只设置其中一个背景,除非没办法都需要背景

● 子view一般绘制后是会覆盖父view,所以一般选择把背景设置在子view

● 视图的层级结构能减少就减少,层级越多绘制速度越慢

● 尽量少设置view的透明度,如果一个view设置了alpha,那他需要知道下面的view是什么内容,再绘制自己,就是过度绘制。如果是文字有透明度,可以在色号里就设置好

移除没用的布局和控件,假设添加个背景,尽可能在已经布局上放,减少只有背景功能的控件

● 减少透明度的使用,假设:#55FFFFFF 和 #888888 颜色类似,建议使用后者,因为前者有Alpha,view需要至少绘制两次

● 去掉多余的不可见颜色背景、图片等,只保留最上层用户可见即可

● 减少布局层次结构,避免多层嵌套 约束布局 ConstraintLayout等父类布局

● 基本控件LinearLayout 性能比RelativeLayout高一些,要提前根据UI想好哪个布局更合适,要有的方式,对症下药

● 自定义View尽可能只更新渲染局部区域,杜绝不断全部重绘

● 推荐使用IDE自带的Lint或者阿里代码检查插件,对于标黄警告等提示重视起来,能改的就改

● 使用 Android Studio 自带的 Layout Inspector 层级检测工具,可以立体的查看界面的层级布局。

MVP 和 MVVM 有什么优点和缺点

MVP

MVP的特点

  • 1.Presenter完全将Model和View解耦,主要逻辑处于Presenter中。
  • 2.Presenter和具体View没有直接关联,通过定义好的接口进行交互。
  • 3.View变更时,可以保持Presenter不变(符合面向对象编程的特点)
  • 4.View只应该有简单的Set/Get方法、用户输入、界面展示的内容,此外没有更多内容。
  • 5.低耦合:Model和View的解耦,决定了该特性。

MVP的优点

  • 1.低耦合:Model、View层的变换不会影响到对方。
  • 2.可重用性:Model层可以用于多个View。比如请求影视数据,可能有多个页面都需要这个功能,但是Model层代码只要有一份就可以了。
  • 3.方便测试:可以单独对Model层和View层进行测试。

MVP的缺点

  • MVP的中使用了接口的方式去连接view层和presenter层,如果有一个逻辑很复杂的页面,接口会有很多,导致维护接口的成本非常大。解决办法:尽可能将一些通用的接口作为基类,其他的接口去继承。

  • Presenter 对 Activity 与 Fragment 的生命周期是无感知的,所以我们需要手动添加相应的生命周期方法,并进行特殊处理,以避免出现异常或内存泄露。

MVVM的优势:

  • MVVM架构的主要核心就是 ViewModelLiveData。ViewModel 的作用是保证当设备因配置改变而重新创建 FragmentActivity(目前 ViewModel 仅支持 FragmentActivity 和 Fragment) 时,数据也不会丢失。LiveData 的作用是保证只在 FragmentActivity 或 Fragment 的生命周期状态为 [onStarted, onResumed] 时,回调 onChanged(T data),所以我们可以在 onChanged() 中安全的更新 UI。

ANR如何分析?导致ANR的原因和解决办法

Android系统中,ActivityManagerService(简称AMS)WindowManagerService(简称WMS) 会检测App的响应时间,如果App在特定时间无法响应屏幕触摸或键盘输入时间,或者特定事件没有处理完毕,就会出现ANR。

造成ANR的原因及解决办法

上面例子只是由于简单的主线程耗时操作造成的ANR,造成ANR的原因还有很多:

  • 主线程阻塞或主线程数据读取

解决办法:避免死锁的出现,使用子线程来处理耗时操作或阻塞任务。尽量避免在主线程query provider、不要滥用SharePreferences

  • CPU满负荷,I/O阻塞

解决办法:文件读写或数据库操作放在子线程异步操作。

  • 内存不足

解决办法:AndroidManifest.xml文件中可以设置 android:largeHeap="true",以此增大App使用内存。不过不建议使用此法,从根本上防止内存泄漏,优化内存使用才是正道。

  • 各大组件ANR

各大组件生命周期中也应避免耗时操作,注意BroadcastReciever的onRecieve()、后台Service和ContentProvider也不要执行太长时间的任务。

如何分析?

    1. 使用Log分析法
    1. 使用Log日志生成的 traces.txt 文件分析,特别注意:产生新的ANR,原来的 traces.txt 文件会被覆盖。
    1. 使用三方检测工具分析,比如友盟统计,腾讯的 bugly 或者是 Testin

zhuanlan.zhihu.com/p/454400067…

AMS如何启动一个应用

从桌面点击一个图标后,到界面显示, 这个过程发生了什么?

image.png

启动流程:

① 点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;

② system_server进程接收到请求后,向zygote进程发送创建进程的请求;

③ Zygote进程fork出新的子进程,即App进程;

④ App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;

⑤ system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;

⑥ App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;

⑦ 主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

⑧ 到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

Android 应用启动方式主要有两种 , 冷启动热启动

  • 冷启动:后台没有应用进程 , 需要先创建进程 , 然后启动 Activity ;
  • 热启动:后台有应用进程 , 不创建进程 , 直接启动 Activity ;

Activity的启动模式,也就是常见的四种启动模式

  • 标准模式(standard)
  • 栈顶复用模式(singleTop)
  • 栈内复用模式(singleTask)
  • 单例模式(singleInstance)

1. standard

标准模式:  在清单文件中声明 Activity 时,如果不设置Activity的启动模式,系统会 默认 将其设置为standard。每次启动一个标准模式的Activity都会重新创建一个新的实例,不管这个Activity之前是否已经存在实例,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈,谁启动了这个Activity,那么这个Activity实例就运行在启动它的那个Activity所在的栈中 。根据上面所说,我们就了解了当用ApplicationContext启动或者在Service中直接调用startActivity启动标准模式Activity时报如下错误的原因了。

android.util.AndroidRuntimeException: Calling startActivity ``from` `outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is ``this` `really what you want

上面错误的原因是被启动的Activity是标准模式,而发起启动请求的 Context 不是 Activity的 Context,而是系统的 Context,所以系统的 Context 需要设置 FLAG_ACTIVITY_NEW_TASK 属性,来启动一个新的 Activity 栈。

2. singleTop

栈顶复用模式:  在这种模式下,如果新启动的 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,只会重新调用 onNewIntent 方法,这个Activity的onCreate、onStart都不会被系统调用。如果新Activity实例已经存在但不在栈顶,那么重新创建 Activity 并放入栈顶。

3. singleTask

栈内复用模式:  这是一种单实例模式,一个栈中同一个Activity只存在唯一一个实例,无论是否在栈顶,只要存在实例,都不会重新创建,和 singleTop 一样会重新调用 onNewIntent 方法。需要注意的是:如果一个Activity被设置为singleTask模式,那么当栈内已经存在该Activity实例时,再启动该Activity,会让该Activity实例之上的Activity被出栈。举个例子:有四个Activity 分别是 A、B、C和D,A是singleTask模式,当先执行A->B->C->D时,A在栈内已存在实例,此时再调用D->A启动A时,会让A实例之上的B、C、D都出栈。一般项目的MainActivity都设置为此模式,方便放回首页和清空中间Activity。

4. singleInstance

单实例模式:  这是一种加强的singleTask模式,它除了具有singleTask模式的所有特性外,还加强了一点,那就是此种模式的Activity只能单独地位于一个任务栈中,不同的应用去打开这个activity 共享公用的同一个activity。他会运行在自己单独、独立的任务栈里面,并且任务栈里面只有他一个实例存在。应用场景:呼叫来电界面。这种模式的使用情况比较罕见,在Launcher中可能使用。或者你确定你需要使Activity只有一个实例。

ViewModel面试点

Viewmodel是什么?

image.png

Viewmodel为什么被设计出来,解决了什么问题?

image.png

屏幕旋转导致Activity销毁重建,ViewModel是如何恢复数据的

当屏幕旋转或切换系统语言时,Activity生命周期会经历销毁再重建,但是ViewModel里面的变量值时不受影响的。说明ViewModel中的数据在此期间进行了存储,在之后又进行了恢复。

我们一般在Activity的onCreate中获取ViewModel的实例

val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

在ViewModelProvider中重点会调用 ensureViewModelStore,如下图:

ViewModelProvider 会调用 ensureViewModelStore(),该方法中有两个很重要的类:ViewModelStoreNonConfigurationInstances

ViewModelStore 是用来存储ViewModel对象的,记得内部应该是一个HashMap,用于缓存ViewModel的实例对象。

NonConfigurationInstances 实际是一个Java Bean类,内部存储了viewmodelStore。

具体的执行流程在.get(MainViewModel::class.java) 中,根据 Key 从 ViewModelStore获取缓存的ViewMode,如果存在,就返回ViewModel实例,如果不存在当前的Class实例,则用工厂方法创建一个,再将新创建的加入缓存。

屏幕旋转前后,mViewModelStore 在屏幕旋转前后都是同一个对象,会在 NonConfigurationInstances.onRetainNonConfigurationInstance()中保存数据。

屏幕旋转后,Activity重建后从 getLastNonConfigurationInstance() 中获取到了屏幕旋转前保存的 NonConfigurationInstances 实例对象,然后从nc对象中获取存储的mViewModelStore对象。