Android 面试准备进行曲-Android 基础四大组件和缓存

1,272 阅读17分钟

基础部分

Activity生命周期

onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDetroy()

11.webp 图片简要说明

  • 启动 onCreate -> onStart -> onResume
  • 被覆盖/ 回到当前界面 onPause -> / -> onResume
  • 在后台 onPause -> onStop
  • 后退回到 onRestart -> onStart -> onResume
  • 退出 onPause -> onStop -> onDestory

另外说一下 其他两个比较重要的 方法

  • onSaveInstanceState : (1)Activity被覆盖或退居后台,系统资源不足将其杀死,此方法会被调用;(2)在用户改变屏幕方向时,此方法会被调用 系统先销毁当前的Activity,然后再重建一个新的,调用此方法时,我们可以保存一些临时数据;(3)在当前Activity跳转到其他Activity或者按Home键回到主屏,自身退居后台时, 系统调用此方法是为了保存当前窗口各个View组件的状态。onSaveInstanceState该方法调用在onStop之前,但和onPause没有时序关系。 不过一般onSaveInstanceState() 保存临时数据为主,而 onPause() 适用于对数据的持久化保存。

  • onRestoreInstanceState : onRestoreInstanceState的调用顺序是在onStart之后。主要用于 恢复一些onSaveInstanceState 方法中保存的数据

onStart()和onResume()/onPause()和onStop()的区别

onStart()与onStop()是从Activity是否可见这个角度调用的 onResume()和onPause()是从Activity是否显示在前台这个角度来回调的 在实际使用没其他明显区别。

Activity A 跳转 Activity B的问题

Activity A启动另一个Activity B会回调的方法: Activity A的onPause() -->Activity B的onCreate()-->onStart()-->onResume()-->Activity A的onStop();

如果Activity B是完全透明的,则最后不会调用Activity A的onStop();如果是对话框Activity,则最后不会调用Activity A的onStop();

Activity 启动流程

22.webp 调用startActivity()后经过重重方法会转移到ActivityManagerService的startActivity(),并通过一个IPC回到ActivityThread的内部类ApplicationThread中,并调用其scheduleLaunchActivity()将启动Activity的消息发送并交由Handler H处理。 Handler H对消息的处理会调用handleLaunchActivity()->performLaunchActivity()得以完成Activity对象的创建和启动。

参考地址:Activity启动流程

Fragment 生命周期

Fragment从创建到销毁整个生命周期中涉及到的方法依次为: onAttach()->onCreate()-> onCreateView()->onActivityCreated()->onStart()->onResume()->onPause()->onStop()->onDestroyView()->onDestroy()->onDetach(), 其中和Activity有不少名称相同作用相似的方法,而不同的方法有:

onAttach():当Fragment和Activity建立关联时调用

onCreateView():当Fragment创建视图时调用

onActivityCreated():当与Fragment相关联的Activity完成onCreate()之后调用

onDestroyView():在Fragment中的布局被移除时调用

onDetach():当Fragment和Activity解除关联时调用

Activity 与 Fragment 通信

  1. 对于Activity和Fragment之间的相互调用 (1)Activity调用Fragment 直接调用就好,Activity一般持有Fragment实例,或者通过Fragment id 或者tag获取到Fragment实例 (2)Fragment调用Activity 通过activity设置监听器到Fragment进行回调,或者是直接在fragment直接getActivity获取到activity实例

  2. Activity如果更好的传递参数给Fragment 如果直接通过普通方法的调用传递参数的话,那么在fragment回收后恢复不能恢复这些数据。google给我们提供了一个方法 setArguments(bundle) 可以通过这个方法传递参数给fragment,然后在fragment中用getArguments获取到。能保证在fragment销毁重建后还能获取到数据

Service 启动及生命周期

service 启动方式

  • 不可通信Service 。 通过startService()启动,不跟随调用者关闭而关闭
  • 可通信Service 。 通过bindService()方式进行启动。跟随调用者关闭而关闭

以上两种Servcie 默认都存在于调用者一样的进程中,如果想要设置不一样的进程中则需要在 AndroidManifest.xml 中 配置 android:process = Remote 属性

生命周期 :

  • 通过startService()这种方式启动的service,生命周期 :startService() --> onCreate()--> onStartConmon()--> onDestroy()。

需要注意几个问题

1. 当我们通过startService被调用以后,多次在调用startService(),onCreate()方法也只会被调用一次,
2. 而onStartConmon()会被多次调用当我们调用stopService()的时候,onDestroy()就会被调用,从而销毁服务。
 2. 当我们通过startService启动时候,通过intent传值,在onStartConmon()方法中获取值的时候,一定要先判断intent是否为null。
  • 通过bindService()方式进行绑定,这种方式绑定service,生命周期走法:bindService-->onCreate()-->onBind()-->unBind()-->onDestroy()

bindService的优点 这种方式进行启动service好处是更加便利activity中操作service,比如加入service中有几个方法,a,b ,如果要在activity中调用,在需要在activity获取ServiceConnection对象,通过ServiceConnection来获取service中内部类的类对象,然后通过这个类对象就可以调用类中的方法,当然这个类需要继承Binder对象

Service 通信方式

  1. 创建继承Binder的内部类,重写Service的onBind方法 返回 Binder 子类,重写ServiceConnection,onServiceConnected时调用逻辑方法 绑定服务。

  2. 通过接口Iservice调用Service方法

IntentService对比Service

IntentService是Service的子类,是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题

优点:

  • 所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止
  • IntentService不会阻塞UI线程,而普通Serveice会导致ANR异常
  • Intentservice若未执行完成上一次的任务,将不会新开一个线程,是等待之前的任务完成后,再执行新的任务,等任务完成后再次调用stopSelf()
  • 为Service的onBind()提供默认实现,返回null;

提高service的优先级

  1. 在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = “1000”这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时实用于广播。
  2. onStartCommand方法,手动返回START_STICKY。
  3. 监听系统广播判断Service状态。
  4. Application加上Persistent属性。
  5. 在onStartCommand里面调用 startForeground()方法把Service提升为前台进程级别,然后再onDestroy里面调用stopForeground ()方法。
  6. 在onDestroy方法里发广播重启service。 service +broadcast 方式,就是当service走ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service。

延伸:进程保活(毒瘤)

黑色保活:不同的app进程,用广播相互唤醒(包括利用系统提供的广播进行唤醒)
白色保活:启动前台Service
灰色保活:利用系统的漏洞启动前台Service

黑色保活 所谓黑色保活,就是利用不同的app进程使用广播来进行相互唤醒。举个3个比较常见的场景: 场景1:开机,网络切换、拍照、拍视频时候,利用系统产生的广播唤醒app 场景2:接入第三方SDK也会唤醒相应的app进程,如微信sdk会唤醒微信,支付宝sdk会唤醒支付宝。由此发散开去,就会直接触发了下面的 场景3 场景3:假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。(只是拿阿里打个比方,其实BAT系都差不多)

白色保活 白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。如下方的LBE和QQ音乐这样:

灰色保活 灰色保活,这种保活手段是应用范围最广泛。它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的。那么如何利用系统的漏洞呢,大致的实现思路和代码如下: 思路一:API < 18,启动前台Service时直接传入new Notification(); 思路二:API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理 熟悉Android系统的童鞋都知道,系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app。这套杀进程回收内存的机制就叫 Low Memory Killer ,它是基于Linux内核的 OOM Killer(Out-Of-Memory killer)机制诞生。

思路二:后台播放无声音频,模拟前台服务,提高等级

思路三:1像素界面

思路四:在Activity的onDestroy()通过发送广播,并在广播接收器的onReceive()中启动Service

进程的重要性,划分5级: 前台进程 (Foreground process) 可见进程 (Visible process) 服务进程 (Service process) 后台进程 (Background process) 空进程 (Empty process)

什么是oom_adj?它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收。对于oom_adj的作用,你只需要记住以下几点即可: 进程的oom_adj越大,表示此进程优先级越低,越容易被杀回收;越小,表示进程优先级越高,越不容易被杀回收 普通app进程的oom_adj>=0,系统进程的oom_adj才可能<0 有些手机厂商把这些知名的app放入了自己的白名单中,保证了进程不死来提高用户体验

Broadcast注册方式与区别

Broadcast广播,注册方式主要有两种.

  • 第一种是静态注册,也可成为常驻型广播,这种广播需要在Androidmanifest.xml中进行注册,这中方式注册的广播,不受页面生命周期的影响,即使退出了页面,也可以收到广播这种广播一般用于想开机自启动啊等等,由于这种注册的方式的广播是常驻型广播,所以会占用CPU的资源。

  • 第二种是动态注册,而动态注册的话,是在代码中注册的,这种注册方式也叫非常驻型广播,收到生命周期的影响,退出页面后,就不会收到广播,我们通常运用在更新UI方面。这种注册方式优先级较高。最后需要解绑,否则会内存泄露 广播是分为有序广播和无序广播。

Broadcast 有几种形式

普通广播:一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们接收的先后是随机的。

有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递,所以此时的广播接收器是有先后顺序的,且优先级(priority)高的广播接收器会先收到广播消息。有序广播可以被接收器截断使得后面的接收器无法收到它。

本地广播:发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收本应用程序发出的广播。

粘性广播:这种广播会一直滞留,当有匹配该广播的接收器被注册后,该接收器就会收到此条广播。

部分Broadcast 之间的区别

BroadcastReceiver: 是可以跨应用广播,利用Binder机制实现,支持动态和静态两种方式注册方式。

LocalBroadcastReceiver: 是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。

OrderedBroadcast : 调用sendOrderedBroadcast()发送,接收者会按照priority优先级从大到小进行排序,如优先级相同,先注册,先处理 广播接收者还能对广播进行截断和修改

ContentProvider

作为四大组件之一,ContentProvider主要负责存储和共享数据。与文件存储、SharedPreferences存储、SQLite数据库存储这几种数据存储方法不同的是,后者保存下的数据只能被该应用程序使用,而前者可以让不同应用程序之间进行数据共享,它还可以选择只对哪一部分数据进行共享,从而保证程序中的隐私数据不会有泄漏风险。

app中有几个Context对象

先看一下源码的解释

/**
* Interface to global information about an application environment.  This is
* an abstract class whose implementation is provided by
* the Android system.  It
* allows access to application-specific resources and classes, as well as
* up-calls for application-level operations such as launching activities,
* broadcasting and receiving intents, etc.
*/
public abstract class Context {
    /**
     * File creation mode: the default mode, where the created file can only
     * be accessed by the calling application (or all applications sharing the
     * same user ID).
     * <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> #MODE_WORLD_READABLE
     * <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> #MODE_WORLD_WRITEABLE
     */
    public static final int MODE_PRIVATE = 0x0000;
 
    public static final int MODE_WORLD_WRITEABLE = 0x0002;
 
    public static final int MODE_APPEND = 0x8000;
 
    public static final int MODE_MULTI_PROCESS = 0x0004;
 
    }

源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。

3.webp 从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。

Application Context 启动问题

如果我们用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。

如何获取 Context对象

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

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

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

如何避免因为Context 造成内存泄漏

一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:

1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。

2:不要让生命周期长于Activity的对象持有到Activity的引用。

3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

getApplication()和getApplicationContext() 区别

其实我们通过程序打印 两个方法获得的对象 Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?

实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。

理解Activity,View,Window三者关系

Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图)LayoutInflater像剪刀,Xml配置像窗花图纸。 1:Activity构造的时候会初始化一个Window,准确的说是PhoneWindow。 2:这个PhoneWindow有一个“ViewRoot”,这个“ViewRoot”是一个View或者说ViewGroup,是最初始的根视图。 3:“ViewRoot”通过addView方法来一个个的添加View。比如TextView,Button等 4:这些View的事件监听,是由WindowManagerService来接受消息,并且回调Activity函数。比如onClickListener,onKeyDown等。

四种LaunchMode及其使用场景

此处延伸:栈(First In Last Out)与队列(First In First Out)的区别 栈与队列的区别:

队列先进先出,栈先进后出 对插入和删除操作的"限定"。 栈是限定只能在表的一端进行插入和删除操作的线性表。 队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。 遍历数据速度不同

standard 模式 这是默认模式,每次激活Activity时都会创建Activity实例,并放入任务栈中。使用场景:大多数Activity。 singleTop 模式 如果在任务的栈顶正好存在该Activity的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该Activity的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类App的内容页面。 singleTask 模式 如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。 singleInstance 模式 在一个新栈中创建该Activity的实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity实例已经存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例( 会调用实例的 onNewIntent() )。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

数据存储

Android中提供哪些数据持久存储的方法?

File 文件存储:写入和读取文件的方法和 Java中实现I/O的程序一样。

SharedPreferences存储:一种轻型的数据存储方式,常用来存储一些简单的配置 信息,本质是基于XML文件存储key-value键值对数据。

SQLite数据库存储:一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,在存储大量复杂的关系型数据的时可以使用。

ContentProvider:四大组件之一,用于数据的存储和共享,不仅可以让不同应用程序之间进行数据共享,还可以选择只对哪一部分数据进行共享,可保证程序中的隐私数据不会有泄漏风险。

SharePreferences 相关问题

  1. SharePreferences是一种轻型的数据存储方式,适用于存储一些简单的配置信息,如int、string、boolean、float和long。由于系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据。

  2. context.getSharedPreferences()开始追踪的话,可以去到ContextImpl的getSharedPreferences(),最终发现SharedPreferencesImpl这个SharedPreferences的实现类,在代码中可以看到读写操作时都有大量的synchronized,因此它是线程安全

  3. 由于进程间是不能内存共享的,每个进程操作的SharedPreferences都是一个单独的实例,这导致了多进程间通过SharedPreferences来共享数据是不安全的,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。

SharePreferences 注意事项及优化办法

  1. 第一次getSharePreference会读取磁盘文件,异步读取,写入到内存中,后续的getSharePreference都是从内存中拿了。
  2. 第一次读取完毕之前 所有的get/set请求都会被卡住 等待读取完毕后再执行,所以第一次读取会有ANR风险。
  3. 所有的get都是从内存中读取。
  4. 提交都是 写入到内存和磁盘中 。apply跟commit的区别在于 apply 是内存同步 然后磁盘异步写入任务放到一个单线程队列中 等待调用。方法无返回 即void commit 内存同步 只不过要等待磁盘写入结束才返回 直接返回写入成功状态 true or false
  5. 从 Android N 开始, 不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE. 一旦指定, 会抛异常 。也不要用MODE_MULTI_PROCESS 迟早被放弃。 8.每次commit/apply都会把全部数据一次性写入到磁盘,即没有增量写入的概念 。 所以单个文件千万不要太大 否则会严重影响性能。

建议用微信的第三方MMKV来替代SharePreference

SP源码解析

SQLite 相关问题

  • 使用事务做批量操作: 使用SQLiteDatabase的beginTransaction()方法开启一个事务,将批量操作SQL语句转化成SQLiteStatement并进行批量操作,结束后endTransaction()

  • 及时关闭Cursor,避免内存泄漏

  • 耗时操作异步化:数据库的操作属于本地IO,通常比较耗时,建议将这些耗时操作放入异步线程中处理

  • ContentValues的容量调整:ContentValues内部采用HashMap来存储Key-Value数据,ContentValues初始容量为8,扩容时翻倍。因此建议对ContentValues填入的内容进行估量,设置合理的初始化容量,减少不必要的内部扩容操作

  • 使用索引加快检索速度:对于查询操作量级较大、业务对要求查询要求较高的推荐使用索引