敞开生长之旅!这是我参与「日新计划 2 月更文挑战」的第 3 天,点击检查活动概况

立刻就要到招(tiao)聘(cao)旺季金三银四了,一批一批的社会精英在寻觅自己的下一家的一起,也开端着手为面试做准备,回想起自己这些年,也大巨细小经历过不少面试,有被面试过,也有当过面试官,其中也总结出了两个观点,一个便是不花必定的时刻背些八股文还真的不行,一些扯皮的话别去听,都是在害人,另一个便是面试造火箭,入职拧螺丝究竟都是少量,真实一场合格的面试问的东西,都是实践开发进程中会遇到的,下面我就说几个我遇到过的面试题吧

为什么ArrayMap比HashMap更适合Android开发

咱们一般习惯在项目傍边运用HashMap去存储键值队这样的数据,所以往往在android面试傍边HashMap是必问环节,但有次面试我记得被问到了有没有有过ArrayMap,我只能说有形象,究竟用的最多的仍是HashMap,然后那个面试官又问我,觉得Android里边更适合用ArrayMap仍是HashMap,我就说不上来了,由于也没看过ArrayMap的源码,后来回去看了下才给弄理解了,现在就简单对比下ArrayMap与HashMap的特色

HashMap

  • HashMap的数据结构为数组加链表的结构,jdk1.8之后改为数组加链表加红黑树的结构
  • put的时分,会先核算key的hashcode,然后去数组中寻觅这个hashcode的下标,假如数据为空就先resize,然后检查对应下标值(下标值=(数组长度-1)&hashcode)里边是否为空,空则生成一个entry刺进,否就判断hascode与key值是否别离都持平,假如持平则掩盖,假如不等就发生哈希冲突,生成一个新的entry刺进到链表后面,假如此时链表长度现已大于8且数组长度大于64,则先转成树,将entry添加到树里边
  • get的时分,也是先去查找数组对应下标值里边是否为空,假如不为空且key与hascode都持平,直接返回value,否就判断该节点是否为一个树节点,是就在树里边返回对应entry,否就去遍历整个链表,找出key值持平的entry并返回

ArrayMap

  • 内部维护两个数组,一个是int类型的数组(mHashes)保存key的hashcode,另一个是Object的数组(mArray),用来保存与mHashes对应的key-value
  • put数据的时分,首要用二分查找法找出mHashes里边的下标index来寄存hashcode,在mArray对应下标index<<1与(index<<1)+1的位置寄存key与value
  • get数据的时分,相同也是用二分查找法找出与key值对应的下标index,接着再从mArray的(index<<1)+1位置将value取出

对比

  • HashMap在寄存数据的时分,无论寄存的量是多少,首要是会生成一个Entry目标,这个就比较浪费内存空间,而ArrayMap只是把数据刺进到数组中,不必生成新的目标
  • 寄存很多数据的时分,ArrayMap功用上就不如HashMap,由于ArrayMap运用的是二分查找法找的下标,当数据多了下标值找起来时刻就花的久,此外还需求将一切数据往后移再刺进数据,而HashMap只需刺进到链表或许树后面即可

所以这便是为什么,在没有那么大的数据量需求下,Android在功用视点上比较适合用ArrayMap

为什么Arrays.asList后往里add数据会报错

这个问题我最初问过不少人,不缺乏一些资历比较深的大佬,可是他们根本都表示不清楚,这说明平时咱们研讨Glide,OkHttp这样的三方库源码比较多,而像一些比较基础的往往会被人忽略,而有些问题假如被忽略了,往往会发生一些捉摸不透的问题,比方有的人喜爱用Arrays.asList去生成一个List

val dataList = Arrays.asList(1,2,3)
dataList.add(4)

可是当咱们往这个List里边add数据的时分,咱们会发现,crash了,看到的日志是

七道Android面试题,先来简单热个身
不被支撑的操作,这让首次遇到这样问题的人必定是一脸懵,List不让添加数据了吗?之前分明能够的啊,可是之前咱们创立一个List是这样创立的

七道Android面试题,先来简单热个身
它所在的包是java.util.ArrayList里边,咱们看下里边的代码

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

是存在add办法的,咱们再回头再去看看asList生成的List

七道Android面试题,先来简单热个身
是在java.util.Arrays包里边的,而这儿边的ArrayList咱们看到了,并没有去完结List接口,所以也就没有add,get等办法,别的在kotlin里边,咱们会看到一个细节,当你敲完Arrays.asList的时分,编译器会提示你,能够转换成listof函数,而这个仍是咱们知道生成的list都是只能读取,不能往里写数据

Thread.sleep(0)到底“睡没睡”

记得在上上家公司,接手的第一个需求便是做一个动画,这个动画需求一个推延启动的功用,我那个时分想都没想加了个Thread.sleep(3000),后来被领导批了,不能够用Thread.sleep完结推延功用,那会还不太理解,后来知道了,Thread.sleep(3000)不必定真的暂停三秒,咱们来举个比方

println("start:${System.currentTimeMillis()}")
Thread(Runnable {
    Thread.sleep(3000)
    println("end:${System.currentTimeMillis()}")
}).start()

咱们在主线程先打印一条数据展现时刻,然后敞开一个子线程,在里边sleep三秒今后在打印一下时刻,咱们看下成果怎么

start:1675665421590
end:1675665424591

如同对了又如同没对,为什么是过了3001毫秒才打印出来呢?有的人会说,1毫秒而已,忽略嘛,那咱们把上面的代码改下再试试

println("start:${System.currentTimeMillis()}")
Thread(Runnable {
    Thread.sleep(0)
    println("end:${System.currentTimeMillis()}")
}).start()

现在sleep了0毫秒,那是不是两条打印日志应该是一样的呢,咱们看看成果

start:1675666764475
end:1675666764477

这下子给整不会了,分明sleep0毫秒,那么多出来的2毫秒是怎么回事呢?其实在Android操作系统中,每个线程运用cpu资源都是有优先级的,优先级高的才有资历运用,而操作系统则是在一个线程开释cpu资源今后,重新核算一切线程的优先级来重新分配cpu资源,所以sleep真实的意义不是暂停,而是在接下去的时刻内不参与cpu的竞赛,比及cpu重新分配完资源今后,假如优先级没变,那么继续履行,所以sleep(0)秒的真实含义是触发cpu资源重新分配

View.post为什么能够获取控件的宽高

咱们都知道在onCreate里边想要获取一个控件的宽高,假如直接获取是拿不到的

val mWith = bindingView.mainButton.width
val mHeight = bindingView.mainButton.height
println("按钮宽:$mWith,高:$mHeight")
......
按钮宽:0,高:0

而假如想要获取宽高,则必须调用View.post的办法

bindingView.mainButton.post {
    val mWith = bindingView.mainButton.width
    val mHeight = bindingView.mainButton.height
    println("按钮宽:$mWith,高:$mHeight")
}
......
按钮宽:979,高:187

很奇特,加个post就能够在相同的当地获取控件宽高了,至于为什么呢?咱们来分析一下

简单的来说

Activity生命周期,onCreate办法里边视图还在制作进程中,所以无法直接获取宽高,而在post办法中履行,便是在线程里边获取宽高,这个线程会在视图没有制作完结的时分放在一个等候行列里边,比及视图制作履行完毕今后再去履行行列里边的线程,所以在post里边也能够获取宽高

复杂的来说

咱们首要从View.post办法里边开端看

七道Android面试题,先来简单热个身

这个代码里边的两个框子,说明了post办法做了两件事情,当mAttachInfo不为空的时分,直接让mHandler去履行线程action,当mAttachInfo为空的时分,将线程放在了一个行列里边,从注释里边的第一个单词Postpone就能够知道,这个action是要推延进行,什么时分进行呢,咱们在慢慢看,既然是判断当mAttachInfo不为空才去履行线程,那咱们找找什么时分对mAttachInfo赋值,整个View的源码里边只要一处是对mAttachInfo赋值的,那便是在dispatchAttachedToWindow 这个办法里边,咱们看下

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    ...省掉部分源码...
    // Transfer all pending runnables.
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}

当走到dispatchAttachedToWindow这个办法的时分,mAttachInfo才不为空,也便是从这儿开端,咱们就能够获取控件的宽高级信息了,别的咱们顺着这个办法往下看,能够发现,之前的那个行列在这儿开端履行了,现在就关键在于,什么时分履行dispatchAttachedToWindow这个办法,这个时分就要去ViewRootIml类里边检查,发现只要一处调用了这个办法,那便是在performTraversals这个办法里边

private void performTraversals() {
        ...省掉部分源码...
    host.dispatchAttachedToWindow(mAttachInfo, 0);
        ...省掉部分源码...
    // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...省掉部分源码...
    performLayout(lp, mWidth, mHeight);
        ...省掉部分源码...
    performDraw();
}

performTraversals这个办法咱们就很熟悉了,整个View的制作流程都在里边,所以只要当mAttachInfo在这个环节赋值了,才干够得到视图的信息

IdleHandler到底有啥用

Handler是面试的时分必问的环节,除了问一下那四大组件之外,有的面试官还会问一下IdleHandler,那IdleHandler到底是什么呢,它是干什么用的呢,咱们来看看

Message next() {
...省掉部分代码...
    synchronized (this) {
        // If first time idle, then get the number of idlers to run.
        // Idle handles only run if the queue is empty or if the first message
        // in the queue (possibly a barrier) is due to be handled in the future.
        if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
            pendingIdleHandlerCount = mIdleHandlers.size();
        }
        if (pendingIdleHandlerCount <= 0) {
            // No idle handlers to run.  Loop and wait some more.
            mBlocked = true;
            continue;
        }
        if (mPendingIdleHandlers == null) {
            mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }
    // Run the idle handlers.
    // We only ever reach this code block during the first iteration.
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
        final IdleHandler idler = mPendingIdleHandlers[i];
        mPendingIdleHandlers[i] = null; // release the reference to the handler
        boolean keep = false;
        try {
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }
        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }
}

只要在MessageQueue中的next办法里边呈现了IdleHandler,作用也很明显,当音讯行列在遍历行列中的音讯的时分,当音讯现已处理完了,或许只存在推延音讯的时分,就会去处理mPendingIdleHandlers里边每一个idleHandler的事情,而这些事情都是通过办法addIdleHandler注册进去的

Looper.myQueue().addIdleHandler {
    false
}

addIdlehandler承受的参数是一个返回值为布尔类型的函数类型参数,至于这个返回值是true仍是false,咱们从next()办法中就能了解到,当为false的时分,事情处理完今后,这个IdleHandler就会从数组中删除,下次再去遍历履行这个idleHandler数组的时分,该事情就没有了,假如为true的话,该事情不会被删除,下次仍然会被履行,所以咱们按需设置。现在咱们能够利用idlehandler去解决上面讲到的在onCreate里边获取控件宽高的问题

Looper.myQueue().addIdleHandler {
    val mWith = bindingView.mainButton.width
    val mHeight = bindingView.mainButton.height
    println("按钮宽:$mWith,高:$mHeight")
    false
}

当MessageQueue中的音讯处理完的时分,咱们的视图制作也完结了,所以这个时分必定也能获取控件的宽高,咱们在IdleHandler里边履行了相同的代码之后,运转后的成果如下

按钮宽:979,高:187

除此之外,咱们还能够做点别的事情,比方咱们常说的不要在主线程里边做一些耗时的作业,这样会降低页面启动速度,严重的还会呈现ANR,这样的场景除了拓荒子线程去处理耗时操作之外,咱们现在还能够用IdleHandler,这儿举个比方,咱们在主线程中给sp塞入一些数据,然后在把这些数据读取出来,看看耗时多久

println(System.currentTimeMillis())
val testData = "aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhas" +
        "jkhdaabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
        "aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
        "aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
        "aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
        "aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd"
sharePreference = getSharedPreferences(packageName, MODE_PRIVATE)
for (i in 1..5000) {
    sharePreference.edit().putString("test$i", testData).commit()
}
for (i in 1..5000){
    sharePreference.getString("test$i","")
}
println(System.currentTimeMillis())
......运转成果
1676260921617
1676260942770

咱们看到在塞入5000次数据,再读取5000次数据之后,总共耗时大约20秒,一起也阻塞了主线程,导致的现象是页面一片空白,只要等读写操作完毕了,页面才展现出来,咱们接着把读写操作的代码用IdleHandler履行一下看看

Looper.myQueue().addIdleHandler {
    sharePreference = getSharedPreferences(packageName, MODE_PRIVATE)
    val editor = sharePreference.edit()
    for (i in 1..5000) {
        editor.putString("test$i", testData).commit()
    }
    for (i in 1..5000){
        sharePreference.getString("test$i","")
    }
    println(System.currentTimeMillis())
    false
}
......运转成果
1676264286760
1676264308294

运转成果仍然耗时二十秒左右,但区别在于这个时分页面不会受到读写操作的阻塞,很快就展现出来了,说明读写操作的确是比及页面烘托完才开端作业,上面进程没有放效果图首要是由于时刻太长了,会影响gif的体会,有兴趣的能够自己试一下

怎么让指定视图不被软键盘遮挡

咱们一般运用android:windowSoftInputMode特点来控制软键盘弹出之后移动界面,让输入框不被遮挡,可是有些场景下,键盘永远都会挡住一些咱们运用频次比较高的控件,比方现在咱们有个登录页面,大约的样子长这样

七道Android面试题,先来简单热个身

它的布局文件是这样

<RelativeLayout
    android:id="@+id/mainroot"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="100dp"
        android:src="@mipmap/ic_launcher_round" />
    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/ll_view1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="120dp"
        android:gravity="center"
        android:orientation="vertical">
        <EditText
            android:id="@+id/main_edit"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:hint="请输入用户名"
            android:textColor="@color/black"
            android:textSize="15sp" />
        <EditText
            android:id="@+id/main_edit2"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginTop="30dp"
            android:hint="请输入暗码"
            android:textColor="@color/black"
            android:textSize="15sp" />
        <Button
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginHorizontal="10dp"
            android:layout_marginTop="20dp"
            android:text="登录" />
    </androidx.appcompat.widget.LinearLayoutCompat>
</RelativeLayout>

在这样一个页面里边,由于输入框与登录按钮都比较靠页面下方,导致当输入完内容想要点击登录按钮时分,必须再一次关闭键盘才行,这样的操作在体会上就比较大打折扣了

七道Android面试题,先来简单热个身

现在希望能够键盘弹出之后,按钮也展现在键盘上面,这样就不必收起弹框今后才干点击按钮了,这样一来,windowSoftInputMode这一个特点现已不够用了,咱们要想一下其他计划

  • 首要,需求让按钮也展现在键盘上方,那只能让布局全体上移把按钮露出来,在这儿咱们能够改动LayoutParam的bottomMargin参数来完结
  • 其次,需求知道键盘什么时分弹出,咱们都知道android里边并没有提供任何监听事情来告知咱们键盘什么时分弹出,咱们只能从其他视点下手,那便是监听根布局可视区域巨细的改变

ViewTreeObserver

咱们先获取视图树的观察者,运用addOnGlobalLayoutListener去监听全局视图的改变

bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {
}

接下去便是要获取根视图的可视化区域了,怎么来获取呢?View里边有这么一个办法,那便是getWindowVisibleDisplayFrame,咱们看下源码注释就知道它是干什么的了

七道Android面试题,先来简单热个身

一大堆英文没必要都去看,只需求看最后一句就好了,大约意思便是获取能够展现给用户的可用区域,所以咱们在监听器里边加上这个办法

bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {
    val rect = Rect()
    bindingView.mainroot.getWindowVisibleDisplayFrame(rect)
}

当键盘弹出或许收起的时分,rect的高度就会跟着改变,咱们就能够用这个作为条件来改动bottomMargin的值,现在咱们增加一个变量oldDelta来保存前一个rect改变的高度值,用来做比较,完好的代码如下

var oldDelta = 0
val params:RelativeLayout.LayoutParams = bindingView.llView1.layoutParams as RelativeLayout.LayoutParams
val originBottom = params.bottomMargin
bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {
    val rect = Rect()
    bindingView.mainroot.getWindowVisibleDisplayFrame(rect)
    val deltaHeight = r.height()
    if (oldDelta != deltaHeight) {
        if (oldDelta != 0) {
            if (oldDelta > deltaHeight) {
                params.bottomMargin = oldDelta - deltaHeight
            } else if (oldDelta < deltaHeight) {
                params.bottomMargin = originBottom
            }
            bindingView.llView1.layoutParams = params
        }
        oldDelta = deltaHeight
    }
}

终究效果如下

七道Android面试题,先来简单热个身

弹出后页面有个抖动是由于自身有个页面平移的效果,然后再去核算layoutparam,假如不想抖动能够在布局外层套个scrollView,用smoothScrollTo把页面滑上去就能够了,有兴趣的能够业余时刻试一下

为什么LiveData的postValue会丢失数据

LiveData现已问世好多年了,大家都很喜爱用,由于它上手方便,一般知道塞数据用setValue和postValue,监听数据运用observer就能够了,然而实践开发中我遇到过好多人,一会这儿用setValue一会那里用postValue,或许交替着用,这种做法也不能严厉意义上说错,究竟运转起来的确没问题,可是这种做法确实是存在危险隐患,那便是接连postValue会丢数据,咱们来做个试验,接连setValue十个数据和接连postValue十个数据,收到的成果都别离是什么

var testData = MutableLiveData<Int>()
fun play(){
    for (i in 1..10) {
        testData.value = i
    }
}
mainViewModel.testData.observe(this) {
    println("收到:$it")
}
//履行成果
收到:1
收到:2
收到:3
收到:4
收到:5
收到:6
收到:7
收到:8
收到:9
收到:10

setValue十次数据都能够收到,现在把setValue改成postValue再来试试

var testData = MutableLiveData<Int>()
fun play(){
    for (i in 1..10) {
        testData.postValue(i)
    }
}

得到的成果是

收到:10

只收到了最后一条数据10,这是为什么呢?咱们进入postValue里边看看里边的源码就知道了

七道Android面试题,先来简单热个身
首要看红框里边,有一个synchronized同步锁锁住了一个代码块,咱们称为代码块1,锁的目标是mDataLock,代码块1做的事情先是给postTask这个布尔值赋值,接着把传进来的值赋给mPendingData,那咱们知道了,postTask除了第一个被履行的时分,值是true,结下去等mPendingData有值了今后就都为false,前提是mPendingData没有被重置为NOT_SET,然后咱们顺着代码往下看,会看到代码接下来就要到一个mPostValueRunnable的线程里边去了,咱们看下这个线程

七道Android面试题,先来简单热个身

发现相同的锁,锁住了另一块代码块,咱们称为代码块2,这个代码块里边恰好是把mPendingData的值赋给newValue今后,重置为NOT_SET,这样一来,postValue又能够承受新的值了,所以这也是正常情况下每次postValue都能够承受到值的原因,可是咱们想想接连postValue的场景,咱们知道假如synchronized假如润饰一段代码块,那么当这段代码块获取到锁的时分,就具有优先级,只要当悉数履行完今后才会开释锁,所以当代码块1接连被访问时分,代码块2是不会被履行的,只要比及代码块1履行完,开释了锁,代码块2才会被履行,而这个时分,mPendingData现已是最新的值了,之前的值现已悉数被掩盖了,所以咱们说的postValue会丢数据,其实说错了,应该是postValue只会发送最新数据

总结

这篇文章讲到的面试题还仅仅只是过去几年遇到的,现在面试估计除了一些常规问题之外,比重会更倾向于Kotlin,Compose,Flutter的知识点,所以只要不断的日积月累,让自己的知识点更加的全面,才干在现在竞赛激烈的行情趋势下逆流而上,不会被拍打在沙滩上