Семь вопросов для Android-интервью, давайте начнем с простой разминки

Начните путь роста Nuggets! Это 3-й день моего участия в "Ежедневном новом проекте Nuggets · Февральское испытание обновления", нажмите, чтобы просмотреть подробности мероприятия

Это будет пик сезона вербовки (тяо) и найма (цао) золото три серебро четыре Группа за группой представители социальной элиты ищут свою следующую семью и в то же время начинают готовиться к собеседованию. В эти годы я также пережил много интервью, больших и малых, некоторые были опрошены, а некоторые были интервьюерами. определенное количество времени на заучивание каких-то стереотипных сочинений. Слушайте, они все вредят людям. Другой проходит собеседования, чтобы строить ракеты. В конце концов, есть всего несколько человек, которые накручивают гайки при поступлении на работу. Вещи, которые действительно спрашивают в Квалификационное собеседование — это те, с которыми я столкнусь в ходе фактического процесса разработки.Я расскажу о некоторых из них ниже.

Почему ArrayMap больше подходит для разработки под Android, чем HashMap

Как правило, мы привыкли использовать HashMap для хранения данных, таких как команды «ключ-значение» в проектах, поэтому HashMap часто является обязательной частью на собеседованиях по Android, но я помню, как на собеседовании меня спросили, использовал ли я когда-либо ArrayMap. только сказать, что у меня сложилось впечатление , Ведь HashMap является наиболее используемым, и тогда интервьюер спросил меня, что больше подходит для Android ArrayMap или HashMap.Понял, теперь просто сравните характеристики ArrayMap и HashMap

HashMap

  • Структура данных HashMap представляет собой структуру массива плюс связанный список.После jdk1.8 она изменена на структуру массива плюс связанный список плюс красно-черное дерево.
  • При вводе он сначала вычислит хэш-код ключа, а затем перейдет к массиву, чтобы найти индекс хэш-кода.Если данные пусты, сначала измените его размер, а затем проверьте соответствующее значение индекса (значение индекса = (массив length-1)&hashcode) Является ли он пустым, если он пустой, будет сгенерирована запись для вставки, если нет, будет судить, равны ли значения hascode и ключа, если они равны, будет перезаписывается, если нет, то возникает конфликт хэшей, и новая запись будет сгенерирована и вставлена ​​позади связанного списка, если в это время Если длина связанного списка больше 8, а длина массива больше 64 , сначала преобразуйте его в дерево и добавьте запись в дерево
  • При получении он также сначала проверяет, является ли значение нижнего индекса, соответствующее массиву, пустым.Если оно не пустое и ключ и hascode равны, значение возвращается напрямую.Если нет, то оценивается, является ли узел узлом дерева , и он возвращается в дереве. Соответствует записи, если нет, проходит весь связанный список, чтобы найти запись с тем же значением ключа и возвращает

Карта массива

  • Внутренне поддерживать два массива, один представляет собой массив типа int (mHashes) для сохранения хэш-кода ключа, а другой представляет собой массив объектов (mArray), который используется для сохранения значения ключа, соответствующего mHashes.
  • При размещении данных сначала используйте метод двоичного поиска, чтобы найти индекс индекса в mHashes для хранения хэш-кода, и сохраните ключ и значение в позициях, соответствующих индексу индекса<<1 и (index<<1)+1 в mArray
  • При получении данных также используется метод бинарного поиска для нахождения индекса нижнего индекса, соответствующего значению ключа, а затем значение вынимается из позиции (index<<1)+1 массива mArray

В сравнении

  • Когда HashMap сохраняет данные, независимо от того, сколько он хранит, он сначала генерирует объект Entry, что является пустой тратой памяти, в то время как ArrayMap просто вставляет данные в массив, не создавая новых объектов.
  • При хранении большого количества данных производительность ArrayMap не так хороша, как у HashMap, потому что ArrayMap использует метод бинарного поиска для поиска индексов, когда в данных слишком много значений индексов, это займет много времени. время, чтобы найти их.Кроме того, все данные должны быть перемещены обратно Затем вставьте данные, а HashMap нужно только вставить за связанным списком или деревом

Вот почему, при отсутствии такого большого объема данных, Android больше подходит для ArrayMap с точки зрения производительности.

Почему добавление данных в Arrays.asList сообщит об ошибке

Я задавал этот вопрос многим людям в начале, и нет недостатка в некоторых опытных воротилах, но они в основном этого не понимают.Это часто игнорируется людьми, и если какие-то проблемы игнорируются, часто возникают какие-то непредсказуемые проблемы. , Например, некоторым людям нравится использовать Arrays.asList для создания списка.

val dataList = Arrays.asList(1,2,3)
dataList.add(4)
复制代码

但是当我们往这个List里面add数据的时候,我们会发现,crash了,看到的日志是

изображение.png 不被支持的操作,这让首次遇到这样问题的人肯定是一脸懵,List不让添加数据了吗?之前明明可以的啊,但是之前我们创建一个List是这样创建的

изображение.png 它所在的包是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

изображение.png 是在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方法里面开始看

изображение.png

这个代码里面的两个框子,说明了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属性来控制软键盘弹出之后移动界面,让输入框不被遮挡,但是有些场景下,键盘永远都会挡住一些我们使用频次比较高的控件,比如现在我们有个登录页面,大概的样子长这样

изображение.png

它的布局文件是这样

<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>
复制代码

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

аааа.gif

现在希望可以键盘弹出之后,按钮也展示在键盘上面,这样就不用收起弹框以后才能点击按钮了,这样一来,windowSoftInputMode这一个属性已经不够用了,我们要想一下其他方案

  • 首先,需要让按钮也展示在键盘上方,那只能让布局整体上移把按钮露出来,在这里我们可以改变LayoutParam的bottomMargin参数来实现
  • 其次,需要知道键盘什么时候弹出,我们都知道android里面并没有提供任何监听事件来告诉我们键盘什么时候弹出,我们只能从其他角度入手,那就是监听根布局可视区域大小的变化

ViewTreeObserver

我们先获取视图树的观察者,使用addOnGlobalLayoutListener去监听全局视图的变化

bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {

}
复制代码

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

изображение.png

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

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
    }
}
复制代码

最终效果如下

aaaa2.gif

弹出后页面有个抖动是因为本身有个页面平移的效果,然后再去计算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 и посмотрим на исходный код внутри.

image.pngВ основном загляните внутрь красного прямоугольника, там есть синхронизированная синхронная блокировка, которая блокирует блок кода, мы называем его блоком кода 1, объектом блокировки является mDataLock, блок кода 1 сначала присваивает логическое значение postTask, а затем передать входящее Значение присваивается mPendingData, тогда мы знаем, что значение postTask истинно, за исключением случаев, когда выполняется первое, и оно будет ложным после того, как mPendingData имеет значение Предпосылка состоит в том, что mPendingData не был сброшен в NOT_SET , а затем мы следуем. Просматривая код, вы увидите, что код перейдет к потоку mPostValueRunnable следующим, давайте посмотрим на этот поток

image.png

Нашли ту же блокировку и заблокировали другой блок кода, который мы называем блоком кода 2. В этом блоке кода, после присвоения значения mPendingData в newValue, он сбрасывается в NOT_SET, Таким образом, postValue может принимать новое значение, поэтому это также является причиной того, что каждый postValue может получить значение при нормальных обстоятельствах, но давайте подумаем о сцене непрерывного postValue, мы знаем, что если synchronized изменяет блок кода, то, когда этот блок кода получает блокировку, он имеет приоритет, и блокировка будет снята только после того, как все выполнения будут завершены.Поэтому при непрерывном доступе к кодовому блоку 1 кодовый блок 2 не будет выполнен.Только когда кодовый блок 1 будет выполнен и блокировка снята, кодовый блок 2 будет выполнен, а в это время mPendingData уже является последним значением, а предыдущие значения все были перезаписаны, поэтому мы сказали что postValue потеряет данные.На самом деле это неправильно.Должно быть что postValue будет отправлять только последние данные .

Подведем итог

Вопросы интервью, упомянутые в этой статье, встречаются только в последние несколько лет.По оценкам, в дополнение к некоторым рутинным вопросам, часть интервью будет больше склоняться к знаниям Kotlin, Compose и Flutter, поэтому только накапливая с течением времени, позвольте себе только с более полным знанием, мы можем идти вверх по течению в соответствии с текущей жесткой рыночной тенденцией и не быть шлепнутыми на пляже

Supongo que te gusta

Origin juejin.im/post/7199537072302374969
Recomendado
Clasificación