当前位置: 首页>后端>正文

带你闯关Android高级面试

一,Java 基础知识

大家都知道Java(包括Kotlin)是 Android 开发者必备的技术,也是后续高级话题的切入点。在这一点上,我们需要牢固的基础。

1 Java 的 char 是两个字节,是怎么存 Utf-8 的字符的?

一、面试官视角:这道题想考察什么?
  • 是否熟悉Java char和字符串(初级)
  • 是否了解字符的映射和存储细节(中级)
  • 是否能触类旁通,横向对比其他语言(高级)
二、题目剖析:

1、分别占多少字节?

char(字符数据类型)可以存储两个字节(byte),UTF-8可以存储一到三个字节

2、和Unicode什么关系?

char u8Test = "庆";

Integer.toHexString(u8Test);

// 打印出来是:0x5e 0x86两个字节,这两个字节就是Unicode码点,所以Unicode其实就是字符集
// ASCII码也是字符集,ASCII码与Unicode两者是兼容关系
// 字符集的作用是:完成字符到整数的映射
// 字符集不是编码,UTF-8是编码

3、如何存储字符?

一个字符(如笑脸)是两个字节(0xd83d,0xde00),这两个字节其实就是字符集(Unicode、ASCII码)码点,如果通过UTF-8编码,会转换成byte类型,也就是四个字节(11110000,10011111,10011000,10000000),但在Java char里面,会通过UTF-16进行编码,转换成byte类型对应的四个字节(11011000(d8),00111101(3d),11011110(de),00000000(00)),然后通过计算器存储(比如序列化或磁盘),其实无所谓存不存的,因为在Java char里面,存储的是UTF-16编码后的字节,跟UTF-8没有关系。

Tip:UTF-8与UTF-16的区别是:UTF-8最小的单位是一个字节,UTF-16最小的单位是两个字节。

(如果字符是一个A,它对应的是65,用ASCII码7个bit来表示,但在UTF-16里面也是需要两个字节的)
4、字符串长度

java:
String emoji = “笑脸”;
System.out.println(emoji.length()); // 长度为:2

// jdk9对Latin字符的存储空间做了优化,但字符串长度不等于字符数

python:
emoji = u"笑脸"
print(len(emoji)) // 长度为:1

// 这一点python就做的很好
三、题目结论:
  • Java char 不存UTF-8的字节,而是UTF-16
  • Unicode通用字符占两个字节,例如“中”
  • Unicode扩展字符集需要用一对char来表示,例如“笑脸”
  • Unicode是字符集,不是编码,作用类似于ASCII码
  • Java String的length不是字符数,而是char的个数

2 Java String 可以有多长?

编译期:65534 = 2^16-1 (JVM的常量池最多可放65535个项)
运行期:Integer.MAX_VALUE = 2147483647 = 2^31-1 由于java中字符是16位存储的,因此大概需要4GB内存

参考:https://segmentfault.com/a/1190000020381157?utm_source=sf-similar-article

3 Java 的匿名内部类有哪些限制?

  • 编写时没有名字。实际上编译器会指定名字,一般是 :外部类名称 + $ + 匿名类顺序 。
  • 没有构造函数
  • 只能继承一个父类或实现一个接口。
    匿名类由父类或接口直接派生,Java语法不允许同时继承父类和实现接口,kotlin可以。
  • 父类是非静态的类型,则需父类外部实例来初始化。
  • 如果定义在非静态作用域内,会引用外部实例。这是Android 内存泄露的一个原因之一
  • 只能使用外部作用域内的final变量。
    匿名内部类会持有一份该变量的引用,为了防止变量变化引起歧义,故要求final保持不变。1.8自动final
  • 创建时只有单一方法的接口可用lambda表达式。

参考:https://cloud.tencent.com/developer/article/1536432

4 怎样理解 Java 的方法分派?

Java的方法分派是三大特性之一的多态性的体现,分为静态分派与动态分派。
静态分派发生在在编译期阶段,依据调用者的静态类型(也就是:声明类型和方法参数类型)来确定方法执行版本;典型应用是方法重载,确定静态分派的动作实际上不是由虚拟机来执行的。
动态分派发生在运行时阶段,依据调用者的实际类型来确定方法执行版本;典型应用是方法重写,确定动态分派的动作是由虚拟机来执行的。
横向对比其他语言,Kotlin中与Java是一致的效果;而Groovy是动态语言,一切依据调用者的实际类型来确定方法执行版本。

参考:https://blog.csdn.net/chenliguan/article/details/109893206
参考:https://juejin.cn/post/6844904084835663885

5 Java 泛型的实现机制是怎样的?

泛型是 Java 开发中常用的技术,了解泛型的几种形式和实现泛型的基本原理,有助于写出更优质的代码。本文总结了 Java 泛型的三种形式以及泛型实现原理。

泛型本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。

Java 编译器通过如下方式实现擦除:

  • 用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;
  • 在恰当的位置插入强制转换代码来确保类型安全;
  • 在继承了泛型类或接口的类中插入桥接方法来保留多态性

Java 中的泛型有 3 种形式,泛型方法,泛型类,泛型接口。
Java 通过在编译时类型擦除的方式来实现泛型。擦除时使用 Object 或者界定类型替代泛型,同时在要调用具体类型方法或者成员变量的时候插入强转代码,为了保证多态特性,Java 编译器还会为泛型类的子类生成桥接方法。
类型信息在编译阶段被擦除之后,程序在运行期间无法获取类型参数所对应的具体类型。

参考:https://blog.csdn.net/chenliguan/article/details/110007427
参考:https://www.cnblogs.com/robothy/p/13949788.html

6 Activity 的 onActivityResult 使用起来非常麻烦,为什么不设计成回调?

参考:https://blog.csdn.net/qq_35345713/article/details/115695288

二,熟悉并发编程

并发编程是一个有难度的话题,要面试高级岗位,你需要熟悉语法规范对关键词的定义,也需要了解虚拟机的运行机制,还要对 JDK提供的并发工具类的用法和实现非常熟悉,不然,面试官就只能和你说再见了。

1 如何停止一个线程?

我们知道当调用 Thread 的 start()方法,执行完 run()方法后,或在 run()方法中 return,线程便会自然消亡。但是如果一些线程长时间的在后台运行,那么怎么去停止呢?下面介绍几种方法:
1、使用 volatile 关键字修饰 变量的方式终止

public class KeywordStop {
    public static void main(String[] args) throws InterruptedException {
        KeywordStopThread thread = new KeywordStopThread("a");
        new Thread(thread).start();
        Thread.sleep(3000);
        thread.isExit = true;
    }
}

class KeywordStopThread implements Runnable {
    private String name;

    public KeywordStopThread(String name) {
        this.name = name;
    }

    /*
    退出线程的标志
    volatile是因为可以保证有序性和可见性
    这里需要修改 isExit 以后里面对主内存可见。
     */
    public volatile boolean isExit = false;

    @Override
    public void run() {
        while (!isExit) {
            try {
                Thread.sleep(1000);
                System.out.println("线程 " + name + " 正在运行中");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("线程终止");
    }
}

2、使用 interrupt() 方式终止

public class InterruptStop {
    public static void main(String[] args) {
        InterruptStopThread mThread = new InterruptStopThread("a");
        mThread.start();
        try {
            Thread.sleep(500);
            System.out.println("线程终止");
            mThread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class InterruptStopThread extends Thread {
    private String name;

    public InterruptStopThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        // isInterrupted() 是否中断,中断退出,不中断继续执行
        while (!isInterrupted()) {
            System.out.println("线程 " + name + " 正在运行中");
        }
        System.out.println("线程终止");
    }
}

3、Stop 方法终止
序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

参考:https://cloud.tencent.com/developer/article/1591144
参考:https://juejin.cn/post/6844903862982148103

2 如何写出线程安全的程序?

多线程编程是软件开发中最棘手的问题之一。无论什么时候用多线程来操作同一份数据,都会出现并发问题。这就让写线程安全的代码成为一件非常困难的事情。本文将会从并发问题的本质出发,带你了解问题的根源,然后通过实例阐述保证线程安全的难点在哪里,最后提供常见的并发控制技术的原理和例子,希望能够帮助读者更好地写出线程安全的代码。

术语解释

  • 线程安全:指多线程编程时线程访问共享变量时的结果符合预期,不会出现并发问题。例如Java中的HashMap在多线程同时读写时会报错,所以它是线程不安全的,而ConcurrentHashMap能够支持多线程并发读写,所以它是线程安全的。

  • 竞争资源:指多线程编程中会被多个线程同时使用到的变量。例如全局变量、文件、数据库连接等。
    并发问题的本质
    我们平时会遇到的并发问题五花八门,但是归根结底可以归类为两类基本问题:

  • 不一致写(更新丢失)

  • 不一致读(数据冲突)

参考:https://zhuanlan.zhihu.com/p/471551847
参考:https://www.dounaite.com/article/62551eccae87fd3f795b6f9c.html
参考:https://blog.csdn.net/chenliguan/article/details/110688852

3 ConcurrentHashMap 如何实现并发访问?

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。
在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。保证HashEntry几乎是不可变的。
在 ConcurrentHashMap 中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry 对象链接成一个链表。由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。(头插法)

ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。

在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  • 1.减小请求 同一个锁的 频率。
  • 2.减少持有锁的 时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  • 1.用分离锁实现多个线程间的更深层次的共享访问。
  • 2.用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  • 3.通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性

在 JDK1.8 中,ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构,在锁的实现上,采用 CAS 操作和 synchronized 锁实现更加低粒度的锁,将锁的级别控制在了更细粒度的 table 元素级别,也就是说只需要锁住这个链表的首节点,并不会影响其他的 table 元素的读写,大大提高了并发度。

参考:https://blog.csdn.net/indeedes/article/details/123778425
参考:https://segmentfault.com/a/1190000022543426
参考:https://segmentfault.com/a/1190000023379814

4 AtomicReference 和 AtomicReferenceFieldUpdater 有何区别?

两者都是利用UnSafe提供的CAS机制实现在多线程场景下安全的、原子的数据更新操作,区别主要有

  • AtomicReference是针对一个引用的增删改查做到线程安全
  • AtomicReferenceFieldUpdater是针对一个对象的一个属性的增删改查做到线程安全
  • AtomicReferenceFieldUpdater操作属性的方式是通过反射,按属性名操作

参考:https://blog.csdn.net/qq_26323323/article/details/121185595
参考:https://juejin.cn/post/6844904013574438926

5 如何在 Android 当中写出优雅的异步代码?

三,JNI 编程细节

一个好的开发者,通常不会局限在一个编程语言体系当中。熟知底层语言的开发,会让我们更加清醒的知道 Java 虚拟机为我们做了什么,也能够让我们的选择合适语言完成需求,自然也能让我们成为面试官青睐的对象。

1 CPU 架构适配需要注意哪些问题?

参考:https://apkok.github.io/2021/04/27/%E9%9D%A2%E8%AF%95%E9%A2%98%E5%8D%81%E4%BA%8C%EF%BC%9ACPU%E6%9E%B6%E6%9E%84%E9%80%82%E9%85%8D%E9%9C%80%E8%A6%81%E6%B3%A8%E6%84%8F%E5%93%AA%E4%BA%9B%E9%97%AE%E9%A2%98%EF%BC%9F/

2 Java Native 方法与 Native 函数是怎么绑定的?

3 JNI 如何实现数据传递?

4 如何全局捕获 Native 异常?

5 只有 C、C++ 可以编写 JNI 的 Native 库吗?

四,Activity 相关知识?

Activity,最简单也最困难。我们可以围绕它聊上三天三夜,面试官问你类似的问题都是在给你突出亮点的机会。

1 Activity 的启动流程是怎样的?

2 如何跨App启动 Activity?有哪些注意事项?

3 如何解决 Activity 参数传递的类型安全以及接口复杂的问题?

4 如何在代码的任意位置为当前 Activity 添加 View?

5 如何实现类似微信右滑返回的效果?

五,Handler相关

Handler 怎么用大家自然都知道,它背后的实现细节由于涉及整个 Android App 的运行机制,却成了我们在应对面试时的巨大宝藏。

1 Android 中为什么非 UI 线程不能更新 UI?

2 Handler 发送消息的 Delay 靠谱吗?

3 主线程的 Looper 为什么不会导致应用 ANR ?

4 如何自己实现一个简单的 Handler - Looper 框架?

六,内存优化相关

内存优化是一个很宽泛的问题,但方法论就那么几条。善于总结是一个高级工程师的必备素养,面试官自然也不会放过这一点。

1 如何避免OOM的产生?

2 如何对图片进行缓存?

3 如何计算图片占用内存的大小?

七,插件化和热修复相关

插件化和热修复颇具“黑客”的味道,我们不甘于 Android 系统给我们的限制,我们勇于探索,勇于突破,哪怕遍体鳞伤。如果能成为真正的勇者,哪有找不到工作的道理。

1 如何规避 Android P 对访问私有 API 的限制?

2 如何实现换肤功能?

3 VirtualApk 如何实现插件化?

4 Tinker 如何实现热修复?试看

5 Shadow 如何实现插件化?

八,追求极致【优化相关】

永远不要说自己的程序是完美的,优化的工作越接近完美就越让人憔悴。你必须学会在理想和现实间找到平衡。你这样讲,面试官会懂的。

1 如何开展优化类的工作-1

2 如何开展优化类的工作-2

3 一个算法策略的优化Case

4 一个工程技术的优化 Case

九,拆解需求设计架构是我常做的事儿【架构设计相关】

万丈高楼平地起,实现 0到1 的突破,就要有合理的安排和规划。搞清楚我们想要什么,细化它,再设计出我们的系统架构,安排合适的人完成它,采用合适的手段监控它:一切尽在掌握。什么都别说了,快来办入职手续吧。

1 如何解答系统设计类问题?

2 如何设计一个短视频App

3 如何设计一个网络请求框架?


https://www.xamrdz.com/backend/3rc1868383.html

相关文章: