Android面试知识点总结(八)

作者: 奔跑吧李博 | 来源:发表于2021-04-18 23:22 被阅读0次
  • double相加会出现什么问题?

double 进行运算时,经常出现精度丢失的问题。比如:

        var a = 43.1
        var b = 0.09
        var result = a + b

得到结果为43.190000000000005,需使用BigDecimal来做运算,注:参数需传String类型

        var a = 43.1
        var b = 0.09
        var bd1 = BigDecimal(a.toString())
        var bd2 = BigDecimal(b.toString())
        var result = bd1.add(bd2).toDouble()
  • kotlin内联函数是什么?

Kotlin里使用关键 inline 来表示内联函数。
Java 方法执行的内存模型是基于 Java 虚拟机栈的:每个方法被执行的时候都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧入栈、出栈的过程。

也就是说每调用一个方法,都会对应一个栈帧的入栈出栈过程,如果你有一个工具类方法,在某个循环里调用很多次,那就会对应很多次的栈帧入栈、出栈过程。栈帧的创建及入栈、出栈都是有性能损耗的。

比如test方法中,重复调用多次sum方法:

fun test() {
        //多次调用 sum() 方法进行求和运算
        println(sum(1, 2, 3))
        println(sum(100, 200, 300))
        println(sum(12, 34))
    }

    /**
     * 求和计算
     */
    fun sum(vararg ints: Int): Int {
        var sum = 0
        for (i in ints) {
            sum += i
        }
        return sum
    }

为了避免多次调用 sum() 方法带来的性能损耗,最好改为调用一次方法,将方法内的代码插入到调用该方法的地方:

fun test2() {
        var sum = 0
        for (i in arrayOf(1, 2, 3)) {
            sum += i
        }
        println(sum)

        sum = 0
        for (i in arrayOf(100, 200, 300)) {
            sum += i
        }
        println(sum)

        sum = 0
        for (i in arrayOf(12, 34)) {
            sum += i
        }
        println(sum)
    }

用关键字 inline 标记函数,该函数就是一个内联函数。还是原来的 test() 方法,编译器在编译的时候,会自动把内联函数 sum() 方法体内的代码,替换到调用该方法的地方。查看编译后的字节码,会发现 test() 方法里已经没了对 sum() 方法的调用,凡是原来代码里出现 sum() 方法调用的地方,出现的都是 sum() 方法体内的字节码了。

将sum方法加入inline声明,即可实现test2方法同等的效率。

    inline fun sum(vararg ints: Int): Int {
        var sum = 0
        for (i in ints) {
            sum += i
        }
        return sum
    }
  • activity横竖屏重走生命周期,viewmodle为什么能长期持有数据

ViewModel 与 Activity 的生命周期几乎是一致的,但是在发生屏幕旋转时,旋转之前的 MainViewModel 与旋转后的 MainViewModel 内存地址一致,使用的是同一个viewModel对象。

而ViewModelStore 这个对象非常重要,ViewModel 的存储与获取都与和他有关,Activity 销毁重建也是从 ViewModelStore 中获取 ViewModel 的实例,并且这个实例一直是同一个对象。

参考:ViewModel : 对状态的持有和维护
ViewModel 凭什么能保存重建数据

  • 线上报错查日志怎么使用mapping文件

mapping.txt文件位于app模块根目录下,它是混淆前方法和字段名与混淆后代码间的映射。比如在bugly上有崩溃日志定位不到类和方法,需要上传mapping文件来处理。

  • let run also apply with等各自都在什么场景下使用
let:

经常和?.操作符号联合使用,替代一次或多次是否为空判断

var value = ""
        if (value != null) { }

        value?.let {}
apply 用于创建对象时初始化设置,并返回对象链式调用
        class User(var name: String = "", var age: Int = 0)

        User().apply {
            name = "小明"
            age = 18
        }
run:

1.全局函数run 替代 java中 { } 代码作用域
在一个作用域中,创建一个类对象不能定义重名,也许会定义为如下情况:

        var user1 = User()
        user1.age = 20

        var user2 = User()
        user2.age = 21

        var user3 = User()
        user3.age = 22

kotlin语法层面不支持代码作用域,可以通过run函数替代,创建多个作用域,使变量名能够只在自己的作用域中生效。

        kotlin.run {
            var user = User()
            user.age = 20
        }

        kotlin.run {
            var user = User()
            user.age = 21
        }

        kotlin.run {
            var user = User()
            user.age = 22
        }

2.用于省略函数返回类型

    fun getUser() : User {
        return User()
    }

    fun getUser2() = kotlin.run { User() }

在run函数当中它不仅仅只是一个作用域,他还有一个返回值。他会返回在这个作用域当中的最后一个对象。

例如现在有这么一个场景,用户领取app的奖励,如果用户没有登录弹出登录dialog,如果已经登录则弹出领取奖励的dialog。我们可以使用以下代码来处理这个逻辑。

        var dialogLogin = Dialog(this)
        var dialogPay = Dialog(this)
        var isLogined = false

        kotlin.run {
            if (isLogined) {
                dialogLogin
            } else {
                dialogPay
            }
        }.show()
also:使用场景与apply类似,都在方法体后返回对象,但是also在方法体中用it代替自身,apply用this代替自身。
with:消除对同一个变量多次引用,用于多次取值或赋值。
        with(user) {
            age = 10
            name = "小明"
            gender = 0
        }
  • 子线程如何更新Ui

在Activity的onCreate方法中写入代码,在子线程中操作view能正常执行起来。

        Thread {
            textview.text = "子线程中访问"
        }.start()

但是一旦改为如下,在onCreate中耗时200ms,就会出现异常:

        Thread {
            Thread.sleep(200)
            textview.text = "子线程中访问"
        }.start()
Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8509)

报错检测来自ViewRootIml类,当在任何要操作view的地方,都是先调用checkThread方法,检验当前线程是否为主线程。

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

那么onCreate中的子线程中为何不报错,解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。深入源码查探,ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。

总结:
ViewRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,ViewRootImpl是没有创建的,无法检测当前线程是否是UI线程,所以程序没有崩溃一样能跑起来,而之后修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经创建了,可以执行checkThread方法检查当前线程。所以在onResume执行之前,都可以进行子线程更新UI。

参考:Android中子线程真的不能更新UI吗?

  • bugly千分之二,这个计算比例是怎么来的

严格的讲,线上产品的崩溃率要看多指标的。
整体指标Q1 崩溃数/启动数 。
用户指标Q2 崩溃过的设备数 / 启动设备数。
去重指标Q3, 崩溃(去重) 总数 / 启动数(去重)。

  • 进程间通信方式
1、Bundle/Intent传递数据:

可传递基本类型,String,实现了Serializable或Parcellable接口的数据结构。Serializable是Java的序列化方法,Parcellable是Android的序列化方法,前者代码量少(仅一句),但I/O开销较大,一般用于输出到磁盘或网卡;后者实现代码多,效率高,一般用户内存间序列化和反序列化传输。

文件共享:

对同一个文件先后写读,从而实现传输,Linux机制下,可以对文件并发写,所以要注意同步。顺便一提,Windows下不支持并发读或写。

2、Messenger:

Messenger是基于AIDL实现的,服务端(被动方)提供一个Service来处理客户端(主动方)连接,维护一个Handler来创建Messenger,在onBind时返回Messenger的binder。

双方用Messenger来发送数据,用Handler来处理数据。Messenger处理数据依靠Handler,所以是串行的,也就是说,Handler接到多个message时,就要排队依次处理。

3、AIDL:

AIDL通过定义服务端暴露的接口,以提供给客户端来调用,AIDL使服务器可以并行处理,而Messenger封装了AIDL之后只能串行运行,所以Messenger一般用作消息传递。

通过编写aidl文件来设计想要暴露的接口,编译后会自动生成响应的java文件,服务器将接口的具体实现写在Stub中,用iBinder对象传递给客户端,客户端bindService的时候,用asInterface的形式将iBinder还原成接口,再调用其中的方法。

4、ContentProvider:

系统四大组件之一,底层也是Binder实现,主要用来为其他APP提供数据,可以说天生就是为进程通信而生的。自己实现一个ContentProvider需要实现6个方法,其中onCreate是主线程中回调的,其他方法是运行在Binder之中的。自定义的ContentProvider注册时要提供authorities属性,应用需要访问的时候将属性包装成Uri.parse("content://authorities")。还可以设置permission,readPermission,writePermission来设置权限。 ContentProvider有query,delete,insert等方法,看起来貌似是一个数据库管理类,但其实可以用文件,内存数据等等一切来充当数据源,query返回的是一个Cursor,可以自定义继承AbstractCursor的类来实现。

5、brocast:

android中的广播可以可以拿来跨进程通信

6、Socket:

学过计算机网络的对Socket不陌生,所以不需要详细讲述。只需要注意,Android不允许在主线程中请求网络,而且请求网络必须要注意声明相应的permission。然后,在服务器中定义ServerSocket来监听端口,客户端使用Socket来请求端口,连通后就可以进行通信。

  • ArrayMap 跟 SparseArray 在 HashMap 上面的改进

SparseArray:
SparseArray 比 HashMap 更省内存,在某些条件下性能更好,主要是因为它避
免了对 key 的自动装箱(int 转为 Integer 类型),它内部则是通过两个数组来进
行数据存储的,一个存储 key,另外一个存储 value,为了优化性能,它内部对
数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,我们从源
码中可以看到 key 和 value 分别是用数组表示:

private int[] mKeys;
private Object[] mValues;

同时,SparseArray 在存储和读取数据时候,使用的是二分查找法。也就是在 put
添加数据的时候,会使用二分查找法和之前的 key 比较当前我们添加的元素的
key 的大小,然后按照从小到大的顺序排列好,所以,SparseArray 存储的元素
都是按元素的 key 值从小到大排列好的。 而在获取数据的时候,也是使用二分
查找法判断元素的位置,所以,在获取数据的时候非常快,比 HashMap 快的多。

ArrayMap:
ArrayMap 利用两个数组,mHashes 用来保存每一个 key 的 hash 值,mArrray
大小为 mHashes 的 2 倍,依次保存 key 和 value。

1、如果 key 的类型已经确定为 int 类型,那么使用 SparseArray,因为它避免了
自动装箱的过程,如果 key 为 long 类型,它还提供了一个 LongSparseArray 来
确保 key 为 long 类型时的使用
2、如果 key 类型为其它的类型,则使用 ArrayMap。

  • 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

结论:java 中静态属性和静态方法可以被继承,但是不可以被重写而是被隐藏。
静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成,不需要
继承机制即可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的
静态方法或属性称之为"隐藏"。

class Father {
    static String name = "father";

    static int getAge() {
        return 40;
    }
}

class Son extends Father {

    public String getName() {
        name = "son"
    }

    //这里获取不到父类的getAge方法
}

  • Copy-On-Write 是什么?

在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写
操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向
原来内存指针指向新的内存,原来的内存就可以被回收掉。

  • Oom 是否可以 try catch ?

在 try 语句中声明了很大的对象,导致 OOM,并且可以确认 OOM 是由 try 语句
中的对象声明导致的,那么在 catch 语句中,可以释放掉这些对象,解决 OOM
的问题,继续执行剩余语句。如果 OOM 的原因不是 try 语句中的
对象(比如内存泄漏),那么在 catch 语句中会继续抛出 OOM。

  • Canvas.save()跟 Canvas.restore()的调用时机

save:用来保存 Canvas 的状态。save 之后,可以调用 Canvas 的平移、放缩、
旋转、错切、裁剪等操作。
restore:用来恢复 Canvas 之前保存的状态。防止 save 后对 Canvas 执行的操作
对后续的绘制有影响。
save 和 restore 要配对使用(restore 可以比 save 少,但不能多),如果 restore
调用次数比 save 多,会引发 Error。save 和 restore 操作执行的时机不同,就能
造成绘制的图形不同。

  • 直接在 Activity 中创建一个 thread 跟在 service 中创建一个 thread 之间的区别?

在 Activity 中被创建:该 Thread 的就是为这个 Activity 服务的,完成这个特定的
Activity 交代的任务,主动通知该 Activity 一些消息和事件,Activity 销毁后,该
Thread 也没有存活的意义了。
在 Service 中被创建:这是保证最长生命周期的 Thread 的唯一方式,只要整个
Service 不退出,Thread 就可以一直在后台执行,一般在 Service 的 onCreate()
中创建,在 onDestroy()中销毁。所以,在 Service 中创建的 Thread,适合长期
执行一些独立于 APP 的后台任务,比较常见的就是:在 Service 中保持与服务器
端的长连接。

  • LinearLayout、FrameLayout、RelativeLayout 性能对比,为什么?

RelativeLayout 会让子 View 调用 2 次 onMeasure,LinearLayout 在有 weight
时,也会调用子 View 2 次 onMeasure。为什么 Google 给开发者默认新建了个 RelativeLayout,而自己却在 DecorView 中用了个
LinearLayout?
因为 DecorView 的层级深度是已知而且固定的,上面一个标题栏,下面一个内
容栏。采用 RelativeLayout 并不会降低层级深度,所以此时在根节点上用
LinearLayout 是效率最高的。而之所以给开发者默认新建了个 RelativeLayout 是
希望开发者能采用尽量少的 View 层级来表达布局以实现性能最优,因为复杂的
View 嵌套对性能的影响会更大一些。

  • 在 Activity 中获取某个 View 的宽高

由于View的measure过程和Activity的生命周期方法不是同步执行的,如果View
还没有测量完毕,那么获得的宽/高就是 0。所以在 onCreate、onStart、onResume
中均无法正确得到某个 View 的宽高信息。解决方式如下:
1.Activity/View#onWindowFocusChanged:此时 View 已经初始化完毕,
当 Activity 的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进
行 onResume 和 onPause,那么 onWindowFocusChanged 也会被频繁地
调用。
2.view.post(runnable): 通过 post 可以将一个 runnable 投递到消息队列的
尾部,始化好了然后等待 Looper 调用次 runnable 的时候,View 也已经
初始化好了。
3.ViewTreeObserver#addOnGlobalLayoutListener:当 View 树的状态发生
改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方
法将被回调。

相关文章

网友评论

    本文标题:Android面试知识点总结(八)

    本文链接:https://www.haomeiwen.com/subject/vgchlltx.html