补:《Android面试题思考与解答》2021年1月刊(一)

简介: 今年最后一篇,《Android面试题思考与解答21年1月刊》送给大家。

前言


今年最后一篇,《Android面试题思考与解答21年1月刊》送给大家。

提前预祝大家新年快乐


希望大家在新的一年:


财运亨通、牛气冲天、爱情工作双丰收、成为移动端发展的中坚力量!


介绍


《面试题思考与解答》系列期刊是将每月的知识点进行总结汇总。


要声明的一点是:面试题的目的不是为了让大家背题,而是从不同维度帮助大家复习,取长补短。


希望大家都能找到满意的工作。


以下为2021年1月刊内容。


简述Android系统启动流程


18.png


Android系统中启动的第一个进程是哪个?


(一般不会问这么深,可以作为知识扩展了解)


这个问题涉及到内核层的启动情况了。


在Kernel层,Android系统会启动linux内核


我们知道Android的核心系统服务都是基于Linux内核的,但是这个Linux内核到底该怎么理解呢?


Linux内核并不指的是Linux操作系统,内核只包括最基本的内存模型,进程调度,权限安全等等。操作系统值得是一个更广的概念,不光有内核,还有自己的设备驱动,应用程序框架以及一些应用程序软件等等。所以Android、Ubuntu等都是基于Linux内核的不同的操作系统。


所以启动了linux内核,就是启动了内核中内存模型,进程调度,安全机制,加载驱动等等,而linux内核中的功能都需要上册的虚拟机进行调用执行。


内核中就启动了系统中的第一个进程:


  • swapper进程(pid=0),该进程又称为idle进程, 系统初始化过程Kernel由无到有开创的第一个进程, 用于初始化进程管理、内存管理。并且会加载屏幕硬件,相机硬件等,这一步就会涉及到待会说到的HAL层了。


第一个用户级进程是哪个?


init进程是Android系统中用户空间的第一个进程,是所有用户进程的鼻祖。


启动入口在system/core/init/init.cpp文件中,init进程中主要做了这些事:

  • 孵化出用户守护进程。守护进程就是运行在后台的特殊进程,它不存在控制终端,会周期性处理一些任务。比如logd进程,就是用来进行日志的读写操作。
  • 启动了一些重要服务。比如开机动画
  • 孵化了Zygote进程。Zygote进程大家都或多或少了解一些了,我们所有的应用程序都是由它孵化出来的。
  • 孵化了Media Server进程,用来启动和管理整个C++ framework,比如相机服务(camera Service)。


Zygote进程做了些什么工作?


  • 创建服务端Socket,为后续创建进程通信做准备。
  • 加载虚拟机。没错,在Zygote进程中,会去加载下层的虚拟机。
  • fork了System Server进程。SystemServer进程大家应该都熟悉了吧,是Zygote fork的第一个进程,负责启动和管理Java Framework层,包括ActivityManagerService,PackageManagerService,WindowManagerService、binder线程池等等。这就涉及到APP的启动流程了,后续几篇会细说下。
  • fork了第一个应用进程——Launcher,以及后续的一些系统应用进程,这就到了最上面一层——应用层了。


Activity启动流程中,大部分都是用Binder通讯,为啥跟Zygote通信的时候要用socket呢


此题来自每日一问(https://www.wanandroid.com/wenda/show/10482


评论区主要有以下观点:


  • ServiceManager不能保证在zygote起来的时候已经初始化好,所以无法使用Binder。
  • Socket 的所有者是 root,只有系统权限用户才能读写,多了安全保障。
  • Binder工作依赖于多线程,但是fork的时候是不允许存在多线程的,多线程情况下进程fork容易造成死锁,所以就不用Binder了。


反射可以修改final类型成员变量吗?


final我们应该都知道,修饰变量的时候代表是一个常量,不可修改。那利用反射能不能达到修改的效果呢?


我们先试着修改一个用final修饰的String变量。


public class User {
    private final String name = "Bob";
    private final Student student = new Student();
    public String getName() {
        return name;
    }
    public Student getStudent() {
        return student;
    }
}
    User user = new User();
    Class clz = User.class;
    Field field1 = null;
    try{
        field1=clz.getDeclaredField("name");
        field1.setAccessible(true);
        field1.set(user,"xixi");
        System.out.println(user.getName());
    }catch(NoSuchFieldException e){
        e.printStackTrace();
    }catch(IllegalAccessException e){
        e.printStackTrace();
    }


打印出来的结果,还是Bob,也就是没有修改到。


我们再修改下student变量试试:


field1 = clz.getDeclaredField("student");
field1.setAccessible(true);
field1.set(user, new Student());
打印:
修改前com.example.studynote.reflection.Student@77459877
修改后com.example.studynote.reflection.Student@72ea2f77


可以看到,对于正常的对象变量即使被final修饰也是可以通过反射进行修改的。


这是为什么呢?为什么String不能被修改,而普通的对象变量可以被修改呢?


先说结论,其实String值也被修改了,只是我们无法通过这个对象获取到修改后的值。


这就涉及到JVM的内联优化了:


内联函数,编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。


简单的说,就是JVM在处理代码的时候会帮我们优化代码逻辑,比如上述的final变量,已知final修饰后不会被修改,所以获取这个变量的时候就直接帮你在编译阶段就给赋值了。


所以上述的getName方法经过JVM编译内联优化后会变成:


public String getName() {
        return "Bob";
    }


所以无论怎么修改,都获取不到修改后的值。


有的朋友可能提出直接获取name呢?比如这样:


//修改为public
public final String name = "Bob";
//反射修改后,打印user.name
field1=clz.getDeclaredField("name");
field1.setAccessible(true);
field1.set(user,"xixi");
System.out.println(user.name);


不好意思,还是打印出来Bob。这是因为System.out.println(user.name)这一句在经过编译后,会被写成:


System.out.println(user.name)
//经过内联优化
System.out.println("Bob")


所以:


反射是可以修改final变量的,但是如果是基本数据类型或者String类型的时候,无法通过对象获取修改后的值,因为JVM对其进行了内联优化。


那有没有办法获取修改后的值呢?


有,可以通过反射中的Field.get(Object obj)方法获取:


//获取field对应的变量在user对象中的值
System.out.println("修改后"+field.get(user));


反射获取static静态变量


说完了final,再说说static,怎么修改static修饰的变量呢?


我们知道,静态变量是在类的实例化之前就进行了初始化(类的初始化阶段),所以静态变量是跟着类本身走的,跟具体的对象无关,所以我们获取变量就不需要传入对象,直接传入null即可:


public class User {
 public static String name;
}
field2 = clz.getDeclaredField("name");
field2.setAccessible(true);
//获取静态变量
Object getname=field2.get(null);
System.out.println("修改前"+getname);
//修改静态变量
field2.set(null, "xixi");
System.out.println("修改后"+User.name);


如上述代码:


  • Field.get(null) 可以获取静态变量。
  • Field.set(null,object) 可以修改静态变量。


怎么提升反射效率


  • 1、缓存重复用到的对象


利用缓存,其实我不说大家也都知道,在平时项目中用到多次的对象也会进行缓存,谁也不会多次去创建。


但是,这一点在反射中尤为重要,比如Class.forName方法,我们做个测试:


long startTime = System.currentTimeMillis();
    Class clz = Class.forName("com.example.studynote.reflection.User");
    User user;
    int i = 0;
    while (i < 1000000) {
        i++;
        //方法1,直接实例化
        user = new User();
        //方法2,每次都通过反射获取class,然后实例化
        user = (User) Class.forName("com.example.studynote.reflection.User").newInstance();
        //方法3,通过之前反射得到的class进行实例化
        user = (User) clz.newInstance();
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));


打印结果:


1、直接实例化
耗时:15
2、每次都通过反射获取class,然后实例化
耗时:671
3、通过之前反射得到的class进行实例化
耗时:31


所以看出来,只要我们合理的运用这些反射方法,比如Class.forName,Constructor,Method,Field等,尽量在循环外就缓存好实例,就能提高反射的效率,减少耗时。


  • 2、setAccessible(true)


之前我们说过当遇到私有变量和方法的时候,会用到setAccessible(true)方法关闭安全检查。这个安全检查其实也是耗时的。


所以我们在反射的过程中可以尽量调用setAccessible(true)来关闭安全检查,无论是否是私有的,这样也能提高反射的效率。


  • 3、ReflectASM


ReflectASM 是一个非常小的 Java 类库,通过代码生成来提供高性能的反射处理,自动为 get/set 字段提供访问类,访问类使用字节码操作而不是 Java 的反射技术,因此非常快。

ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。


简单的说,这是一个类似反射,但是不同于反射的高性能库。他的原理是通过ASM库,生成了一个新的类,然后相当于直接调用新的类方法,从而完成反射的功能。


感兴趣的可以去看看源码,实现原理比较简单——https://github.com/EsotericSoftware/reflectasm


小总结:经过上述三种方法,我想反射也不会那么可怕到大大影响性能的程度了,如果真的发现反射影响了性能以及实际使用的情况,也许可以研究下,是否是因为没用对反射和没有处理好反射相关的缓存呢?


反射原理


如果我们试着查看这些反射方法的源码,会发现最终都会走到native方法中,比如

getDeclaredField方法会走到


public native Field getDeclaredField(String name) throws NoSuchFieldException;


那么在底层,是怎么获取到类的相关信息的呢?


首先回顾下JVM加载Java文件的过程:


  • 编译阶段,.java文件会被编译成.class文件,.class文件是一种二进制文件,内容是JVM能够识别的机器码。
  • .class文件里面依次存储着类文件的各种信息,比如:版本号、类的名字、字段的描述和描述符、方法名称和描述、是不是public、类索引、字段表集合,方法集合等等数据。
  • 然后,JVM中的类加载器会读取字节码文件,取出二进制数据,加载到内存中,并且解析.class文件的信息。
  • 类加载器会获取类的二进制字节流,在内存中生成代表这个类的java.lang.Class对象。
  • 最后会开始类的生命周期,比如连接、初始化等等。


而反射,就是去操作这个 java.lang.Class对象,这个对象中有整个类的结构,包括属性方法等等。


总结来说就是,.class是一种有顺序的结构文件,而Class对象就是对这种文件的一种表示,所以我们能从Class对象中获取关于类的所有信息,这就是反射的原理。



目录
相关文章
|
1月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
67 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
28天前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
26 8
|
26天前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
15 1
|
29天前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
32 3
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
1月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
35 2
|
1月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
27 1
|
27天前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
27 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
14 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
15 0