Android中高级开发工程师-面试记录-长期更新

这是一篇长文,长期不定期更新

(注:如果有不能分享的,请及时联系,我会及时处理,如果有冒犯的地方,请谅解~)

滴滴橙心优选一面:2021-05-11 20:00 线上面试

1、简要说一下四种启动模式及应用场景
2、如果一个Activity里面是一个webview,页面上面有一个最小化按钮和关闭按钮,这个最小化效果如何实现,做到再次打开不重新加载页面(提示利用启动模式)
3、Activity的生命周期
4、ActivityA启动ActivityB的完整生命周期、启动过程(AMS怎么知道要启动哪个Activity)
5、ActivityRecord你了解不,有什么作用?
6、线程A与线程B如何通信的,线程间通信原理
7、简单讲讲OkHttp发送网络请求的过程,都做了哪些事
8、我想要打印请求的一些信息以及返回的一些信息怎么处理 ?
9、你做过哪些性能优化相关的事情?
10、Bitmap的优化
11、共享屏幕:利用两个栈实现队列效果
12、任意实现一个排序算法

一个一个问题来总结一下:

1、简要说一下四种启动模式及应用场景

答:
1、standard:Activity默认的启动方式,每次启动都会往Activity栈中不断添加新的Activity实例。最经常使用的一种模式
2、singleTop:栈顶复用模式,当启动一个Activity时,如果Activity栈的栈顶不是这个Activity的实例,则创建新的实例加入栈中。主要应用于自己打开自己,例如:电商APP中,商品详情页面下面会有推荐商品,点击还是商品详情页面,此时使用singleTop
3、singleTask:栈内复用模式,当启动一个新的Activity时,如果目标Activity栈中不存在此Activity实例,则创建新的实例加入栈中。如果存在此Activity实例的话,将此实例上面的实例出栈,将自己置于栈顶并获取焦点。主要用于应用程序的主页
4、singleInstance:当启动一个新的Activity时,都会新建一个任务栈,自己独栈一个Activity栈,比较少用。实际应用中,我好像没用过这个,应用场景:在做支付时,我感觉调用支付宝微信支付的页面应该就属于这种启动模式,不知道是不是。

2、如果一个Activity里面是一个webview,页面上面有一个最小化按钮和关闭按钮,这个最小化效果如何实现,做到再次打开不重新加载页面(提示利用启动模式)

这个题没答上来,后来查了以后才知道其实挺简单的。
答:利用singleInstance+moveTaskToBack()方法就可以实现最小化效果,要真正关闭的话调用finish()方法。moveTaskToBack()方法需要传递一个nonRoot : Boolean类型的参数,意思是:是否是根Task,我试的时候没有什么区别,true和false的区别还需要再研究研究。这里还要再说一下,如果没有设置启动模式为singleInstance,调用moveTaskToBack()方法,效果就类似于用户按下了Home键的效果。

3、Activity的生命周期

答:Activity正常的生命周期:onCreate、onStart、onResume、onPause、onStop、onDestory。横竖屏切换时,需要看Activity有没有设置android:configChanges="",如果没有设置则会销毁后重新执行Activity的各个生命周期方法,如果设置了,则不会销毁Activity,不会执行各项生命周期,回调onConfigurationChanged方法。

4、ActivityA启动ActivityB的完整生命周期、启动过程(AMS怎么知道要启动哪个Activity)

ActivityA启动ActivityB时的生命周期:
A-onPause
B-onCreate
B-onStart
B-onResume
A-onStop

ActivityB返回ActivityA的生命周期:
B-onPause
A-onRestart
A-onStart
A-onResume
B-onStop
B-onDestory

Activity的启动过程:(AMS怎么知道你要启动的是哪个页面)
这个答的不好,重新总结一下:
答:Activity启动是通过startActivity方法进行的,最后会通过Instrumentation对象调用execStartActivity方法,Instrumentation用来管理applicaiton及Activity的生命周期。Instrumentation最终通过binder通信将信息给到AMS,AMS在startActivity时,AMS把启动Activity的功能实现转移到ATMS中,ATMS用于管理Activity及其容器(任务、堆栈、显示等)的系统服务。之后通过getActivityStartController().obtainStarter获取一个ActivityStarter实例,ActivityStarter通过startActivity时,会初始化一个ActivityRecord对象实例,它可以说是一个Activity,存储了Activity的所有信息。之后如果有需要还会创建一个TaskRecord(任务栈),加入到ActivityStack(Activity栈),之后将ActivityRecord加入到TaskRecord栈顶位置。AMS在初始化的时候会会创建一个ActivityStackSupervisor,维护了两个栈对象:mHomeStack、mFocusedStack(),管理着ActivityStack(Activity栈)、ActivityStack管理着TaskRecord任务栈、TaskRecord管理着ActivityRecord。
AMS就是通过ActivityStackSupervisor来直接或间接管理ActivityStack、TaskRecord、ActivityRecord,从ActivityRecord中就能得知要启动的Activity是哪一个,知道要启动的Activity是哪个包下的。
Instrumentation通过Binder与AMS通信,知道了要启动哪个页面,之后AMS又通过binder将启动的操作交给ActivityThread,之后执行scheduleTransaction方法,通过mH(Handler)来切换线程,最终执行handleLaunchActivity方法去启动Activity。真正启动方法是:performLaunchActivity方法,通过ActivityClientRecord获取ActivityInfo对象,实例话具体的Activity实例、ContextImpl实例等等,之后执行生命周期的一些方法。

5、ActivityRecord你了解不,有什么作用?

答:ActivityRecord是存储一个Activity所有信息的类,可以代表一个Activity。AMS在创建时,初始化了一个ActivityStackSupervisor实例,执行startActivityLocked方法时初始化的ActivityRecord对象,用于存储要启动的Activity的所有信息,如:调用者所在的进程、包名信息、启动来源的Activity包名、配置信息、Intent的信息等。

6、线程A与线程B如何通信的,线程间通信原理

答:线程A要往线程B发送消息,则需要在线程B中初始化一个Handler对象,在线程B初始化Handler对象前需要确保线程B中有Looper对象(Looper.prepare(),一个线程只能有一个Looper对象,多次执行Looper.prepare()方法会抛出异常),否则在初始化Handler时会抛出异常。在线程A中,持有线程B的Handler对象实例,使用handler.sendMessage(Message msg)方法,最终还是调用sendMessageAtTime(),将Mesaage对象存入MessageQueue消息队列中,MessageQueue消息队列是Looper.prepare方法初始化的。线程B中Looper.loop方法就会开启一个无限循环,不断的从MessageQueue消息队列中取出Message对象,Message对象中有一个属性target,其实就是对应的handler,调用msg.target.dispatchMessage()方法,最终就回调到handler的handlerMessage方法了,也就是线程B的handler的handlerMessage方法去处理消息了。整个线程间通信的流程就是这样的。具体源码分析过程可以看看我的另外一篇博客:Android面试基础

7、简单讲讲OkHttp发送网络请求的过程,都做了哪些事

答:OKHttp采用建造者模式来初始化OkHttpClient以及Request对象,OkHttpClient中包含有调度器(Dispatcher)、连接池(ConnectionPool)、拦截器(Interceptor)等等,Request对象里面包含有method、url、header、body等等,通过OkHttpClient对象调用newCall方法获取Call对象(实际上是RealCall对象),之后通过call对象进行同步/异步请求。

同步请求将任务加入到调度器(Dispatcher)的runningSyncCalls双端队列中,然后直接调用了getResponseWithInterceptorChain。
异步请求将请求加入到调度器(Dispatcher)中,经历两个阶段:readyAsyncCalls、runningAsyncCalls,之后调用getResponseWithInterceptorChain。

调度器中定义了两个并发执行数的变量,分别对并发做限制,最大同时执行数为64,同主机最大执行数为5。

任务执行是通过线程池进行的,OkHttpClient的线程池是用的CacheThreadPool,其特点是核心线程为0的无界线程池,任务到达后检查是否有可复用的线程,没有则创建新的线程去执行任务,线程空闲超时时间为60秒。
getResponseWithInterceptorChain方法是通过责任链模式来执行的一串拦截器链,拦截器总共有七个,两个自定义拦截器,五个内置拦截器。其执行顺序是:

  • 自定义拦截器,通过addInterceptor添加的自定义拦截器,可以用来添加header头等等操作
  • 重定向拦截器,会对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作
  • 桥拦截器,构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包
  • 缓存拦截器,主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
  • 连接拦截器,负责建立连接,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
  • network拦截器,自定义网络拦截器,这个用的比较少,添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试
  • 呼叫拦截器,前面的链接拦截器其实已经建立起了连接,这里就是负责读写数据,执行read、write的相关操作了
    至于这些拦截器、缓存之类的具体实现逻辑我记得不是很清楚了,有需要的时候再去看看源码,之前也写过一篇博客来记录这些,博客地址:OkHttp相关源码解读
8、我想要打印请求的一些信息以及返回的一些信息怎么处理 ?

答:这个我们可以利用上面所说的拦截器来实现,通过OkHttp的建造者模式构建OkHttpClient时的一个addInterceptor方法添加一个自定义拦截器,实现Interceptor的intercept方法,利用Chain对象可以获取到Request信息以及Response信息。

9、你做过哪些性能优化相关的事情?

答:性能优化包括的内容比较多,例如:App的启动优化、内存优化、稳定性优化、APK瘦身优化、操作流畅度优化等等。

  • APP启动优化上,之前通过阅读APP启动流程相关源码及一些博文了解到,APP启动最终会通过AMS通知ActivityThread去初始化Application、Activity,也就是能优化的点就是在Application的onCreate方法、attachBaseContext方法以及Activity的onCreate方法里面去做优化。我在项目中,如果非必须的及时初始化的SDK我就会使用阿里的Alpha框架来对第三方SDK进行初始化。同时,我记得是5.0以后,对Android方法数限制65536的问题所引入的MultiDex框架,它需要在attachBaseContext方法中进行install,那我们如果也需要加载额外的dex文件的话,非必须实时加载,也可以使用异步的方式去进行加载。至于Activity的onCreate方法中,最主要的就是setContentView方法了,加载的这个布局尽量不要有太多的嵌套,使用自定义View的方式或者ViewStub、include、merge的方式,减少层级嵌套,也就是对布局的一些优化。同时最好把系统默认的主题背景设置为空白,这样减少一次绘制背景的时间。
  • 内存优化:在我开发过程中,就尽量避免一些资源使用完成后未关闭资源连接,也养成注册的一些广播在销毁的时候取消注册等一些代码习惯的问题。然后针对Handler、静态变量的使用上也要比较注意,因为这些有可能导致内存泄漏,最终导致OOM的产生,比如Handler的问题,发送的Message可能无法及时处理,生命周期比Activity生命周期长的时候就造成内存泄漏,GC无法及时回收Activity,造成Activity的内存泄漏。还有一个也非常常用的webview页面,因为webview比较耗费内存,一般就采用指定单独进程去运行。在功能完成之后,时间充裕的情况下,我就会利用LeakCanary 这款性能检测工具去检测APP里面是不是有内存泄漏的地方,然后再去做优化。同时也会使用AndroidStudio里面自带的Profiler工具,查看当前内存、网络使用情况,然后根据具体情况去分析优化。
    (多说一句:如果遇到说Bitmap的内存优化的时候,就了解一下Glide、Picasso一些图片加载框架吧,Bitmap优化一般不需要自己处理了,已经有很多第三方框架帮我们处理的很好了,有轮子用就直接装上,只不过这个我们有时间的时候也要去了解一下这个轮子,要不然哪天翻车爆胎了束手无策就不行了。Glide的处理Bitmap的时候用到了BitmapPool池化的概念,以及内存缓存、磁盘缓存等等。)
  • 稳定性优化:这个我最主要是在Crash、ANR方面的优化,也不能算是优化吧,就是尽量减少Crash产生的概率,监控Crash产生的时间、地点(哪个模块哪个代码)、人物(什么机型),利用Thread.UncaughtExceptionHandler 接口来捕获全局异常,并记录到我们自己的服务器端,当然也会使用第三方如友盟的一些服务。利用这些信息来做一些优化。ANR方面,基本就是在开发阶段,如果产生ANR又不知道哪里出的问题的时候,可以去看看data/anr/目录下面的trace.txt文件,里面记录了一些ANR产生的信息。
  • APK瘦身优化:这个我主要是对图片上、资源文件上、还有一些不必要的so包,开启混淆等操作。图片现在Android支持了SVG,有SVG的资源最好就使用SVG的资源,相对来讲比较节省空间,webp图片格式也比jpg、png等格式的图片占用空间小。
  • 流畅度优化:一些页面要是层级嵌套太深我就会使用自定义组合控件的方式去做,或者使用inclue、merge、ViewStub等方式去写页面。然后现在准备再去研究一下Jetpack的 Compose框架,这个框架就类似Flutter,时一个申明式UI框架,听起来就挺厉害也挺唬人的,有时间的话,会再去看看这个最新的UI框架。除了这个UI方面的流程度,还有主线程的卡顿,Android是16ms刷新机制,不要在主线程中去做一些比较大的计算,要不然就会影响UI刷新造成卡顿的现象。
10、Bitmap的优化

答:Bitmap方面的优化就是压缩、bitmap复用等等。这里了解一点:Bitmap的内存不是由图片文件大小决定的,而是由图片长宽及单位像素所占字节数来确定的:长 * 宽 * 单位像素占用字节数。其他的就去了解一下Glide对Bitmap的优化。

11、利用两个栈实现队列效果

这里就知道栈是先进后出、队列是先进先出的结构。
这个题,当时没写出来,现在想想,挺简单的,先上代码,后面解读一下:

//Java版本:
import java.util.Stack;
public class Solution {

    private Stack stack1 = new Stack();
    private Stack stack2 = new Stack();

    public void push(T node){
        stack1.push(node);      //入栈还是全部都放在stack1中
    }

    public T pop(){
        if(stack1.size() > 0){
            //将stack1中的数据全部反向push到stack2中
            while (stack1.size() > 0){
                stack2.push(stack1.pop());
            }
            T node = (T) stack2.pop();
            //重新存回到stack1中
            while (stack2.size() > 0){
                stack1.push(stack2.pop());
            }
            return node;
        } else {
            return null;
        }
    }

    public int size(){
        return stack1.size();
    }
}

//Kotlin版本:
import java.util.Stack
class SolutionKt {

    private val stack1: Stack = Stack()
    private val stack2: Stack = Stack()

    fun push(node: T) {
        stack1.push(node) //入栈还是全部都放在stack1中
    }

    fun pop(): T? {
        return if (stack1.size > 0) {
            //将stack1中的数据全部反向push到stack2中
            while (stack1.size > 0) {
                stack2.push(stack1.pop())
            }
            val node = stack2.pop() as T
            //重新存回到stack1中
            while (stack2.size > 0) {
                stack1.push(stack2.pop())
            }
            node
        } else {
            null
        }
    }
    
    fun size(): Int {
        return stack1.size
    }
}

        //使用
        val solution = Solution()
        solution.push(1)
        solution.push(2)
        solution.push(3)
        solution.push(4)
        Log.e("BBBBB", solution.pop().toString())
        solution.push(5)
        solution.push(6)
        while (solution.size() > 0){
            Log.e("BBBBB", solution.pop().toString())
        }

    //正常输出:1,2,3,4,5,6 先进先出的队列效果

解读一下吧,栈其实就是先进后出,那么存入到stack1中,将其循环pop到stack2中,比如入栈stack1中的顺序是 先入 1、2、3、4、5、6,那栈1中的存储是 从上往下应该是 6、5、4、3、2、1。pop时,先pop一个6,那循环pop出去,push到stack2中的时候,statck2就和刚刚的stack1完全相反,然后再执行pop的时候,就实现了类似队列的效果了。当然在代码上实现,要重置stack2,存回到stack1中,因为始终是往stack1中push的。
看一个手绘图:


image.png
12、任意实现一个排序算法

这里就稍微简单一些,实现一个冒泡排序,再加一个快速排序吧,有一个博文讲的挺好,可以了解一下 Java中常见的排序算法

    /**
     * 冒泡排序
     * 1、通过每一次遍历获取最大值/最小值
     * 2、将最大值/最小值放在尾部/头部
     * 3、除开最大值/最小值,剩下的数据进行遍历获取最大/最小值
     * @param arr
     */
    public static void bubbleSort(int[] arr){
        for(int i=0; i < arr.length; i++){
            for(int j=0; j < arr.length -i -1; j++){
                //遍历获取最大值,放在尾部
                if(arr[j] > arr[j+1]){
                    //交换
                    int temp = arr[j+1];
                    arr[j+1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }

    /**
     * 快速排序
     */
    public static void quickSort(int[] arr){
        int len;
        if(arr == null || (len = arr.length) == 0 || len == 1){
            return; //不需要排序
        }
        quickSort(arr, 0, len -1);
    }

    /**
     * 快速排序
     * @param arr 要排序的数组
     * @param left  要排序的左边范围
     * @param right  要排序的右边范围
     */
    private static void quickSort(int[] arr, int left, int right) {
        //如果左侧 大于 右侧,此次排序结束,代表基准值找到它要在的临界点
        if(left > right){
            return ;
        }
        //获取基准数,以左侧第一个开始
        int base = arr[left];
        
        int i=left; int j=right;
        while(i != j) {
            // 顺序很重要,先从右边开始往左找,直到找到比base值小的数
            while(arr[j] >= base && i < j) {
                j--;
            }

            // 再从左往右边找,直到找到比base值大的数
            while(arr[i] <= base && i < j) {
                i++;
            }

            // 上面的循环结束表示找到了位置或者(i>=j)了,交换两个数在数组中的位置
            if(i < j) {
                int tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
            }
        }

        // 交换基准数找到的较小数位置,把较小数放到基准数位置
        arr[left] = arr[i];
        arr[i] = base;
        // 递归,继续向基准的左右两边执行和上面同样的操作
        // i的索引处为上面已确定好的基准值的位置,无需再处理
        quickSort(arr, left, i - 1);        //基准数左边
        quickSort(arr, i + 1, right);        //基准数右边
    }

美团一面:2021-05-13 19:00 线上面试

1、Activity的生命周期,A启动B及B返回A的完整生命周期
2、主线程与子线程使用Handler的区别
3、Looper.prepare方法做了哪些事?
4、View与ViewGroup的onMesure的方法有什么去区别
5、View与ViewGroup的事件分发有什么区别
6、include、merge、ViewStub相关,include会影响xml的层级么?
7、MVC、MVP、MVVM之间的区别
8、HashMap底层原理了解过么?说说HashMap的底层原理
9、说几个你了解的排序算法
10、现场共享一下屏幕,写一个检测一个字符串是否是IP地址的代码
11、聊聊个人经历,对未来的的看法,工作地点的看法之类的,这个就是闲聊了几句,面试官人挺好的

1、Activity的生命周期,A启动B及B返回A的完整生命周期

这个上面滴滴面试的时候提到过了,这里就不说了,往上面翻看看~

2、主线程与子线程使用Handler的区别

这里还是考察对Handler的了解,上面滴滴的线程A与线程B如何通信也类似。
答:主线程与子线程在使用Handler时主要区别在于子线程在new Handler之前需要保证子线程有Looper对象,否则会抛出异常,也就是在new Handler之前需要先执行Looper.prepare方法,初始化一个Looper对象,存到ThreadLocal中。同一个线程中,Looper.prepare方法不能执行多次,否则也是会抛出异常的,因为如果你初始化多次的话,存入到ThreadLocal中,就不知道到底该用哪一个Looper对象了。主线程中不需要Looper.prepare是因为在APP启动的时候,ActivityThread的main方法中执行了Looper.getMainLooper方法,初始化了一个Looper对象。

3、Looper.prepare方法做了哪些事?

答:Looper.prepare方法主要就是执行Looper的构造函数,Looper构造函数中初始化了一个MessageQueue,用于存储Handler发送多来的Message对象,同时会把Looper对象存储到ThreadLocal中,new Handler时会从ThreadLocal中取出Looper对象。

4、View与ViewGroup的onMeasure的方法有什么去区别

这里在答的时候答的不是很好,说成了View的onMeasure方法就是对自己做一个测量,ViewGroup除了会测量自身,还会遍历所有子View进行测量。
这里再总结一下,争取下次遇到这个问题时能答的更好:
答:简单来讲就是:ViewGroup除了测量自身,还需要测量子View的大小,ViewGroup中提供了对子View的测量方法: measureChildren(int widthMeasureSpec, int heightMeasureSpec),在measureChildren中遍历所有子View,调用measureChild((int widthMeasureSpec, int heightMeasureSpec),在measureChild中调用了View的measure(int widthMeasureSpec, int heightMeasureSpec)方法,最终执行View的onMeasure()方法,让子View测量自身大小。
实际上ViewGroup是View的子类,也就说ViewGroup可以看成一个特殊的View,我们在这里可以不叫ViewGroup和View,可以说成父控件和子控件比较容易理解。
父控件在测量时,会测量自身,然后调用measureChild方法去循环测量子控件,测量子控件时,会带两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件,子控件拿着这些条件就能正确的测量自身的宽高了。也就是子控件测量自身时,需要关注父控件的两个参数。至于这个限制条件就要了解一下MeasureSpec类,MeasureSpec是View的内部类,值保存在一个int值当中,一个int有32位,前两位是 mode(模式),后30位是 size(大小) 即:MeasureSpec = mode + size。ViewGroup中有一个getChildMeasureSpec方法,这个方法规定了子View的测量模式及大小。主要就是规则就是:

  • 如果父View是EXACTLY模式(match_parent):子View如果是固定了大小,则子View大小为自己的大小,测量模式为:EXACTLY;子控件是match_parent,将父View的大小赋值给子View,测量模式为:EXACTLY;如果子View是wrap_content,设置子View的最大尺寸为父View,测量模式为:AT_MOST

  • 如果父View是AT_MOST模式(wrap_content):如果子view有自己的尺寸,则使用自己的尺寸,测量模式为:EXACTLY;当子View是match_parent,父View的尺寸为子View的最大尺寸,测量模式为:AT_MOST;如果子View是wrap_content,父View的尺寸为子View的最大尺寸,测量模式为:AT_MOST

  • 如果父View没有做任何限制(UNSPECIFIED):如果子view有自己的尺寸,则使用自己的尺寸,测量模式为:EXACTLY;因父View没有对子View做出限制,当子View为MATCH_PARENT时则大小为0,测量模式为:UNSPECIFIED;因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0,测量模式为:UNSPECIFIED;

5、View与ViewGroup的事件分发有什么区别

答:View和ViewGroup的事件分发主要区别在于ViewGroup比View多了一个拦截事件onInterceptTouchEvent方法,如果返回true,则交给自身的onTouchEvent方法,不再往下继续分发。看自身是否要消费事件,如果消费事件,则onTouchEvent返回true,不再向上传递。View就是一个View,不存在子View,所以不需要拦截,就是消费或者不消费的问题。

6、include、merge、ViewStub相关,include会影响xml的层级么?

答:include用于重用布局,一般用于顶部ToolBar、侧边栏等共用的一些布局抽取。
merge一般配合include使用,意为合并布局,意思就是说:如果include的layout的ViewGroup类型是LinearLayout,包裹include的类型也是LayoutLayout时,就会合并两个LinearLayout,就是两个ViewGroup类型如果是相同的就会合并成为一个,减少层级嵌套。所以,include标签如果单独使用时,方便布局重用的同时,会增加一个层级嵌套,和merge配合使用时,可以减少include带来的层级嵌套。
ViewStub一般用来暂时不需要显示,在某种情况下展示出来的布局。如果只使用visibility,在xml加载的时候就会加载到内存,影响加载效率。使用ViewStub时,先inflate,之后setVisibility,inflate只能调用一次,否则会抛出异常,因为inflate之后,ViewStub就被替换成真正的View了,之后就只是setVisibility方法即可。

7、MVC、MVP、MVVM之间的区别

答:MVC是比较早的一种软件开发模式了。
M对应模型层(Model)针对业务模型,建立的数据结构和相关的类,它主要负责网络请求,数据库处理,I/O的操作。
V代表视图层:对应于xml布局文件和java代码动态view部分。
C代表Controller层对应Activity,用于绑定View及向Model层发送指令获取数据。

MVP中把MVC的C改P,View层和Model层不直接通信,通过实现一个View接口来进行通信,实现了View和Model的解耦,但是这样比较麻烦,每个V都需要对应一个Presenter及View接口,实现较为复杂。

MVVM 中View和Model通过ViewModel实现数据互通,与Databinding的配合使用实现了View和Model的双向绑定,主要也是通过观察者模式来实现相互的解耦。个人觉得是目前最为常用的Android软件开发的一种模式了。当然业务非常简单且后期不需要太多的维护的就还是用MVC,写法简单。较为复杂的还是推荐MVVM模式。

8、HashMap底层原理了解过么?说说HashMap的底层原理

答:HashMap的底层是数组+链表实现的,数组长度默认为16,负载因子0.75,也就是说当数组长度超过12的时候,数组会自动扩容,扩容倍数为2。存储在数组位置是由key的hashCode值的hash散列值与数组长度的取模得来的,即hash(key.hashCode()) / 数组长度。当数组扩容后,数组存储位置要发生改变,所以数组扩容是一个比较耗费资源的一个操作。同时,数组上存储的是链表,1.7之前采用单链表,1.8之后采用单链表和红黑树,当链表长度超过8时,单链表改为红黑树,当长度变为6的时候,改回单链表,因为链表长度为6时,单链表和红黑树的查询时间复杂度一样,当链表超过6的时候,红黑树效率要比单链表高,为什么设置为8改变,应该是为了中间有一个过渡,使得不会在单链表和红黑树之间频繁的切换。

9、说几个你了解的排序算法

排序算法在上面滴滴的提到过了。我回答的时候只说了思想:
答:我们常见的排序算法有冒泡排序、快速排序、插入排序、选择排序等等,比如冒泡排序其实就是循环进行两两对比,遇到大的放右边,一次循环过后就找到一个最大值,然后扣除最大值再进行循环。快速排序其实就是先确定一个基准值,然后先从右边进行查找,找到一个比基准值小的,然后从左边开始查找,找到一个比基准值大的,两个交换位置,然后基准值再和小的交换位置,就找到了基准值应该在的位置,然后就分为左右两边,采用递归的方式再对两边进行上述操作。

10、现场共享一下屏幕,写一个检测一个字符串是否是IP地址的代码

这个应该就是考察一下对IP地址的理解以及正则表达式吧,要不然这个其实挺简单的一个机试题:
上代码:

    private fun isIp(str: String) : Boolean {
        //正则表达式判断是否是IPV4类型IP地址(只判断长度 xxx.xxx.xxx.xxx)
        val regexStr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
        if(!str.matches(Regex(str))){
            return false
        }
        val split = str.split(".")
        for(value in split){
            //判断每个段是否是在0-255区间
            if(value.toInt() < 0 || value.toInt() > 255){
                return false
            }
        }
        return true
    }
11、聊聊个人经历,对未来的的看法,工作地点的看法之类的,这个就是闲聊了几句,面试官人挺好的。

答:这个就是闲聊了几句,面试官这边也挺好,面试就一个沟通交流的事情,不那么严肃的面试官~总结下来,面试很顺利,面试官很不错,面试的问题也都比较基础,手下留情了应该是!

同时:在总结这次面试的时候,也收到了美团二面的通知,希望大佬不要把我虐的太惨,手下留情!!

杭州某科技一面:2021-05-14 19:00 电话面试

1、HashMap、LinkedHashMap的原理及区别
2、HashMap、LinkedHashMap存入和取出有什么区别
3、Kotlin 变量、构造函数、init方法
4、内存泄漏相关
5、组件moudle循环依赖问题
6、build.gradle targetSdkVersion minSdkVersion compileSdkVersion 怎么理解?
7、Handler相关
8、Handler如何保证线程安全的
9、Flutter有用过吗?
10、Retrofit相关,怎么把Interface转换成OkHttp的请求的。
11、组件化有用过么?组件之间的通信怎么实现的?

1、HashMap和LinkedHashMap的区别?

答:HashMap底层是数组+链表组成,而LinkedHashMap继承自HashMap,LinkedHashMap和HashMap的主要区别就是LinkedHashMap中的Entry会以双向链表的方式进行连接起来,即HashMap中是存储的单链表,LinkedHashMap中每个单链表上的Entry会有双向链表关联。

2、HashMap、LinkedHashMap存入和取出有什么区别

答:第一个问题说了,LinkedHashMap中每个链表上的Entry是以双向链表连接的,意味着在迭代取出时的顺序是按照存入的顺序,存入的顺序是什么,取出的顺序就是什么,而HashMap存入的时候是按照key的hashCode值的hash散列值与数组长度的取模得到存储数组上的位置,之后插入到数组上的链表中,取出时是无序的。

3、Kotlin 变量、伴生对象、构造函数、init方法的初始化顺序?

答:先进行变量的初始化,伴生对象、之后进行init方法块,而后进行构造函数。kotlin在编译后的Java代码中,伴生对象类似与静态变量、方法,所以优先执行,init方法块的代码会比构造函数中的代码块先行调用。

4、内存泄漏相关

答:内存泄漏总结一句话就是:长生命周期持有了短生命周期对象,短生命周期对象结束后,仍然被长生周期对象所引用,JVM在GC时,无法回收短生命周期对象,就导致了内存泄漏。以下举例用Activity代表短生命周期对象。

比较常见的如:Handler在使用过程,直接new Handler,匿名内部类会(只要是非静态的内部类)持有外部类的引用,即Handler持有了Activity的引用,而Handler在sendMessage时,Message对象中有一个target属性,实际上这个target属性就是Handler,就意味Message对象间接持有了Activity的引用,当取出的消息无法及时处理或处理时间较长的情况下,Activity关闭了就导致了内存泄漏。还有像静态变量持有了Activity、单例中持有了Activity、未取消注册或回调如广播、资源未关闭或释放、WebView等等

解决方案就比如Handler,采用静态内部类,如果需要Activity的引用,则使用弱引用,如单例中如果非要使用Context的话,使用applicationContext等,其实很多时候就是和我们自己的一些代码习惯有关系。当然在功能都完成时,还可以使用LeakCanary这个内存泄漏检测工具来协助查询解决内存泄漏问题,当然我们也可以使用AndroidStudio中的profiler工具来查看内存使用情况等等

5、组件或者Lib库、moudle等循环依赖问题怎么解决

这个问题,当时其实挺蒙圈的,这是啥意思?当时想的是moudleA依赖moudleB、moudleB依赖moudleA?一般不这么玩的吧,我也没这么玩过,所以这题回答不上来。我答:没这么玩过,不知道如何处理。
现在想想,面试官说的应该是有间接依赖导致一些包重复的问题吧,比如libA依赖了一个support25.0.0,你自己的build.gradle依赖support25.2.0之类的,又或者Retrofit依赖了OkHttp,你自己也要一个OkHttp,或者说,其实有些不处理应该也没多大关系,但是之前有遇到一个包重复问题的报错。这里可以说有两种解决方案:一种就是如果知道是哪个一个moudle或知道哪一个库的话,直接在库后面使用exclude去除重复,如果不知道或者很多的话,直接就在build.gradle中使用configurations{ all*. exclude}去除。

6、build.gradle targetSdkVersion minSdkVersion compileSdkVersion 怎么理解?

答:

  • compileSdkVersion 25:表示在AS本地开发环境中的编译版本,代表当前使用哪一个SDK版本开发编译。
  • minSdkVersion 16:用来指定可运行应用最低的API级别,如果不加指定该值,默认值是1
  • targetSdkVersion 25:用来指定应用目标API级别,如果不设置,默认与minSdkVersion值一样。如果新发布的SDK版本会对UI显示甚至操作系统运行机制产生影响,而你的APP又没有做好应对措施,为了保证你的APP正常运行,那你需要降低目标版本。因为你的目标版本仍然是旧的SDK,所以在新版系统中那些新的变化会在你的APP中被忽略,继而保证其正常运行。例如6.0之后的危险权限的适配。
7、Handler相关

这个这里就不再解析了,上面滴滴和美团都已经提到过了,这里看到一个文章,涉及到Handler的一些问题,可以了解了解Handler相关问题

8、Handler如何保证线程安全的?

答:在Handler发送消息时,会将Message存入MessageQueue消息队列中,即enqueueMessage方法,这个方法中,有一个synchronized(this){}的方法块,同时在Looper.loop()方法中的MessageQueue.next()方法中也是使用synchronized加锁的方式来保证存取Message的线程安全的。

9、Flutter有用过吗?

答:这个我真没用过,只是了解了一点点语法,我是真的受不了Flutter的语法...就像JetPack里面有一个新的组件好像叫Compose,有时间去了解了解

10、Retrofit相关,怎么把Interface转换成OkHttp的请求的。

答:这个主要是通过Retrofit对象实例调用create方法,create方法中使用动态代理的方式获取接口的代理对象,这个动态代理方法里面还做了循环获取所有注解的方法、解析注解如@Header、@Body等等,最终由ServiceMethod创建一个OkHttpCall对象,create方法最终返回一个动态代理对象,然后就可以通过动态代理对象调用接口的方法了。

11、组件化有用过么?组件之间的通信怎么实现的?

答:对于这块并不是很熟,目前使用的就是自己在github上开源了一个快速开发框架,是用Kotlin+Jetpack(LifeCycle、LiveData、ViewModel、Databinding)+Arouter+Retrofit+Glide+协程来抽取封装的一套快速开发框架,项目地址:Kotlin+Jetpack+Databinding+协程+Retrofit+Glide+MMKV封装的MVVM快速开发框架,这里面其实Arouter、Databinding都是看着文档来使用的,对里面的原理还没了解过。这个会抽时间再去了解了解的。

美团二面:2021-05-18 19:00 视频面试

1、Retrofit用到设计模式
2、动态代理你了解多少
3、Glide.with传入的application和activity、fragment有什么区别
4、application和activity的Context的区别,application可以启动Activity么?
5、Glide的缓存有了解吗?
6、LRUCache算法的了解?
7、屏幕适配方案
8、AIDL的in和out的了解么
9、so打包流程
10、v1和v2签名的了解
11、LBS相关
12、冒泡排序
13、假如有一个场景,有大量的数据需要排序,结合刚刚的排序算法,排序后需要toast一个结果,你要怎么做,写一段代码

1、Retrofit用到的设计模式

答:在初始化的时候用了 建造者模式,通过不同的调用顺序以及传入不同的参数来实现不同功能的Retrofit,比如传入的CallAdapterFactory可以自定义实现等等( 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示),在创建CallAdapter/Converter的时候用到 工厂模式 (用于创建对象的接口,让子类决定实例化哪个类),还有 适配器模式 :CallAdapter用来适配OkHttpCall对象,在调用CallAdapter.adapt方法时,OkHttpCall实例会作为参数传递给adapt方法交给CallAdapter去适配(将一个类的接口变成客户端所需要的另一个接口,从而使原本因接口不匹配而无法一起工作的两个类可以在一起工作),当然,Retrofit.create方法应该是Retrofit最为重要的方法,用了 动态代理 来获取一个接口的代理对象,之后通过代理对象去调用接口中的方法获取Call对象,发起请求。

2、动态代理你了解多少?

答:动态代理是在运行时才会生成代理类,通过ClassLoader进行加载,动态代理需要实现InvocationHandler接口,实现invoke方法。Retrofit中的create方法通过Proxy.newProxyInstance方法实现动态代理,在invoke方法中通过ServiceMethod对接口方法及注解的解析,返回一个接口的代理对象。

3、Glide.with传入的application和activity、fragment有什么区别

答:Glide.with传入application时,会调用getApplicationManager方法,方法中通过双重校验锁的单例模式方式获取一个RequestManager,使用ApplicationLifecycle来管理生命周期。传入的activity、fragment等其他非application的Context时,会判断当前是否是主线程,如果不是主线程的话,则会调用getApplicationManager,如果是主线程的话,会使用一个空白Fragment来管理生命周期。也就是说传入的是application和activity、fragment最大的区别应该是Glide的生命周期不同,application是整个应用程序的生命周期。

4、application和activity的Context的区别,application可以启动Activity么?

答:两个都是Context的子类,application都继承自ContextWrapper,Activity继承自ContextThemeWrapper,而ContextThemeWrapper又继承自ContextWrapper,只不过Activity代表界面,需要一个主题。ContextWrapper的主要实现类是ContextImpl。application对应的生命周期是应用程序的整个生命周期,只能有一个实例,应用启动时就初始化。activity生命周期较短,生命周期只在页面启动时-页面关闭时。启动Activity时,会生成一个叫ActivityRecord的对象(包含了所有Activity的信息),之后要将其加入到ActivityTask管理的TaskRecord任务栈中,application启动时,如果当前任务栈不存在则会抛出一个异常,所以需要携带一个flag:Intent.FLAG_ACTIVITY_NEW_TASK,实际上非Activity Context 启动时,都要加上创建新任务栈,否则都会抛出异常。(Android 7和8好像不需要携带,源码中有其他条件)

5、Glide的缓存有了解吗?

答:Glide缓存应该说是有两种吧,内存缓存和磁盘缓存,内存缓存又分为活动缓存和非活动缓存。活动缓存(activeResources)中使用弱引用的HashMap来存储,非活动缓存为:LruResourceCache,其实就是利用LruCache算法来缓存的。Glide中,先从活动缓存中获取缓存,如果没有则从非活动缓存中获取,然后从非活动缓存中取出,放入到活动缓存,不可见后重新放回到非活动缓存。而Glide的磁盘缓存通过DiskLruCache来实现。不管是内存缓存还是磁盘缓存,缓存的key都是通过对URL进行一系列的变化得到的,所以有时候我们经常会见到存储在七牛云等一些服务器上的图片会有缓存失效的问题,其原因是因为那些图片后面会加一个token的问题,所以如果出现这种问题,我们就需要通过重写GlideUrl类中的getCacheKey方法,在这个方法里去除多余的token部分。然后通过load(new MyGlideUrl(url))的方式去进行加载。

6、LRUCache算法的了解?

答:LRUCache采用最近最少使用算法,即当缓存到达设置的大小时,将会移除最近最少用的数据。

  • LruCahce基于LinkHashMap双向链表实现,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。
  • LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。
7、屏幕适配方案

答:目前市场上最常见的应该是两种,一种是最小宽度限定符方案(SW),一种是今日头条适配方案。比较早以前我用的是限定符方案,有一个开源的jar包,可以生成一堆dimens.xml文件,在适配时,系统会精准定位使用某一个dimens.xml文件,如果定位不到则是向上还是向下选择一个来匹配,具体向上还是向下忘记了,反正就是就近原则。现在我用的是今日头条适配方案,主要是动态计算density的值,比如一个540dp的设计稿,在1080px手机上时,算出来的density值为 1080/540 = 2,在540px手机上时,算出来的density值为:540/540 = 1,假如一个View的宽度为50dp,那在1080px手机上所占用的像素就为502 = 100px,在540px手机上所占像素为501 = 50px,则它们的 百分比值是相等的 100/1080 = 50/540。

8、AIDL的in和out的了解么

AIDL平时开发过程中用的还真是少,网上找的一个解释:

  • in、out、inout表示跨进程通信中数据的流向(基本数据类型默认是in,非基本数据类型可以使用其它数据流向out、inout)
  • in 表示数据只能由客户端流向服务端。(表现为服务端修改此参数,不会影响客户端的对象)
  • out 表示数据只能由服务端流向客户端。(表现为服务端收到的参数是空对象,并且服务端修改对象后客户端会同步变动)
  • inout 则表示数据可在服务端与客户端之间双向流通。(表现为服务端能接收到客户端传来的完整对象,并且服务端修改对象后客户端会同步变动)
    详细解释可以看看:AIDL参数中in、out、inout的区别
9、so打包流程

这块也不熟,之前有过串口通信的东西,也是自己打包的,都是通过网络上的一些文章来做的。后续工作有需要的情况再去详细了解吧。

10、v1和v2签名的了解

答:v1签名的时候是我知道是使用SHA1签名的方式,v2不是很了解。详细的可以看看这篇文章 APK签名

11、LBS相关

这个就不说了,问项目上的一些事情,接入的时候做了哪些事,都有相关文档来做。

12、冒泡排序

上面也有一些算法的介绍,翻上去看看吧,这里自己再写一遍冒泡算法

    public int[] sort(int[] arr){
        for(int i=0; i arr[j+1]){
                    int temp = arr[j+1];
                    arr[j+1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        return arr;
    }
13、假如有一个场景,有大量的数据需要排序,结合刚刚的排序算法,排序后需要toast一个结果,你要怎么做,写一段代码
public class MainActivity extends AppCompatActivity {

    private Handler mHandler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                switch (msg.what){
                    case 1:
                        Toast.makeText(MainActivity.this, msg.obj.toString(), Toast.LENGTH_SHORT).show();
                        break;
                }
                return false;
            }
        });

        new MyThread(this).start();

    }
    
    //静态内部类不持有Activity的引用,防止Thread长时间执行下,造成Activity的内存泄漏。
    public static class MyThread extends Thread{
        //静态内部类不持有外部类的引用,无法获取外部的mHandler变量,需要弱引用一个Activity,通过get方法获取
        WeakReference mThreadActivityRef;
        public MyThread(MainActivity activity) {
            mThreadActivityRef = new WeakReference<>(activity);
        }

        @Override
        public void run() {
            super.run();
            int[] arr = {1,2,5,3,20,29,15,25,15};
            Sort sort = new Sort();
            //这里可能耗时,所以开一个线程执行
            int[] sort1 = sort.sort(arr);
            Message message = Message.obtain();
            message.what = 1;
            message.obj = sort1;
            if (mThreadActivityRef == null)
                return;
            if (mThreadActivityRef.get() != null)
                mThreadActivityRef.get().mHandler.sendMessage(message);
        }
    }

}

浙江DH一面:2021-05-21 09:15 电话面试

1、HashMap实现原理、SparseArray原理
2、MMKV实现原理,和SP有什么不同的地方
3、Arouter实现原理
4、JVM内存模型,Android中使用的是哪一种垃圾识别算法
5、进程间通信
6、自定义View你做过哪些东西?
其他一些比较熟悉,也是之前提过的一些内容,这里就不再提了

1、HashMap实现原理、SparseArray原理

HashMap前面也已经有过介绍了,这里就不再提了
SparseArray底层其实是两个数组实现的,一个int数组用于存放key,一个Object数组,用于存放value,key和value的数组位置是一一对应的,数组默认长度为10。可以参考一下:SparseArray相关, SparseArray相关

  • get()方法中使用二分查找算法来查询key在数组中是否存在,如果存在返回key的位置的值(key>0),如果key在数组中不存在,则会记录下key应该存储的位置之后进行取反返回(key < 0)(在插入的时候能知道应该存放的位置,如:[1,3,4],查询/插入key为5时,返回值应该是-3,key为1时,返回值应该是0)

  • put()方法中,同样进行二分查找,找到了直接在那个位置上替换,找不到则对负值取反,判断这个位置上的value是否是DELETE状态,如果是的话,直接复用返回(这里也是SparseArray节省内存的一个表现,删除时是标记DELETE状态,并没有直接删除,在需要gc时,这个gc是重新排序时才会清除。),否则开始判断是否需要gc(重新排序),也重新计算要插入的位置,之后进行扩容判断,如果需要扩容则进行扩容,一般扩容大小为数组大小的两倍。

  • 扩容机制中有一行代码,currentSize <= 4 ? 8 : currentSize * 2,这里总感觉只会执行*2的操作,这里如果有人再问到的话,可以和面试官再交流一下。

  • 总结一下和 HashMap的区别,SparseArray在插入时候,可能可以直接复用,内存方面较为节省,但是要对插入位置后面的数据全部后移一位,效率上应该没有HashMap高的吧,总的来说,小数据量的可以采用SparseArray,数据量大一些的,可以用HashMap,数组扩容的概率较小,效率来讲也应该较高。就是空间和时间的选择。

2、MMKV实现原理,和SP有什么不同的地方

答:他们两个都是用来持久化key-value形式的数据,

  • SP是Android提供的,MMKV是腾讯开源的一个框架。SP保存的格式为XML,在初始化SP的时候是使用IO的方式读取(用户空间拷贝到内核空间,内核空间拷贝到磁盘,即两次数据拷贝)整个XML文件进行解析,存入内存Map集合中(ArrayMap),在每一次修改的时候的时候,都是将Map转化为XML文件进行文件的覆盖操作(全量更新)。
  • MMKV其实就是利用mmap内存映射的来实现的,通过NDK的方式,调用了Linux的mmap方法以及memcpy方法实现的,就是一次内存映射和一次数据拷贝 ,MMKV保存的数据格式是连续的一个字节数组,数据总长度-》key长度-key-value长度-value的形式。mmap:在用户空间开辟一块内存,和文件建立起一个一对一的映射关系(内存映射),只需要做一次数据拷贝,所以操作内存就是操作文件,不需要再开线程进行IO操作。相比较来说,MMKV存储的数据量较小、存储效率较高、支持多进程数据同步等。
3、Arouter实现原理

答:这个了解的并不多,就看着文档进行使用。然后只是简单的了解了一点点,Arouter最终还是通过Intent来实现跳转的,只不过利用是利用APT技术,在编译时动态的生成一些类,存储了一些key-value形式的数据,key就是注解中的path、value就是类信息,在要启动的时候,根据传入的path取出类信息,然后还是使用Intent显示启动Activity。APT技术主要就是实现了AbstractProcessor这个类,这个类可以在编译时,执行打包dex之前动态的生成类信息,之后编译执行打包dex时,就会将动态生成的类信息一起打包到dex中,也就可以使用Intent的显示启动了。

4、JVM内存模型,Android中使用的是哪一种垃圾识别算法

感觉这里面试官混淆了两个概念:JVM内存结构和JAVA内存模型。JVM内存结构就简单的了解了一些,但是现实工作中,我真不知道怎么使用(因为没有处理过这些东西,其实存在即有意义,是自己太孤陋寡闻了)

  • JVM内存结构分为五大类:程序计数器、JAVA虚拟机栈、本地方法栈、方法区、堆。前三个随线程而生,随线程而死,方法区和堆是JVM在gc时主要关注的两个地方。
    • JVM垃圾识别算法 主要有两种:引用计数法、可达性分析法
      1、引用计数法:一个对象被引用一次,引用计数+1,断开引用,引用计数 -1,当引用计数为0的时候就可以认为是垃圾。无法解决相互引用的问题,如A引用B,B引用A,两个引用计数都不为0
      2、可达性分析法:GC ROOT的对象作为搜索起点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可以被回收。可以作为GC ROOT的对象有: 1、JAVA虚拟机栈中引用的对象 2、方法区中类静态属性引用的对象 3、方法区中常量引用的对象 4、本地方法栈中JNI引用的对象
    • 垃圾回收算法 主要有四种:标记清除算法、复制算法、标记整理算法、分代收集算法
      1、标记清除算法:标记清除算法有两个过程,一:标记的过程、二:清除的过程。标记清除的过程会产生内存碎片(不连续),如果有一个对象需要申请大量连续内存时,就可能无法找到可以用的内存块,不得不触发新的一轮垃圾回收操作。
      2、标记整理算法:和标记清除算法的区别是在清除之前会将要清除的内存进行重新整理成一块连续内存块,之后再进行清除,优点是清除后的内存是连续的,但是重新整理时间成本将提高
      3、复制算法:是将一块内存分成两份,一份是空的,一个用于存储对象。当发生GC的时候,将一个上面存活的存入空的那份里面,然后将非空的清除,循环往复。优点是不会出现内存碎片化问题,缺点可以用内存对半区分了。
      4、分代收集算法是将内存分为新生代、老年代,新生代占用1/3,老年代占用2/3。新生代中主要使用复制算法,新生代回收是比较频繁的,复制算法较优。老年代采用标记整理算法。
5、进程间通信

答:由于进程间内存数据不能共享,Android中的进程间通信(IPC)主要有几种方式:共享文件、广播、内容提供者、Messenger、AIDL、socket等方式。Messenger是基于AIDL,AIDL是基于Binder机制实现的。Binder基于内存映射原理(mmap)实现,实现用户空间到内核空间的一次数据拷贝和内存映射。
AIDL的方式实现进程间通信步骤:1、创建一个aidl文件 2、build后生成接口 3、创建service实现接口及aidl中的方法(其实就是binder对象) 4、在onBind中返回binder对象 5、在使用时,使用bindService中传入ServiceConnection接口对象。在onServiceConnected方法中即可拿到service中的binder对象,调用aidl的方法。详细了解可以看看Android中进程间通信

6、自定义View你做过哪些东西?

答:自定义View,我做过最多的就是几种控件的组合,就好比顶部Toolbar的一些自定义。这样做可以减少一些xml的层级嵌套,减少加载xml过程中的Xml2View的过程,加快一些渲染速度。其他一些比较炫酷的自定义View基本就是github找一些现有的来进行修改。主要就是对自定义View中的测量、布局、绘制这三个方法以及事件的分发等做一些修改。看看自定义View相关 , 事件分发相关。

北京某公司 2021-05-24 19:00 视频面试

这里吐槽一下:面试这么多家,就遇到这家公司了,问的时候不知道是我自己答的不好还是什么,每个问题后面都喜欢加一句:你确定?面试体验很差...如:String类可以继承么?答:不能,String类是final修饰的,即是一个最终态。final可用于声明属性、方法和类,分别表示属性不可变,方法不可重写,类不可继承。面试官:你确定?虽然面试体验不好,但是自己还是有很多掌握不好的地方,记录几个自己答的不是很好的问题吧。
1、Lifecycle的用法(以下一块总结一下LiveData、ViewModel的用法及原理吧)
2、ViewPager和ViewPager2的区别,如何实现懒加载?
3、APP启动流程中,为什么是用socket和Zygote通信的?
4、组件间如何进行通信?
5、在onCreate方法中能获取到view的宽高吗?怎么才能获取view的宽高

1、Lifecycle的用法:

答:

  • 1、通过getLifecycle()方法获取一个Lifecycle对象(实际上是LifecycleRegistry对象,被观察者),之后调用addObserve(),传入一个观察者。
  • 2、观察者实现LifecycleObserver,方法上使用OnLifecycleEvent注解关注对应生命周期,生命周期触发时就会执行对应方法。

Support Library 26.1.0及以上、AndroidX的 Fragment 和 Activity 已实现 LifecycleOwner 接口,所以我们在Activity中可以直接使用getLifecycle()。要自定义实现的话,可以自己实现LifecycleOwner接口,new 一个 LifecycleRegistry对象,在各个生命周期中进行绑定markState()。

  • 原理:LifecycleOwner(如Activity)在生命周期状态改变时(也就是生命周期方法执行时),遍历观察者,获取每个观察者的方法上的注解,如果注解是@OnLifecycleEvent且value是和生命周期状态一致,那么就执行这个方法。
    再详细的也不多说了,看看这三篇文章,好好看看,写的非常不错。终于懂了-Lifecycle,终于懂了-LiveData, 终于懂了-ViewModel
2、ViewPager和ViewPager2的区别,如何实现懒加载?

答:

  • ViewPager默认会帮我们预加载前后两个页面的数据,并且这2个view是不可见的。
    由于ViewPager对offscreenPageLimit做了限制,默认设置为1,因此页面的预加载不可避免。这也容易造成资源浪费。一般使用ViewPager与frament配合使用,利用fragment的setUserVisibleHint方法,来实现控制数据懒加载,而布局只能提前进入(预布局)。
  • ViewPager2的预加载与离屏加载在view层面有着本质的区别,离屏加载的view已经添加到parent上,而预加载只是准备了布局,并没有加载到parent上。ViewPager2默认是开启预加载关闭离屏加载的,可能会预加载一条数据,而离屏加载即设置offscreenPageLimit为0。ViewPager2设置offscreenPageLimit为1时,在第1页会加载2条数据,每滑动一页,都会加载下一页数据,直到第五页,会移除第一页数据。
    1.目前ViewPager2对Fragment支持只能用FragmentStateAdapter,FragmentStateAdapter在遇到预加载时,只会创建Fragment对象,不会把Fragment真正的加入到布局中,所以自带懒加载效果;
    2.FragmentStateAdapter不会一直保留Fragment实例,回收的ItemView也会移除Fragment,所以得做好Fragment重建后恢复数据的准备;
    3.FragmentStateAdapter在遇到offscreenPageLimit>0时,处理离屏Fragment和可见Fragment没有什么区别,所以无法通过setUserVisibleHint判断显示与否。
3、APP启动流程中,为什么是用socket和Zygote通信的?

答:这个我是真的不了解,感兴趣去网上看看吧,都已经吵疯了,但我的感觉就是没有一个解释清楚的,要是有人能解释清楚的,还请在后面的评论中指出,学习一下,在这里先谢谢了~网上找的一篇文章,作为参考

4、组件间如何进行通信?

到现在我也没明白这个问题的该怎么回答,也不知道面试官真正想问的问题。组件间是指的组件化还是Android的四大组件?
组件化各组件模块之间通信用的是Arouter框架,其实Arouter最终的也还是通过Intent来进行通信的。只不过利用APT技术,在编译时动态生成一个路由信息类,之后将动态生成类信息打包到dex中。里面用一个Map来保存信息,key为注解声明的,value是要启动的Activity,在要跳转时,通过path(就是key)去获取到Activity,然后使用Intent去通信。

5、在onCreate方法中能获取到view的宽高吗?怎么才能获取view的宽高

答:不能在onCreate中获取View的宽高,获取到的值都为0。实际上,要真正获取到View的宽高,必须是等View的测量布局绘制完成之后才可以获取到真实的View的宽高。有三种方法可以去获取View的宽高

  • 1、通过view.post(new Runnable(){});方法去获取view的宽高。
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getWidth();
                int hight = view.getHeight();
            }
        });
  • 2、为ViewTreeObserver注册OnGlobalLayoutListener接口,OnGlobalLayoutListener的作用是当View树的状态发生改变或者View树中某view的可见性发生改变时,OnGlobalLayoutListener的onGlobalLayout方法将会被回调。
        ViewTreeObserver observer = title_name.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            
            @Override
            public void onGlobalLayout() {
                // TODO Auto-generated method stub
               int width = view.getWidth();
               int hight = view.getHeight();
            }
        });
  • 3、在Activity的onWindowFocusChanged方法中获取,但是注意这个方法意为焦点发生变化时,即Activity的焦点发生变化就会回调这个方法,有可能调用多次。
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        // TODO Auto-generated method stub
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width = view.getWidth();
            int hight = view.getHeight();
        }
    }

你可能感兴趣的:(Android中高级开发工程师-面试记录-长期更新)