Android面试题知识点积累(四)

  • 泛型中类型擦除是什么

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。
举例:

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
        
System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。

  • 什么是二叉搜索树,什么是线段树

二叉搜索树的根节点编号为1,对于每个节点,假如其编号为N,它的左儿子编号为2N,右儿子编号为2N+1。因此,整个二叉搜索树的编号如下:


线段树,其本质也是一个二叉搜索树,区别在于线段树的每一个节点记录的都是一个区间,每个区间都被平均分为2个子区间,作为它的左右儿子。比如说区间[1,10],被分为区间[1,5]作为左儿子,区间[6,10]作为右儿子:


  • RxJava 的线程切换原理

安卓中线程切换无外乎就两种:Handler+Thread和runOnUIThread()。
RxJava 通过 RxAndroid 来切换到主线程运行,其实 RxAndroid 的核心就是 Handler。

  • 乐观锁悲观锁,悲观锁的使用场景

悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

  • 启动图标小圆点怎么做

应用启动图标未读消息数显示,使用BadgeUtil工具类 (效果如:QQ、微信、未读短信 等应用图标)依赖于第三方手机厂商(如:小米、三星)的Launcher定制,调用各个方法分别向各品牌手机发送消息数。
参考文章

  • 数据库范式

范式的英文名称是Normal Form,范式是关系数据库理论的基础,通常所用到的只是前三个范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)。
第一范式就是属性不可分割,每个字段都应该是不可再拆分的。比如name在国内不可再拆分,但在国外可拆分为firstname,lastname,就不满足第一范式。
第二范式就是要求表中要有主键,表中其他其他字段都依赖于主键,因此第二范式只要记住主键约束就好了。比如说有一个表是学生表,学生表中有一个值唯一的字段学号,那么学生表中的其他所有字段都可以根据这个学号字段去获取。
第三范式就是要求表中不能有其他表中存在的、存储相同信息的字段,通常实现是在通过外键去建立关联,因此第三范式只要记住外键约束就好了。比如学生表中不要加入系主任字段,不然该系1000个学生要存1000遍,而应该在学生表里存一个系编号,作为外键去查询。

  • View、SurfaceView、TextureView、GLSurfaceView 区别及使用场景

View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。
SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快。SurfaceView和View最本质的区别在于,surfaceView是在一个新起的单独线程中可以重新绘制画面而View必须在UI的主线程中更新画面。
GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。
TextureView:与SurfaceView一样也是在子线程中更新画面,而SurfaceView不能加上动画、平移、缩放,而TextureView可以。对于一些类似于坦克大战等需要不断更新画布的游戏来说,SurfaceView绝对是极好的选择。但是比如视频播放器或相机应用的开发,需要画面旋转缩放,TextureView则更加适合。

  • 图的广度、深度优先搜索算法

根据搜索路径的不同,我们可以将遍历图的方法分为两种:广度优先搜索和深度优先搜索。因为图中的任意顶点都可能与其他顶点相邻,所以在图的遍历中必须记录已被访问的顶点,避免重复访问。广度优先算法依靠队列来实现,深度优先算法依靠递归来实现,类似于二叉树深度的计算。

  • 二叉树的深度优先遍历与广度优先遍历

二叉树的深度优先遍历:
先、中、后序遍历。深度优先遍历则是先搜索一个结点的所有子孙结点,再去搜索这个结点的兄弟结点。

二叉树的广度优先遍历:
与深度优先遍历不同的是,广度优先遍历是先搜索所有兄弟和堂兄弟结点再搜索子孙结点。为同一层级先后的遍历。

深度优先遍历使用递归,广度优先遍历借助队列来实现。

  • 如何判断一个单链表有环

假设有两个学生A和B在跑道上跑步,两人从相同起点出发,假设A的速度为2m/s,B的速度为1m/s,结果会发生什么?
答案很简单,A绕了跑道一圈之后会追上B!
将这个问题延伸到链表中,跑道就是链表,我们可以设置两个指针,a跑的快,b跑的慢,如果链表有环,那么当程序执行到某一状态时,a==b。如果链表没有环,程序会执行到a==NULL,结束。

  • kotlin的lateinit 和by lazy 区别

1.lazy 只能用在val类型 修饰不可变的常量,lateinit 只能用在var类型 修饰变量
2.lateinit不能用在可空的属性上

  • 如何加载100M的图片却不撑爆内存?

BitmapRegionDecoder:区域解码器,可以用来解码一个矩形区域的图像,有了这个我们就可以自定义一块矩形的区域,然后根据手势来移动矩形区域的位置就能慢慢看到整张图片了。需要监听缩放,平移,获取需要加载的矩形区域,对该区域进行加载到内存。绘制也很简单,通过区域解码器解码一个矩形的区域,返回一个Bitmap对象,然后通过canvas绘制Bitmap。
参考

  • 最快的排序算法是哪个

下面是一个测试数据:



总结起来就是,快排的最坏时间虽然复杂度高,但是在统计意义上,这种数据出现的概率极小,而堆排序过程里的交换跟快排过程里的交换虽然都是常量时间,但是常量时间差很多。所以综合来说,最快的排序算法(从常见的上述排序中选择)是快速排序。

  • 数据结构中堆的概念

Full Binary Tree(满二叉树): 满二叉树的所有层,包括最后一层,都是满的。
Complete Binary Tree(完全二叉树): 在完全二叉树当中,除了最后一层之外,所有层的节点都是满的,且最后一层的节点也是从左到右的。优先填满左边的节点。
Heap(堆): 堆是一种完全二叉树。在树的性质之外,堆要求节点按照大小(父节点比子节点大/父节点比子节点小)来排列。
Min Heap(最小堆): 最小的键值总是在最前面。换句话说,所有的父节点都比他们的子节点小。
Max Heap(最大堆): 最大的键值总是在最前面。换句话说,所有的父节点都比他们的子节点大。


  • commit和commitAllowStateLoss有何区别

commit()和commitAllowingStateLoss()都调用了commitInternal(boolean allowStateLoss)。

        if (!allowStateLoss) {
            checkStateLoss();
        }

        if (mStateSaved) {
             throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }

如果commitAllowingStateLoss()传的是true所以忽略掉了检查,commit()传的是false,所以进行了检查。activity调用了onSaveInstanceState()之后,再commit一个事务就会出现该异常。那如果不想抛出异常,也可以很简单调用commitAllowingStateLoss()方法来略过这个检查就可以了。

参考

  • 执行requestLayout时,draw方法执行吗

如果只掉requestlayout的话,触发onMeasure和onLayout。如果在layout过程中发现l,t,r,b和以前不一样,那就会触发一次invalidate,否则不触发invalidate。

  • 描述请点击 Android Studio 的 build 按钮后发生了什么

使用Android Asset Packaging Tool(aapt) ,将AndroidManifest.xml和res下的资源编译生成R.java文件,这样java文件就可以去引用资源了 - 使用aidl 工具去生成对应的Java interfaces - 将src和通过aapt生成的R.java,.aidl文件通过javaC命令去生成.class 文件 - 使用dex tool 将class文件转化成Dalvik byte code.这时候要将所有class文件和第三方的jar包都包括。 - 所有没有编译过得图片和编译过的图片,.dex文件传给apkbuilder去打包成.apk - 最后采用zipalign tool 打入签名。

  • 权限管理底层的权限是如何进行 grant 的

参考

  • 为什么系统不建议在子线程访问UI

UI控件不是线程安全的,如果多线程并发访问UI控件可能会出现不可预期的状态。
那为什么系统不对UI控件的访问加上锁机制呢?
缺点有两个:
加上锁机制会让UI访问的逻辑变复杂;
锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行;
将于这两个缺点,最简单且高效的方法就是采用单线程模型来处理UI操作。

  • 数据库添加一个字段怎么做

1.升级数据库版本。
2.修改onCreate()里面的创建表的代码,添加新字段。
3.在onUpgrate()里面,要执行表修改,添加字段的逻辑。

  • Java 多线程引发的性能问题,怎么解决?

造成问题:
会消耗过多的CPU资源,如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
解决:
使用线程池,是指管理一组同构工作线程的资源池。
线程池的本质就是:有一个队列,任务会被提交到这个队列中。一定数量的线程会从该队列中取出任务,然后执行。任务的结果可以发回客户端、可以写入数据库、也可以存储到内部数据结构中,等等。

  • 谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
类锁

对静态方法使用synchronized关键字后,无论多线程访问单个对象还是多个对象的sychronieds块,都是同步的。

// 类锁:形式1 
public static synchronized void Method1() 
 
// 类锁:形式2
synchronized (Test.class)
对象锁

对实例方法使用synchronized关键字后,如果多个线程访问同个对象的sychronized块是同步的,访问不同对象是不同步的

// 对象锁:形式1(方法锁) 
public synchronized void Method1() 
 
// 对象锁:形式2(代码块形式)
synchronized (this)

方法锁
对方法使用synchronized关键字

内置锁

每个Java对象都可以用作一个实现同步的锁,线程进入同步代码块或方法时自动获得锁,退出时自动释放锁。内置锁是一个互斥锁,也就是说最多只有一个线程能够获得该锁。

重入锁:

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

参考

  • 谈谈NIO的理解

IO是一个同步阻塞的IO,由于同步阻塞,则在IO操作时,我们也不能做其他事情,很浪费资源。

服务端为了方便操作,会为每一个连接新建一个线程,一个线程处理一个客户端的数据交互。
但是当大量客户端同服务端连接时,会创建大量的线程,线程之间的切换会严重影响服务端性能,并且有时每一个的线程寿命并不长,有的甚至很短。
影响服务端性能的根本原因就是线程数量过多。

NIO(同步非阻塞IO)
NIO三大组件: 1.channel(进行读写操作)
2.selector(相当于一个管家,轮询检查数据的就绪状态)
3.buffer(读取数据的缓冲)

NIO,采用多路复用IO模型,改善了传统IO对线程资源的消耗,将channel注册在selector上,通过selecotor轮询检查数据的就绪状态,一旦有数据准备好,会通知channel去取数据。当多个客户端连接时,并不是为每个客户端创建一个线程为其服务,而是通过注册的方式,当有客户端准备好数据时,再去取数据。

  • Android动画框架实现原理

参考

  • App消息推送原理是什么?

在Android开发中,app消息推送的基本原理就是要在推送服务器和客户端之间建立连接,而连接的建立方式主要有两种pull和push。在实践过程中我们发现,相较于通过轮询(pull)的方式来获得消息通知,建立长连接(push)进行推送无论是对用户终端的电量消耗,还是对云端数据访问流量的耗费都比轮询要好,因此目前主流的app消息推送基本都是通过push的方式实现的。
推送的实现技术简单来说就是利用Socket维持Client和Server间的一个长连接。
实现消息推送可以有客户端轮询访问;SMS短信推送;Xmpp协议推送等。

  • 如何绕过9.0Http限制?

Android P以上的应用默认都被限制了明文流量的网络请求,非加密的流量请求都会被系统禁止掉。需要在AndroidManifest.xml文件中的application标签中设置:

android:usesCleartextTraffic="true"

android:usesCleartextTraffic 指示应用程序是否打算使用明文网络流量,例如明文HTTP。

  • JNI开发中怎么定位native crash

可使用add2line 和ndk-stack等工具分析JNI Crash的log。

特殊域变量(volatile)关键字

通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。

a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

//需要同步的变量加上volatile
private volatile int account = 100;

  • 今日头条的轻量级适配方案了解吗,说说原理

主流的屏幕适配方案:
1.smallestWidth适配:
指的是Android会识别屏幕可用高度和宽度的最小尺寸的dp值,然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件,如果没有找到对应尺寸,就寻找最近的。
2.AndroidAutoLayout
3.今日头条适配方案
如果每个 View 的 dp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp 值

屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度

在这个公式中我们要保证 屏幕的总 dp 宽度 和 设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了

当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density

这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配。

参考

推荐阅读更多精彩内容