Android 滴滴面经
文章目录
- Android 滴滴面经
- 一面
- 二面
- 三面
一面
- Activity的启动的四种模式,四种启动模式的应用场景,单例模式的启动场景,我回答的是闹钟,反问:在单例模式下闹钟运行时点击back键,是回退到闹钟的主页面还是整个桌面?
LauchMode | Instance |
---|---|
standard | 默认场景,不需设置 |
singleTop | App接受到多条推送消息,点开不同消息,均由同一实例展示 |
singleTask | App的主页,无论那种业务场景下次回到此页面,都不应该保留之上的Activity |
singleInstance | 如App经常调用拨打电话号码,系统通讯录,不同App调用此类Activity时,首次创建实例,之后其他的App只能复用此实例,回到主页面 |
- Dialog怎么添加到Activity中,Toast怎么添加到Activity中,答:直接attach到Activity中,反问:那么当Toast的显示时间没到时,Activity退出时,Toast会怎么样?
:::info
- Window是一个抽象类,具体的实现类是PhoneWindow
- android系统里面,每个界面是对应一个window,window也是一个抽象的概念,他是以view的形式存在,在使用中,无法直接访问window,只能通过windowmanager才能访问window,每个window都是对应一个View和一个ViewRootImpl,ViewRootImpl是连接window和 WMS的桥梁,WMS的消息是通过ViewRootImpl转发给View的
- WindowManager继承自ViewManager(间接证明Window其实对应的是View),常用的只有三个方法addView ,updateView 和 removeView,windowmanager对应的实例是windowManagerImpl
- 各种window的不同,主要是token以及type的不同,app中控制window是通过WindowManager.LayoutParams去控制,eg:通过x,y,gravity去控制
- Activity的window添加过程
- 每个 Activity 内部都持有一个 window 对象,其实现为 PhoneWindow,在 Activity#attachContext 时创建
- PhoneWindow 的根布局为 DecorView,其包括 TitleView 和 ContentView,Activity setContentView 就是把布局添加到 ContentView
- 当 Activity 第一次回调 onResume 后,开始将 Activity 的窗口添加到 WMS,首先调用了 WindowManagerImpl,WindowManagerImpl 进一步调用进程单例的 WindowManagerGlobal
- WindowManagerGlobal 中创建了与 DecorView 对应的 ViewRootImpl,并将 DecorView 和 ViewRootImpl 记录下来
- 在 ViewRootImpl 中与 WMS 发生交互,应用端通过 WindowSession 调用 WMS,WMS 通过 IWindow 调用应用端
- WMS 中会对窗口进行权限、类型等检查,最终将应用窗口信息记录下来
- **推荐阅读:
Activity Window 创建及添加过程
Android Activity 创建 Window 及添加 View 流程分析
**Activity和dialog的窗口添加源码分析
- dialog窗口添加过程
- 首先获取windowManager实例,创建PhoneWindow对象,设置回调,对PhoneWindow设置windowManager实例
- 调用setContentView方法,把视图添加到PhoneWindow的DecorView中去
- Dialog#show中调用WindowManger#addView(DecorView,LayoutParams)方法去显示
:::
- Service两种启动方式和区别以及运行线程,应用场景,反问:既然Service默认在主线程执行耗时操作,我们为什么不直接在Activity中直接新建一个线程去执行耗时操作,还要搞一个Service里面在新建子线程去执行计算耗时操作?这两者有什么区别?多个Activity可以同时绑定一个Service吗?顺便讲讲IntentService
:::info
- 多个Activity可以同时绑定一个Service
- 假设TestService已经处于运行状态和ActivityA通过bindService已经绑定好了,那么ActivityB调用bindService时不会重新创建TestService的实例,所以也不会执行TestService的OnCreate方法,由于在ActivityA执行bindService的时候就已经执行了TestService的onBind回调方法而获取IBinder实例,并且该IBinder实例在所有的client之间是共享的,所以当ActivityB执行bindService的时候,不会执行其onBind回调方法,而是直接获取上次已经获取到的IBinder实例。并将其作为参数传入ActivityB的ServiceConnection的onServiceConnected方法中,标志着ActivityB与TestService建立了绑定连接,
- ActivityB销毁并Service并不会执行onUnbind方法进行销毁,只有在ActivityA销毁之后,ActivityA与Service解除了绑定,此时再没有client与Service处于连接绑定状态,这样Service就会执行onUnbind回调方法,表示没有client和我玩了,最后执行onDestroy回调方法。
- 推荐阅读:Android 多个Activity bindService的使用及Service生命周期
- HandlerThread
- HandlerThread继承自Thread,是一个线程
- HandlerThread中维护了一个Looper对象,在线程执行体内启动Loop.loop开启循环
- IntentService
- 是一个继承自Service的类,他通过在Service服务中开启HandlerThread的消息队列;
- 使用HandlerThread的Looper来生成一个Handler对象,在onStart()方法中发送消息,然后执行onHandleIntent()方法;
- onHandleIntent在子线程中执行,我们可以将耗时任务放在这个方法中实现。
- 推荐阅读:唠唠 HandlerThread 和 IntentService
:::
- Activity与Fragment的通信方式有哪些?setArgument()通信方式和接口回调通信方式有什么区别,扩展:Fragment与Fragment通信方式?
:::info
- Activity传递数据给Fragment
- 在Activity中创建Bundle数据包,调用Fragment实例的setArguments(bundle),从而将Bundle数据包传递给fragment
- 若Activity中包含自己管理的Fragment的引用(每个Fragment都有一个唯一的TAG或者ID,可以通过getFragmentManager.findFragmentByTag()或者findFragmentById()获得任何Fragment实例 ),可以直接通过引用访问所有的Fragment的public方法
- Fragment传递数据给Activity
- 在Fragment中定义一个内部回调接口,再让包含Fragment的Activity实现该回调接口,Fragment的创建方法中把Activity的引用传递过来,Fragment就可以通过回调方法来传输数据了
- Fragment与Fragment通信
- 在Fragment A中,调用Fragment B时,通过newInstance函数获取实例并传递参数,在newInstance函数中封装成bundle传递给FragmentB
- 直接在一个Fragment中调用另外一个Fragment中的方法,(ContentFragment) getActivity() .getFragmentManager().findFragmentById( R.id.content_fg);
- 用广播实现两个Fragment之间的通信,广播算是这里最灵活的通信方式了,LocalBroadcastManager
- 推荐阅读:fragment的使用(静态使用和动态使用)
:::
- Fragment的添加方式有哪些?静态加载和动态加载的区别?静态加载的fragment可以被动态替换吗?两个fragmentA 和 fragmentB 分别使用add 和 replace 加载其生命周期有什么区别
:::info
- 静态加载
- 定义fagment的xml布局文件
- 自定义Fragment类,继承Fragment类或者子类,同时实现onCreateView()方法,在方法中,通过LayoutInflater.inflate加载布局,直接返回view
- 在需要加载Fragment的Activity对应布局文件中的name属性设为全限定类名
- 动态加载
- 通过getSupportFragmentManager()获得FragmentManager对象
- 通过fm.beginTransaction()获得FragmentTransaction
- 调用add()方法或者replace()方法加载Fragment
- 最后通过commit方法提交事务
- fragment的onCreateView方法返回Fragment的UI布局 ,须要注意的是inflate()的第三个参数是false ,在Fragment内部实现中,会把布局添加到container 中,如果设置为true,那么会做两次重复添加,会抛出The specified child already has a parent异常
- 静态加载的fragment不可以被动态替换
- 当fragment不可见时,如果你要保留fragment中的数据以及view显示的状态,那么可以使用add操作,后续中针对不同的状态隐藏显示不同的fragment
- 优点:快,只是Fragment中的view的显示和隐藏
- 缺点:内存的保留太多的数据,容易导致OOM的风险
- 当fragment不可见时,不需要保留fragment中的数据以及view显示状态,那么可以使用replace。
- 优点:节省内存,不需要的数据能立即释放掉。
- 缺点:频繁创建fragment,会有很大的性能开销
- 如果当前Activity同一个id存在Fragment,replace传递的Fragment实例和已存在的Fragment实例不一样,replace操作会转换为 remove和add操作,即删除旧的Fragment,添加新的Fragment
- 旧的Fragment执行 onPause->onStop->onDestoryView->onDestory->onDetach
- 新的Fragment执行 onAttach->onCreate->onCreateView->onActivityCreated->onStart-onResume
:::
- Android本地保存的几种方案,sp的commit和apply的提交区别?
:::info
- sharedpreferences
- 内部存储,数据存储到手机内置存储器
- 外部存储,外部SD卡中存储数据
- SqlLite本地数据库存储
- ContentProvider
:::
- 线程池的有几种实现,讲一下阻塞队列,核心线程和非核心线程池和最大线程池数的区别,拒绝策略的作用,我们怎么确定一个线程池的线程数的多少
字节飞书
:::info
- AbortPolicy:被拒绝了就抛出异常
- CallerRunsPolicy:使用调用者所在的线程执行,就是哪里来回哪里去
- DiscardOldestPolicy:尝试去竞争第一个,失败了也不抛异常
- DiscardPolicy:默默丢弃,不抛异常
:::
- 三个线程A,B,C,怎么让线程A先执行,在让线程B执行,在线程C执行(jion,synchronized)反问:join的底层实现是什么?
:::info
- Join()方法,使调用此方法的线程wait() ,直到调用此方法的线程对象所在的线程(在例子中是子线程)执行完毕后被唤醒。其实是JVM的Thread执行完毕会自动执行一次notifyAll。
- 推荐阅读:多案例理解Object的wait,notify,notifyAll与Thread的sleep,yield,join等方法
Java Thread的join() 之刨根问底
:::
- lock和synchronized的区别和选择,sleep和wait的区别
- 线程安全的hashmap的实现原理
- 类加载过程的每一步骤都具体干了什么?
:::info
一个Java文件从编码完成到最终执行,一般主要包括两个过程
- 编译
- 运行
编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
运行,则是把编译生成的.class文件交给Java虚拟机(JVM)执行。
而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
类加载
类加载的过程主要分为三个部分:
- 加载
- 链接
- 初始化
而链接又可以细分为三个小部分:
- 验证
- 准备
- 解析
加载
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
这里有两个重点:
- 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
- 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
注:为什么会有自定义类加载器?
- 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
- 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456
解析
将常量池内的符号引用替换为直接引用的过程。
两个重点:
- 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
- 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
:::
二面
- 注解保留时期: 源文件保留, 编译期保留默认值,运行期保留在性能开销上有什么区别
:::info
- 源码期保留:只是保留在源码时期,编译器不对他做处理,只是一个标志
- 编译期保留:主要是APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,他用来在编译器扫描和处理注解,注解处理器以Java代码作为输入,生成.java文件作为输出
- 运行期保留:在代码中通过注解进行标记,运行时通过反射寻找标记进行某种处理
- 运行期主要是通过反射实现的,性能低
- 反射性能低的原因:
- Java反射性能不行,实际上是慢在getDeclaredMethod上,也就是根据Class对象到方法区里面查找类的方法定义过程
- Method#invoke方法会对参数做封装和解封装的操作
- 需要检查方法的可见性以及校验参数
- JIT无法优化,请求JVM去查找方法区中的方法定义,需使用JNI,开销相对较大
- 推荐阅读:大家都说 Java 反射效率低,你知道原因在哪里么
:::
- 动态代理实现原理
:::info
- 动态代理需要实现InvocationHandler接口,用来生成动态代理对象,代理对象需要实现newProxyInstance() : 根据指定的类装载器、一组接口 & 调用处理器 生成动态代理类实例,并最终返回和 invoke():动态代理对象调用目标对象的任何方法前,都会调用调用处理器类的invoke()
- 动态代理对象的生成是依托于Proxy.newProxyInstance(ProxyObject.getClass().getClassLoader(), ProxyObject.getClass().getInterfaces(),this);在
Proxy#newProxyInstance
方法种主要是通过Class c1 = getProxyClass(load,interfaces)
为Proxy
指定类加载器对象 & 一组interface,从而创建动态代理类 ,getProxyClass() : 将目标类的实现的接口加载到内存中,根据传入的接口&代理对象创建动态的代理类的字节码Constructor cons = cl.getConstructor(constructorParams);
通过反射机制获取动态代理类的构造函数,其参数类型是调用处理器接口类 return (Object) cons.newInstance(new Object[] { h }); 通过动态代理类的构造函数 创建 代理类实例(传入调用处理器对象 - 动态代理类$Proxy0 extends Proxy implements Subject ,在使用动态代理类对象时,才可以调用目标对象的同名方法,原因是调用了Proxy的h参数的invoke 其中h参数 = 在创建动态代理实例中newProxyInstance(ClassLoader loader, Class<?>[]interfaces,InvocationHandler h)传入的第3个参数InvocationHandler对象。
- 推荐阅读:Carson带你学设计模式:动态代理模式(Proxy Pattern)
:::
- 注解的实现原理
:::info
注解@interface 是一个实现了Annotation接口的 接口, 然后在调用getDeclaredAnnotations()方法的时候,返回一个代理$Proxy对象,这个是使用jdk动态代理创建,使用Proxy的newProxyInstance方法时候,传入接口 和InvocationHandler的一个实例(也就是 AnotationInvocationHandler ) ,最后返回一个代理实例。
期间,在创建代理对象之前,解析注解时候 从该注解类的常量池中取出注解的信息,包括之前写到注解中的参数,然后将这些信息在创建 AnnotationInvocationHandler时候 ,传入进去 作为构造函数的参数。
当调用该代理实例的获取值的方法时,就会调用执行AnotationInvocationHandler里面的逻辑,将之前存入的注解信息 取出来。
推荐阅读:java注解的本质以及注解的底层实现原理
:::
- 插桩有什么用?卡顿检测用插桩去确定每个方法执行的时间
:::info
插桩:就是将一段代码通过某种策略插入到另外一段代码或者替换另一段代码,这里的代码可以分为源码和字节码,而我们所说的一般是字节码插桩,就是在.class文件转为.dex文件之前,修改.class文件从而达到修改或者替换代码的目的。
应用场景:
1. 代码插入:通过插桩,我们扫描每一个class文件,并针对特定规则进行字节码修改从而达到监控每个方法耗时的目的。
2. <font style="color:rgb(51, 51, 51);">无痕埋点,性能监控</font>
插桩的主要实现步骤:
1. ASM是生成和转换已编译的Java类工具,就是我们插桩需要使用的工具。ASM提供了两种API来生成和转换已编译类,一个是核心API,以基于事件形式来表示类;另一个是树API,以基于对象形式来表示类。
2. Transform API ,这是android在将class转换为dex之前给我们预留的一个接口,在改接口中通过插件的方式来修改class文件
:::
- 类对象唯一的判定标准
:::info
- 在 JVM 中,类路径和类加载器唯一确定一个 Java 类
- 方法名、形参类型、形参个数、返回参数类型唯一确定一个 Java 类中的方法。
:::
三面
- Activity之间的通信
:::info
- 在Intent跳转时携带数据
- 借助类的静态变量可以实现
- 使用SharedPreference实现Activity之间的通信
- 使用SQLite来实现数据共享的通信,SQLite是Android提供的一种数据持久化操作的一种方式
- 直接使用文件File的读写的方式
:::
- SQLite数据库的升级,扩展Room数据库
:::info
- 继承一个SQLiteOpenHelper类,重写里面的两个方法
**<font style="color:rgb(153, 0, 0);">onCreate</font>**<font style="color:rgb(51, 51, 51);">(SQLiteDatabase database) </font>**<font style="color:rgb(153, 0, 0);">onUpgrade</font>**<font style="color:rgb(51, 51, 51);">(SQLiteDatabase database, </font>**<font style="color:rgb(68, 85, 136);">int</font>**<font style="color:rgb(51, 51, 51);"> oldVersion, </font>**<font style="color:rgb(68, 85, 136);">int</font>**<font style="color:rgb(51, 51, 51);"> newVersion) </font>
- 自己封装一些操作的Dao,
<font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);"> dbHelper = </font>**<font style="color:rgb(51, 51, 51);">new</font>****<font style="color:rgb(153, 0, 0);">DBHelper</font>**<font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">(MyApplication.getContext()); </font>**<font style="color:rgb(68, 85, 136);">SQLiteDatabase</font>**<font style="color:teal;">db</font><font style="color:rgb(51, 51, 51);">=</font><font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);"> dbHelper.getWritableDatabase(); </font>
- 执行数据库操作
:::
:::warning
Room优势:
- 是对SQLite的一层抽象,可以最大限度减少重复和容易出错的样板代码的注解
- 简化了数据库的迁移路径
- 针对编译期SQL语法检查
- API设计友好,更容易上手理解
Room的三个主要模块:
- **Entity:**Entity用来表示数据库中的一个表。需要使用@Entity(tableName = “XXX”)注解,其中的参数为表名。
- Dao: 数据库访问对象,用于访问和管理数据(增删改查)。在使用时需要@DAO注解
- Database: 它作为数据库持有者,用@Database注解和Room Database扩展的类
Room的使用:
- 创建Entity实体类,用来表示数据库中的一张表(table)
- 创建数据访问对象(Dao)处理增删改查
- 创建Room database
:::
@Database(entities = [UserEntity::class], version = 1)
abstract class UserDatabase : RoomDatabase() {abstract fun userDao(): UserDao
}
//通过Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()获取Database实例
@Database(entities = [UserEntity::class], version = 1)
abstract class UserDatabase : RoomDatabase() {abstract fun getUserDao(): UserDao
companion object {@Volatileprivate var INSTANCE: UserDatabase? = null
@JvmStaticfun getInstance(context: Context): UserDatabase {val tmpInstance = INSTANCEif (tmpInstance != null) {return tmpInstance}//锁synchronized(this) {val instance =Room.databaseBuilder(context, UserDatabase::class.java, "userDb").build()INSTANCE = instancereturn instance}}}
}