Android面试考点

作者: jingwenpeng | 来源:发表于2022-09-16 12:09 被阅读0次

    1、Activity、Dialog、PopupWindow、Toast 与Window的关系

    简单的从创建方式的角度来说一说:

    Activity。在Activity创建过程中所创建的PhoneWindow,是层级最小的Window,叫做应用Window,层级范围1-99。(层级范围大的Window可以覆盖层级小的Window)

    Dialog。Dialog的显示过程和Activity基本相同,也是创建了PhoneWindow,初始化DecorView,并将Dialog的视图添加到DecorView中,最终通过addView显示出来。

    但是有一点不同的是,Dialog的Window并不是应用窗口,而是子窗口,层级范围1000-1999,子Window的显示必须依附于应用窗口,也会覆盖应用级Window。这也就是为什么Dialog传入的上下文必须为Activity的Context了。

    PopupWindow。PopupWindow的显示就有所不同了,它没有创建PhoneWindow,而是直接创建了一个View(PopupDecorView),然后通过WindowManager的addView方法显示出来了。

    没有创建PhoneWindow,是不是就跟Window没关系了呢?

    并不是,其实只要是调用了WindowManager的addView方法,那就是创建了Window,跟你有没有创建PhoneWindow无关。View就是Window的表现形式,只不过PhoneWindow的存在让Window形象更立体了一些。

    所以PopupWindow也是通过Window展示出来的,而它的Window层级属于子Window,必须依附与应用窗口。

    Toast。Toast和PopupWindow比较像,没有新建PhoneWindow,直接通过addView方法显示View即可。不同的是它属于系统级Window,层级范围2000-2999,所以无须依附于Activity。

    四个比较下来,可以发现,只要想显示View,就会涉及到WindowManager的addView方法,也就用到了Window这个概念,然后会根据不同的分层依次显示覆盖到界面上。

    不同的是,Activity和Dialog涉及到了布局比较复杂,还会有布局主题等元素,所以用到了PhoneWindow进行一个解耦,帮助他们管理View。而PopupWindow和Toast结构比较简单,所以直接新建一个类似DecorView的View,通过addView显示到界面。

    2、onSaveInstanceState()什么时候会被调用呢?

    概括的讲,onSaveInstanceState 这个方法会在activity 将要被kill之前被调用以保存每个实例的状态,以保证在将来的某个时刻回来时可以恢复到原来的状态,但和activity 的生命周期方法onStop 和 onPause 不一样,与两者并没有绝对的先后调用顺序,或者说并非所有场景都会调用onSaveInstanceState 方法。

    那么onSaveInstanceState 方法何时会被调用呢,或者这么问,什么时候activity 会被系统kill 掉呢?

    有以下几种比较常见的场景:

    (1)用户主动按下home 键,系统不能确认activity 是否会被销毁,实际上此刻系统也无法预测将来的场景,比如说内存占用,应用运行情况等,所以系统会调用onSaveInstanceState保存activity状态 ;

    (2)activity位于前台,按下电源键,直接锁屏;

    (3)横竖屏切换;

    (4)activity B启动后位于activity A之前,在某个时刻activity A因为系统回收资源的问题要被kill掉,A通过onSaveInstanceState保存状态。

    换句话说,onSaveInstanceState()的调用遵循一个重要原则,即当系统存在“未经你许可”时销毁了我们的Activity,则onSaveInstanceState()会被系统调用,这是系统的职责,因为它必须要提供一个机会让用户保存数据。

    3、Android 数据持久化之 SharedPreferences

    Android之SharedPreferences内部原理浅析

    剖析 SharedPreference apply 引起的 ANR 问题

    总结:

    1. sSharedPrefsCache 是一个 ArrayMap<String,ArrayMap<File,SharedPreferencesImpl>>,它会保存加载到内存中的 SharedPreferences 对象,ContextImpl 类中并没有定义将 SharedPreferences 对象移除 sSharedPrefsCache 的方法,所以一旦加载到内存中,就会存在直至进程销毁。相对的,也就是说,SP 对象一旦加载到内存,后面任何时间使用,都是从内存中获取,不会再出现读取磁盘的情况

    2. SharedPreferences 和 Editor 都只是接口,真正的实现在 SharedPreferencesImpl 和 EditorImpl ,SharedPreferences 只能读数据,它是在内存中进行的,Editor 则负责存数据和修改数据,分为内存操作和磁盘操作

    3. 获取 SP 只能通过 ContextImpl#getSharedPerferences 来获取,它里面首先通过 mSharedPrefsPaths 根据传入的 name 拿到 File ,然后根据 File 从 ArrayMap<File, SharedPreferencesImpl> cache 里取出对应的 SharedPrederenceImpl 实例

    4. SharedPreferencesImpl 实例化的时候会启动子线程来读取磁盘文件,但是在此之前如果通过 SharedPreferencesImpl#getXxx 或者 SharedPreferences.Editor 会阻塞 UI 线程,因为在从 SP 文件中读取数据或者往 SP 文件中写入数据的时候必须等待 SP 文件加载完

    5. 在 EditorImpl 中 putXxx 的时候,是通过 HashMap 来存储数据,提交的时候分为 commit 和 apply,它们都会把修改先提交到内存中,然后在写入磁盘中。只不过 apply 是异步写磁盘,而 commit 可能是同步写磁盘也可能是异步写磁盘,在于前面是否还有写磁盘任务。对于 apply 和 commit 的同步,是通过 CountDownLatch 来实现的,它是一个同步工具类,它允许一个线程或多个线程一致等待,直到其他线程的操作执行完之后才执行

    6. SP 的读写操作是线程安全的,它对 mMap 的读写操作用的是同一把锁,考虑到 SP 对象的生命周期与进程一致,一旦加载到内存中就不会再去读取磁盘文件,所以只要保证内存中的状态是一致的,就可以保证读写的一致性

    注意事项以及优化建议
    1. 强烈建议不要在 SP 里面存储特别大的 key/value ,有助于减少卡顿 / ANR

    2. 请不要高频的使用 apply,尽可能的批量提交;commit 直接在主线程操作,更要注意了

    3. 不要使用 MODE_MULTI_PROCESS

    4. 高频写操作的 key 与高频读操作的 key 可以适当的拆分文件,以减少同步锁竞争

    5. 不要连续多次 edit,每次 edit 就是打开一次文件,应该获取一次 edit,然后多次执行 putXxx,减少内存波动,所以在封装方法的时候要注意了

    6. apply 在 QueueWork 维护的单线程池调用,虽然是异步的但是可能会阻塞 Service.onStop 和 Activity.onPause 方法,可能会导致 ANR

    ANR 容易发生的地方:

    1. sp.getXxx,首先会调用 awaitLoadedLocked 等待首次 sp 文件创建与读取操作完成

    2. sp.apply 虽然是异步的但是可能会在 Service Activity 等生命周期期间 mcr.writtenToDiskLatch.await() 等待过久

    3. sp.commit 最终会调用 sp.writeToFile 方法,很耗时

    4. ContextImpl.getSharedPreferences,主线程直接调用的话,如果 sp 文件很大处理时间也就会变成

    4、Activity的启动过程

    image.png

    应用启动过程

    1. Launcher通过Binder进程间通信机制通知AMS,它要启动一个Activity

    2. AMS通过Binder进程间通信机制通知Launcher进入Paused状态

    3. Launcher通过Binder进程间通信机制通知AMS,它已经准备就绪进入Paused状态,于是AMS就创建一个新的线程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行

    4. ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给AMS,以便以后AMS能够通过这个Binder对象和它进行通信

    5. AMS通过Binde进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了

    5、Service生命周期

    startService() --> onCreate() --> onStartCommand() --> Service running --> onDestory()

    bindService() --> onCreate() --> onBind() --> Service running --> onUnbind() --> onDestory()

    onCreate():

    系统在Service第一次创建时执行此方法,来执行只运行一次的初始化工作,如果service已经运行,这个方法不会调用。

    onStartCommand():

    每次客户端调用startService()方法启动该Service都会回调该方法(多次调用),一旦这个方法执行,service就启动并且在后台长期运行,通过调用stopSelf()或stopService()来停止服务。

    onBind():

    当组件调用bindService()想要绑定到service时,系统调用此方法(一次调用),一旦绑定后,下次在调用bindService()不会回调该方法。在你的实现中,你必须提供一个返回一个IBinder来使客户端能够使用它与service通讯,你必须总是实现这个方法,但是如果你不允许绑定,那么你应返回null

    onUnbind():

    当前组件调用unbindService(),想要解除与service的绑定时系统调用此方法(一次调用,一旦解除绑定后,下次再调用unbindService()会抛异常)

    onDestory():

    系统在service不在被使用并且要销毁的时候调用此方法(一次调用)。service应在此方法中释放资源,比如线程,已注册的监听器、接收器等等。

    三种情况下Service的生命周期
    1. startService / stopService

      生命周期:onCreate --> onStartCommand --> onDestory

      如果一个Service被某个Activity调用Context.startService 方法启动,那么不管是否有Activity使用bindService绑定或unbindService解除绑定到该Service,该Service都在后台运行,直到被调用stopService,或自身的stopSelf方法。当然如果系统资源不足,Android系统也可能结束服务,还有一种方法可以关闭服务,在设置中,通过应用 --> 找到自己应用 --> 停止。

      注意:

      第一次startService会触发onCreate和onStartCommand,以后在服务运行过程中,每次startService都只会触发onStartCommand

      不论startService多少次,stopService一次就会停止服务

    2. bindService / unbindService

      生命周期:onCreate --> onBind --> onUnbind --> onDestory

      如果一个Service在某个Activity中被调用bindService方法启动,不论bindService被调用几次,Service的onCreate方法只会执行一次,同时onStartCommand方法始终不会调用。

      当建立连接后,Service会一直运行,除非调用unbindService来解除绑定、断开连接或调用该Service的Context不存在了(如Activity被finish --- 即通过bindService启动的Service的生命周期依附于启动它的Context),系统会在这时候自动停止该Service。

      注意:

      第一次bindService会触发onCreate和inBind,以后在服务运行过程中,每次bindService都不会触发任何回调

    3. 混合型

      当一个Service再被启动(startService)的同时又被绑定(bindService),该Service将会一直在后台运行,不管调用几次,onCreate方法始终只会调用一次,onStartCommand的调用次数与startService调用的次数一致(使用bindService方法不会调用onStartCommand)。同时,调用unBindService将不会停止Service,必须调用stopService或Service自身的stopSelf来停止服务。

    三种情况下的应用场景

    如果你只是想启动一个后台服务长期进行某项任务,那么使用startService便可以了。

    如果你想与正在运行的Service取的联系,那么有两种方法,一种是使用broadcast,另外是使用bindService。前者的缺点是如果交流较为频繁,容易造成性能上的问题,并且BroadcastReceiver本身执行代码的时间是很短的(也许执行到一半,后面的代码便不会执行),而后者则没有这些问题,因此我们肯定选择使用bindService(这个时候便同时使用了startService和bindService了,这在Activity中更新Service的某些运行状态是相当有用的)

    如果你的服务只是公开了一个远程接口,供连接上的客户端(Android的Service是C/S架构)远程调用执行方法。这个时候你可以不让服务一开始就运行,而只用bindService,这样在第一次bindService的时候才会创建服务的实例运行它,这会节约很多系统资源,特别是如果你的服务是Remote Service,那么该效果会越明显。

    6、BroadcastReceiver

    应用场景:

    1. 不同组件之间的通信(包括应用内 / 不同应用之间)

    2. 与Android系统在特定情况下的通信,如当电话呼入时,网络可用时

    3. 多线程通信

    实现原理
    • 使用了观察者模式:基于消息的发布/订阅事件模型。

    • 模型中有三个角色:消息订阅者(广播接收者)、消息发布者(广播发布者)和消息中心(AMS,即Activity Manager Service)

      [图片上传中...(image-100ae0-1663300893576-0)]

    • 原理描述

      1. 广播接收者通过Binder机制在AMS注册

      2. 广播发送者通过Binder机制向AMS发送广播

      3. AMS根据广播发送者要求,在已注册列表中,寻找合适的广播接收者,寻找依据:IntentFilter / Permission

      4. AMS将广播发送到合适的广播接收者相应的消息循环队列

      5. 广播接收者通过消息循环拿到此广播,并回调onReceive()

      注意:广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有没有接收者接收,也不确定接收者何时能接受到。

      广播的类型主要分为5类:

      • 普通广播(Normal Broadcast)

      • 系统广播(System Broadcast)

      • 有序广播(Ordered Broadcast)

      • 粘性广播(Sticky Broadcast)Android 5.0 & API 21中已经失效

      • App应用内广播(Local Broadcast)

    动态广播最好在Activity的onResume()注册,onPause()注销,否则会导致内存泄漏,当然,重复注册和重复注销也不允许。

    7、ContentProvider

    作用

    进程间进行数据交互&共享,即跨进程通信

    image.png
    原理

    ContentProvider的底层采用Android中的Binder机制

    统一资源标识符(RUI)

    作用:唯一标识ContentProvider & 其中的数据,外界进程通过URI找到对应的ContentProvider & 其中的数据,再进行数据操作

    具体使用:

    URI分为系统预置 & 自定义,分别对应系统内置的数据(如通讯录、日程表等等)和自定义数据库。

    8、Context

    Android应用模型是基于组件的应用设计模式,组件的运行要有一个完整的Android工程环境。在这个工程环境下,Activity、Service等系统组件才能够正常工作,而这些组件并不能采用普通的Java对象创建方式,new一下就能创建实例了,而是要有它们各自的上下文环境,也就是Context,Context是维持Android程序中各组件能够正常工作的一个核心功能类。

    如何生动形象的理解Context?

    一个Android程序可以理解为一部电影,Activity、Service、BroadcastReceiver和ContentProvider这四大组件就好比戏了的四个主角,它们是剧组(系统)一开始定好的,主角并不是大街上随便拉个人(new 一个对象)都能演的。有了演员当然也得有摄像机拍摄啊,它们必须通过镜头(Context)才能将戏传给观众,这也就正对应说四大组件必须工作在Context环境下。那么Button、TextView等等控件就相当于群演,显然没那么重用,随便一个路人甲都能演(可以new一个对象),但是它们也必须在面对镜头(工作在Context环境下),所以Button mButtom = new Button(context) 是可以的。

    [图片上传中...(image-df38f2-1663300893577-4)]

    它有两个具体实现类:ContextImpl和ContextWrapper。

    其中ContextWrapper类,是一个包装类而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象指定真正的Context对象,调用ContextWrapper的方法都会被转向其包含的真正的Context对象。ContextThemeWrapper类,其内部包含了与主题Theme相关的接口,这里所说的主题就是指在AndroidManifest,xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,所以Service直接继承与ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所有函数,应用程序中所调用的各种Context类的方法,其实现均来源于该类。Context得两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。 Activity、Application、Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。

    一个应用程序有几个Context?

    在应用程序中Context的具体实现子类就是:Activity、Service和Application。那么Context数量=Activity数量+Service数量+1。那么为什么四大组件中只有Activity和Service持有Context呢?BroadcastReceiver和ContextPrivider并不是Context的子类,它们所持有的Context都是其他地方传过去的,所以并不计入Context总数。

    Context的作用域

    虽然Context神通广大,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。由于Context的具体实例是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会报错。

    [图片上传中...(image-d795d0-1663300893576-2)]

    从上图我们可以发现Activity所持有的Context的作用域最广,无所不能,因此Activity继承至ContextThemeWrapper,而Application和Service继承至ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大。着重讲一下不推荐使用的两种情况:

    1. 如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错:

      android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

      这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式都不推荐,Service同Application。

    2. 在Application和Service中去LayoutInflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用,这种方式也不推荐使用。

    一句话总结:凡是跟UI相关的,都应该使用Activity作为Context来处理;其他的一些操作,Service、Activity、Application等实例都可以,当然了注意Context引用的持有,防止内存泄露。

    如何获取Context?

    有四种方法:

    1. View.getContext 返回当前View对象的Context对象,通常是当前正在展示的Activity对象。

    2. Activity.getApplicationContext 获取当前Activity所在的进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。

    3. ContextWrapper.getBaseContext() 用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用的不多,也不建议使用。

    4. Activity.this 返回当前Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

    getApplication()和getApplicationContext()的区别?

    其内存地址是一样的。Application本身就是一个Context,这里获取getApplicationContext得到的结果就是Application本身的实例。getApplication方法的语义性很强,就是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application,但是如果在一些其他的场景,比如BroadcastReceiver中也想获取Application实例,这时就可以借助getApplicationContext方法了。

    9、Android APK编译打包流程

    [图片上传中...(image-e1bab6-1663300893576-1)]

    1. AAPT(Android Asset Packaging Tools)工具会打包应用中的资源文件,如AndroidManifest.xml、layout布局中的xml等,并将xml文件编译成二进制形式,当然assets文件夹中的文件不会被编译,图片以及raw文件夹中的资源也会保持原有的形态,需要注意的是raw文件夹中的资源也会生成资源ID。AAPT编译完成后会生成R.java文件。

    2. AIDL工会将所有的aidl接口转换为java接口。

    3. 所有的Java源代码、R文件、接口都会编译器编译成.class文件。

    4. Dex工具会将上述产生的.class文件以及第三方库和其他class文件转化为dex(Dalvik虚拟机可执行文件)文件,dex文件最终会被打包进APK文件。

    5. apkbuilder会把编译后的资源和其他资源文件同dex文件一起打入APK中。

    6. 生成APK文件之后,,需要对其签名才能安装到设备上,平时测试都会使用debug keystore,当发布应用时必须使用release版的keystore对应用进行签名。

    7. 如果对APK正式签名,还需要使用zipalign工具对APK进行对齐操作,这样做的好处是当应用运行时能提高速度,但是会相应的增加内存开销。

    总结:编译 --> DEX --> 打包 --> 签名和对齐

    10、Window、Activity、DecorView以及ViewRoot之间的关系

    Activity

    Activity并不负者视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window以及View进行交互。

    Window

    Window是视图的承载器,内部持有一个DecorView,而这个DecorView才是view的跟布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局。Window通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。

    DecorView

    DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。

    DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面是内容栏。在Activity中通过setContentView所设置的布局文件其实就是被加到内容栏之中的,成为其唯一子View。

    ViewRoot

    ViewRoot可能比较陌生,但是其作用非常重大。所有View的绘制以及事件分发等交互都是通过它来执行或传递的。

    ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量、布局、绘制)均通过ViewRoot来完成。

    ViewRoot并不属于View树的一份子。从源码实现上来看,它既是非View的子类,也是非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件,按键事件、界面刷新等事件都是通过ViewRoot来进行分发的。

    [图片上传中...(image-6f8e3a-1663300893576-3)]

    总结

    Activity就像个控制器,不负责视图部分。Window像个承载器,装着内部视图。DecorView就是个顶级视图,是所有View的最外层布局。ViewRoot像个连接器,负者沟通,通过硬件感知来通知视图,进行用户之间的交互。

    11、Assets目录与res目录的区别

    assets目录与res下的raw、drawable目录一样,也可用来存放资源文件,但它们三者区别如下:


    image.png

    res/raw和assets的区别:

    res/raw中的文件会被映射到R.java文件中,访问的时候直接使用资源ID即可,assets文件夹下的文件不会被映射到R文件中,

    访问的时候需要AssetManager类。

    res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹。

    读取res/raw下的文件资源,通过以下方式获取输入流来进行写操作:

    InputStream is = getResources().openRawResource(R.id.filename)

    注意:

    AssertManager中不能处理单个超过1M的文件,而raw没有这个限制

    assets文件夹是存放不进行编译加工的原生文件,即该文件夹里面的文件不会像xml、java文件被预编译,可以存放一些图片、html、js等等

    112、View视图绘制过程原理

    View视图绘制需要搞清楚两个问题,一个是从哪里开始绘制,一个是怎么绘制?

    从哪里开始绘制?我们平常使用Activity的时候,都会调用setContentView来设置布局文件,没错,视图绘制就是从这个方法开始。

    怎么绘制?

    在我们的Activity中调用了setContentView之后,会转而执行PhoneWindow的setContentView,在这个方法里面会判断我们存放内容的ViewGroup(这个ViewGroup可以是DecorView也可以是DecorView的子View)是否存在。不存在的话,则会创建一个DecorView处理,并且会创建出相应的窗体风格,存在的话则会删除原先的ViewGroup上面已有的View,接着会调用LayoutInflater的inflate方法以pull解析的方式将当前布局文件中存在的View通过addView的方式添加到ViewGroup上面来,接着在addView方法里面就会执行我们常见的invalidate方法了,这个方法不只是在View视图绘制的过程中经常用到,其实动画的实现原理也是不断的调用这个方法来实现视图不断重绘的,执行这个方法的时候会调用父View的invalidateChild方法,这个方法是属于ViewParent的,ViewGroup以及ViewRootImpl中都会他进行了实现,invalidateChild里面主要做的是就是通过do while循环一层一层计算出当前View的四个点所对应的矩阵在ViewRoot中所对应的位置,那么有了这个矩阵的位置之后最终都会执行ViewRootImpl的invalidateChildInParent方法,执行这个方法的时候首先会检查当前线程是不是主线程,因为我们要开始准备更新UI了,不是主线程的话是不允许更新UI的,接着就会执行scheduleTraversals方法了,这个方法会通过handler来执行doTraversal方法,在这个方法里面就见到了我们平常所熟悉的View视图绘制的起点方法performTraversals了。

    那么接下来就是真正的视图绘制流程了,大体上讲View的绘制流程经历了Measure测量、Layout布局以及Draw绘制的三个过程,具体来讲是从ViewRootImpl的performTraversals方法开始,首先执行的将是performMeasure方法,这个方法里面会传入两个MeasureSpec类型的参数,它在很大程度上决定了View的尺寸规格,对于DecorView来说宽高的MeasureSpec值的获取与窗口尺寸以及自身的LayoutParams有关,对于普通View来说其宽高的MeasureSpec值获取由父容器以及自身的LayoutParams属性共同决定,在performMeasure里面会执行measure方法,在measure方法里面会执行onMeasure方法,到这里Measure测量过程对View与ViewGroup来说是没有区别的,但是从onMeasure开始两者有差别了,因为View本身已经不存在子View了,所以他onMeasure方法将执行setMeasuredDimension方法,该方法会设置View的测量值,但是对于ViewGroup来说,因为它里面还存在着子View,那么我们就需要继续测量它里面的子View了,调用的方法是measureChild方法,该方法内部又会执行measure方法,而measure方法转而又会执行onMeasure方法,这样不断的递归进行下去,直到整个View树测量结束,这样performMeasure方法执行结束了。接着便是执行performLayout方法了,performMeasure只是测量出了View树中View的大小了,但是还不知道View的位置,所以也就出现了performLayout方法了,performLayout方法首先会执行layout方法,以确定View自身的位置,如果当前View是ViewGroup的话,则会执行onLayout方法。在onLayout方法里面又会递归的执行layout方法,直到当前遍历到的View不再是ViewGroup为止,这样整个layout布局过程就结束了。在View树中View的大小以及位置都确定之后,接下来就是真正的绘制View显示在界面的过程了,该过程首先从performDraw方法开始,performDraw首先会执行draw方法,在draw方法中首先绘制背景,接着调用onDraw方法绘制自己,如果当前View是ViewGroup的话,还要调用dispatchDraw方法绘制当前ViewGroup的子View,而dispatchDraw方法里面实际上是通过drawChild方法间接调用draw方法形成递归绘制整个View树,直到当前View不再是ViewGroup为止,这样整个View的绘制过程就结束了。

    总结:

    • ViewRootImpl会调用performTraversals(),其内部会调用performMeasure()、performLayout、performDraw

    • performMeasure会调用最外层的ViewGroup的measure() --> onMeasure() ,ViewGroup的onMeasure()是抽象方法,但其提供了measureChildren(),这之中会遍历子View然后循环调用measureChild(),传入MeasureSpec参数,然后调用子View的measure()到View的onMeasure() -->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize()默然返回measureSpec的测量数值,所以继承View进行自定义的wrap_content需要重写。

    • performLayout()会调用最外层的ViewGroup的layout(l,t,r,b),本View在其中使用setFrame()设置本View的四个顶点位置。在onLayout(抽象方法)中确定子View的位置,如LinearLayout会遍历子View,循环调用setChildFrame() --> 子View.layout()

    • performDraw()会调用最外层的ViewGroup的draw()方法,其中会先后调用background.draw()绘制背景,onDraw(绘制自己),dispatchDraw(绘制子View)、onDrawScrollBars(绘制装饰)

    • MeasureSpec由两位SpecMode(UNSPECIFIED、EXACTLY(对于精确值和match_parent)、AL_MOST(对应warp_content))和三十位SpecSize组成一个int,DecorView的MeasureSpec由窗口大小和其LayoutParams决定,其他View有父View的MeasureSpec和本View的LayoutParams决定。ViewGroup中有getChildMeasureSpec()来获取子View的MeasureSpec。

    113、IntentService

    IntentService是继承并处理异步请求的一个类,其本质上是一个Service,因为它是继承至Service,所以开启IntentService和普通的Service一致。但是他和普通的Service不同之处在于它可以处理异步任务,在任务处理完之后会自动结束。另外,我们可以启动多次IntentService,而每一个耗时任务会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且是串行执行。其实IntentService的内部是通过HandleThread和Handle来实现异步操作的。

    14、requestLayout、invalidate、postInvalidate 的区别

    1. requestLayout 会回掉 onMeasure、onLayout、onDraw(ViewGroup.setWillNotDraw(fasle)情况下)方法

    2. invalidate 只会回掉 onDraw 方法

    3. postInvalidate 只会回掉 onDraw 方法(可以在非 UI 线程中调用)

    15、深入理解Android插件化技术

    16、美团外卖Android Crash治理之路

    17、对 Activity.runOnUiThread 的理解

    当前线程不是ui线程,即发送post消息切换到ui线程(这个和sendMessage是有区别的,sendMessage是在非ui线程发送消息,这个在当前线程发送消息,然后因为activity初始化的时候就有looper、和MessageQueue,就能直接处理消息,从而将mUiThread切换到当前线程,再次执行就直接进行action.run())

    是ui线程,即直接实现方法

    18、什么是 RemoteViews?使用场景有哪些?

    RemoteViews

    RemoteViews翻译过来就是远程视图.顾名思义,RemoteViews不是当前进程的View,是属于SystemServer进程.应用程序与RemoteViews之间依赖Binder实现了进程间通信.

    用法

    通常是在通知栏

    //1.创建RemoteViews实例

        RemoteViews mRemoteViews=new RemoteViews("com.example.remoteviewdemo", R.layout.remoteview_layout);
    
        //2.构建一个打开Activity的PendingIntent
    
        Intent intent=new Intent(MainActivity.this,MainActivity.class);
    
        PendingIntent mPendingIntent=PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    
        //3.创建一个Notification
    
        mNotification = new Notification.Builder(this)
    
        .setSmallIcon(R.drawable.ic_launcher)
    
        .setContentIntent(mPendingIntent)
    
        .setContent(mRemoteViews)
    
        .build();
    
        //4.获取NotificationManager
    
        manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    
        Button button1 = (Button) findViewById(R.id.button1);
    
        button1.setOnClickListener(new OnClickListener() {
    
            @Override
    
            public void onClick(View v) {
    
                //弹出通知
    
                manager.notify(1, mNotification);
    
            }
    
        });
    

    19、谈谈 AIDL

    AIDL 是一种辅助工具,不用AIDL ,一样可以实现跨进程通讯

    AIDL 的原理是binder,真正有跨进程通讯能力的也是 Binder,所以 AIDL 只是一个能帮你少写代码,少出错的辅助工具,由于设计的太好,使用太方便,所以非常常用

    就像 retrofit 和okhttp 关系一样, retrofit 提供 更加友好的api,真正的网络请求还是由 okhttp发起的

    20、Android进程间的通信方式?

    image.png

    21、Binder机制:

    1.为了保证进程空间不被其他进程破坏或干扰,Linux中的进程是相互独立或相互隔离的。

    2.进程空间分为用户空间和内核空间。用户空间不可以进行数据交互;内核空间可以进行数据交互,所有进程共用一个内核空间。

    3.Binder机制相对于Linux内传统的进程间通信方式:(1)性能更好;Binder机制只需要拷贝数据一次,管道、消息队列、Socket等都需要拷贝数据两次;而共享内存虽然不需要拷贝,但实现复杂度高。(2)安全性更高;Binder机制通过UID/PID在内核空间添加了身份标识,安全性更高。

    4.Binder跨进程通信机制:基于C/S架构,由Client、Server、Server Manager和Binder驱动组成。

    5.Binder驱动实现的原理:通过内存映射,即系统调用了mmap()函数。

    6.Server Manager的作用:管理Service的注册和查询。

    7.Binder驱动的作用:(1)传递进程间的数据,通过系统调用mmap()函数;(2)实现线程的控制,通过Binder驱动的线程池,并由Binder驱动自身进行管理。

    8.Server进程会创建很多线程处理Binder请求,这些线程采用Binder驱动的线程池,由Binder驱动自身进行管理。一个进程的Binder线程池默认最大是16个,超过的请求会阻塞等待空闲的线程。

    9.Android中进行进程间通信主要通过Binder类(已经实现了IBinder接口),即具备了跨进程通信的能力。

    首先 所有的server都会在 serviceManager 中注册

    client 访问 server时 要向 serviceManager 发起请求,

    serviceManager 找到 这个 server 并通过 binder驱动 生成一个 server代理 返回给client

    client得到了 代理之后 访问 代理server

    代理server 相应方法被调用后 会向 binder驱动中去调用真正的server方法。

    相关文章

      网友评论

        本文标题:Android面试考点

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