当前位置: 首页 > news >正文

Android开发高频面试题之——Android篇

Android开发高频面试题之——Android篇

Android开发高频面试题之——Java基础篇
Android开发高频面试题之——Kotlin基础篇
Android开发高频面试题之——Android基础篇

1. Activity启动模式

  • standard 标准模式,每次都是新建Activity实例。
  • singleTop 栈顶复用。如果要启动的Activity已经处于任务栈顶,则直接复用不会新建Activity实例,此时会调用onNewIntent方法。如果栈内不存在或者不在栈顶。则会新建Activity实例。
  • singleTask 栈内单例。如果任务栈内已经存在Activity实例,则直接复用。如果不在栈顶,则把该activity实例之上的全部出栈,让自身位于栈顶。此时会调用onNewIntent方法。
  • singleInstance 新建任务栈栈内唯一。应用场景:来电话界面,即使来多个电话也只创建一个Activity;

2. Activity生命周期

在这里插入图片描述

  • 启动状态(Starting):Activity的启动状态很短暂,当Activity启动后便会进入运行状态(Running)。
  • 运行状态(Running):Activity在此状态时处于屏幕最前端,它是可见、有焦点的,可以与用户进行交互。如单击、长按等事件。即使出现内存不足的情况,Android也会先销毁栈底的Activity,来确保当前的Activity正常运行。
  • 暂停状态(Paused):在某些情况下,Activity对用户来说仍然可见,但它无法获取焦点,用户对它操作没有没有响应,此时它处于暂停状态。例如,当前Activity弹出Dialog,或者新启动Activity为透明的Activity等情况。
  • 停止状态(Stopped):当Activity完全不可见时,它处于停止状态,但仍然保留着当前的状态和成员信息。如系统内存不足,那么这种状态下的Activity很容易被销毁。
  • 销毁状态(Destroyed):当Activity处于销毁状态时,将被清理出内存。

Activity的生命周期
在这里插入图片描述

  • onCreate() : 在Activity创建时调用,通常做一些初始化设置,不可以执行耗时操作。;
  • onNewIntent()*:注意 !!只有当 当前activity实例已经处于任务栈顶,并且使用启动模式为singleTop或者SingleTask再次启动Activity时才会调用此方法。此时不会走OnCreate(),而是会执行onNewIntent()。因为activity不需要创建而是直接复用。
  • onStart(): 在Activity即将可见时调用;可以做一些动画初始化的操作。
  • onRestoreInstanceState()*:注意 !!当app异常退出重建时才会调用此方法。可以在该方法中恢复以保存的数据。
  • onResume(): 在Activity已可见,获取焦点开始与用户交互时调用;当Activity第一次启动完成或者当前Activity被遮挡住一部分(进入了onPause())重新回到前台时调用,比如弹窗消失。当onResume()方法执行完毕之后Activity就进入了运行状态。根据官方的建议,此时可以做开启动画和独占设备的操作。
  • onPause(): 在当前Activity被其他Activity覆盖或锁屏时调用;Activity停止但是当前Activity还是处于用户可见状态,比如出现弹窗;在onPause()方法中不能进行耗时操作(当前Activity通过Intent启动另一个Activity时,会先执行当前Activity的onPause()方法,再去执行另一个Activity的生命周期)
  • onSaveInstanceState():注意 !! 只有当app可能会异常销毁时才会调用此方法保存activity数据。以便于activity重建时恢复数据
    Activity的onSaveInstanceState回调时机,取决于app的targetSdkVersion:
    targetSdkVersion低于11的app,onSaveInstanceState方法会在Activity.onPause之前回调;
    targetSdkVersion低于28的app,则会在onStop之前回调;
    28之后,onSaveInstanceState在onStop回调之后才回调。
  • onStop() : 在Activity完全被遮挡对用户不可见时调用(在onStop()中做一些回收资源的操作)
  • onDestroy() :在Activity销毁时调用;
  • onRestart() : 在Activity从停止状态再次启动时调用;处于stop()状态也就是完全不可见的Activity重新回到前台时调用(重新回到前台不会调用onCreate()方法,因为此时Activity还未销毁)

Activity横竖屏切换生命周期
横竖屏切换涉及到的是Activity的android:configChanges属性;
android:configChanges可以设置的属性值有:

orientation:消除横竖屏的影响
keyboardHidden:消除键盘的影响
screenSize:消除屏幕大小的影响

  • 设置Activity的android:configChanges属性为orientation或者orientation|keyboardHidden或者不设置这个属性的时候,横竖屏切换会重新调用各个生命周期方法,切横屏时会执行1次,切竖屏时会执行1次;
  • 设置Activity的属性为 android:configChanges=“orientation|keyboardHidden|screenSize” 时,横竖屏切换不会重新调用各个生命周期方法,只会执行onConfigurationChanged方法;

3. 了解Service吗

Service一般用于没有ui界面的长期服务。
Service有两种启动方式

  • StartService 这种方式启动的Service生命周期和启动实例无关。启动后会一直存在,直到app退出,或者调用stopService或者stopSelf。生命周期为onCreate-》onStartCommand-》onDestroyed。
    • 多次启动StartService。onStartCommand会调用多次
  • bindService 这种方式启动的Service和生命周期会和调用者绑定。一旦调用者结束,Service也会一起结束。生命周期为onCreate-》onBind-》onUnbind-》onDestroyed

如何保证Service不被杀死

  • onStartCommand方式中,返回START_STICKY或者START_REDELIVER_INTENT
    • START_STICKY:如果返回START_STICKY,Service运行的进程被Android系统杀掉之后,Android系统会将该Service依然设置为started状态(即运行状态),会重新创建该Service。但是不再保存onStartCommand方法传入的intent对象
    • START_NOT_STICKY:如果返回START_NOT_STICKY,表示当Service运行的进程被Android系统强制杀掉之后,不会重新创建该Service
    • START_REDELIVER_INTENT:如果返回START_REDELIVER_INTENT,其返回情况与START_STICKY类似,但不同的是系统会保留最后一次传入onStartCommand方法中的Intent再次保留下来并再次传入到重新创建后的Service onStartCommand方法中
  • 提高Service的优先级: 在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = "1000"这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时适用于广播;
  • 在onDestroy方法里重启Service: 当service走到onDestroy()时,发送一个自定义广播,当收到广播 时,重新启动service;
  • 提升Service进程的优先级。 进程优先级由高到低:前台进程 一》 可视进程 一》 服务进程 一》 后台进程 一》 空进程
    可以使用 startForegroundservice放到前台状态,这样低内存时,被杀死的概率会低一些; 系统广播监听Service状态将APK安装到/system/app,变身为系统级应用。

4. 使用过broadcastReceiver吗?

可分为标准广播(无序广播)有序广播
按照作用范围可分为全局广播本地广播
按照注册方式可分为静态广播动态广播

  • 标准广播
    标准广播(normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的BroadcastReceiver几乎都会在同一时刻接收到收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。
  • 有序广播
    有序广播(ordered broadcasts)是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的,优先级高的BroadcastReceiver就可以先收到广播消息,并且前面的BroadcastReceiver还可以截断正在传递的广播,这样后面的BroadcastReceiver就无法收到广播消息了。
    • 清单文件里的android:priority属性的数字大小设置优先级 范围为(-1000到1000)数字越大,优先级越高
    • setResultExtras(bundle) 向下游广播接收器传递额外的键值对信息或者**setResultData(“”)**直接传送字符串
    • 下游广播通过getResultExtras方法接收信息, getResultData() 方法接收字符串信息
    • abortBroadcast() 进行截断
        <receiverandroid:name=".broadcast.MyHaveBroadcastReceiver02"android:exported="true"><intent-filter android:priority="003"><action android:name="my_have_broadcast_receiver"></action></intent-filter></receiver>
/*** 有序广播*/
public class MyHaveBroadcastReceiver02 extends BroadcastReceiver {private static final String TAG = "MyHaveBroadcastReceiver02";@Overridepublic void onReceive(Context context, Intent intent) {if (intent != null) {Bundle extras = intent.getExtras();String data = "";if (extras != null) {data = extras.getString("name3");/*** 2.1有序广播可以通过setResultExtras向下游广播接收器传递数据*/extras.putString("name2", "喊你速速到龙阳路集合");setResultExtras(extras);/*** 2.2有序广播可以通过setResultData向下游广播接收器传递字符串*/setResultData("2号口的信息");}Log.d(TAG, "MyHaveBroadcastReceiver02 onReceive" + data);/*** 1.有序广播可以通过abortBroadcast();方法对广播进行截断*/
//            abortBroadcast();}}
}
  • 本地广播:发送的广播事件不被其他应用程序获取,也不能响应其他应用程序发送的广播事件。本地广播只能被动态注册,不能静态注册。动态注册或发送时时需要用到LocalBroadcastManager
  • 全局广播:发送的广播事件可被其他应用程序获取,也能响应其他应用程序发送的广播事件(可以通过 exported–是否监听其他应用程序发送的广播 在清单文件中控制) 全局广播既可以动态注册,也可以静态注册。
  • 静态广播
    静态广播在清单文件AndroidMainfest.xml中注册,生命周期随系统,不受Activity生命周期影响,即使进程被杀死,仍然能收到广播,因此也可以通过注册静态广播做一些拉起进程的事。随着Android版本的增大,Android系统对静态广播的限制也越来越严格,一般能用动态广播解决的问题就不要用静态广播。
  • 动态广播
    动态广播不需要在AndroidManifest.xml文件中进行注册,动态注册的广播受Activity声明周期的影响,Activity消亡,广播也就不复存在。动态广播在需要接受广播的Activity中进行注册和解注册。

5. 说说你对handler的理解

Handler是Android用来解决线程间通讯问题的消息机制。Handler消息机制分为四个部分。

  • Handler 消息的发送者处理者
  • Message 消息实体 消息的载体和携带者。
  • MessageQueen 消息队列,使用双向链表实现,是存放消息的队列。
  • Looper 消息循环器,不停的从消息队列中中取出消息。

如何使用Handler?

  • 使用Handler需要一个Looper环境,在主线程直接新建Handler实例然后实现handleMessage方法,然后在需要发送消息的地方,使用handler.sendMessage等方法即可。
private static class MyHandler extends Handler {private final WeakReference<MainActivity> mTarget;public MyHandler(MainActivity activity) {mTarget = new WeakReference<MainActivity>(activity);}@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);HandlerActivity activity = weakReference.get();super.handleMessage(msg);if (null != activity) {//执行业务逻辑if (msg.what == 0) {Log.e("myhandler", "change textview");MainActivity ma = mTarget.get();ma.textView.setText("hahah");}Toast.makeText(activity,"handleMessage",Toast.LENGTH_SHORT).show();}}}private Handler handler1 = new MyHandler(this);new Thread(new Runnable() {@Overridepublic void run() {handler1.sendEmptyMessage(0);}}).start();
  • 在子线程使用需要先创建Looper环境,调用Looper.prepare(),然后再创建Handler。最后在调用Looper.loop()启动消息循环。子线程Handler不使用时要调用handler.getLooper().quitSafely()退出Looper否则会阻塞。
	private static class MyHandler extends Handler {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);if (msg.what == 0) {Log.e("child thread", "receive msg from main thread");}}}private Handler handler1;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {     new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare(); //准备Looper环境handler1 = new MyHandler();Looper.loop(); //启动LooperLog.e("child thread", "child thread end");}}).start();handler1.sendEmptyMessage(0);handler1.getLooper().quitSafely();//子线程Handler不用时,退出Looper
}

主线程使用Handler为什么不用Looper.prepare()?

因为在app启动时,ActivityThread的Main方法里帮我们调用了Looper.prepareMainLooper()。并且最后调用了Looper.loop()
启动了主线程。

public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");// Install selective syscall interceptionAndroidOs.install();// CloseGuard defaults to true and can be quite spammy.  We// disable it here, but selectively enable it later (via// StrictMode) on debug builds, but using DropBox, not logs.CloseGuard.setEnabled(false);Environment.initForCurrentUser();// Make sure TrustedCertificateStore looks in the right place for CA certificatesfinal File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());TrustedCertificateStore.setDefaultUserDirectory(configDir);// Call per-process mainline module initialization.initializeMainlineModules();Process.setArgV0("<pre-initialized>");Looper.prepareMainLooper();// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.// It will be in the format "seq=114"long startSeq = 0;if (args != null) {for (int i = args.length - 1; i >= 0; --i) {if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {startSeq = Long.parseLong(args[i].substring(PROC_START_SEQ_IDENT.length()));}}}ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}

简述一下Handler的工作流程

  • Handler使用SendMessage或者post等方法。最终都会调用MessageQueue的enqueueMessage()方法。将消息按照执行时间先后顺序入队。
  • Looper里面是个死循环,不停地在队列中通过MessageQueue.next()方法取出消息,取出消息后,通过msg.target.dispatchMessage() 方法分发消息。先交给msg消息的runnable处理,再交给Handler的Callable处理,最后再交给Handler实现的handleMessage方法处理
    public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}

一个线程中最多有多少个Handler,Looper,MessageQueue?

  • 一个线程可以有多个Handler
  • 一个handler只能有一个Looper和一个MessageQueen
    因为创建Handler必须有Looper环境,而Looper只能通过Looper.prepare和Looper.prepareMainLooper来创建。同时将Looper实例存放到线程局部变量sThreadLocal(ThreadLocal)中,也就是每个线程有自己的Looper。在创建Looper的时候也创建了该线程的消息队列,prepareMainLooper会判断sMainLooper是否有值,如果调用多次,就会抛出异常,所以主线程的Looper和MessageQueue只会有一个。同理子线程中调用Looper.prepare()时,会调用prepare(true)方法,如果多次调用,也会抛出每个线程只能由一个Looper的异常,总结起来就是每个线程中只有一个Looper和MessageQueue。
    public static void prepare() {prepare(true);}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}

Looper死循环为什么不会导致应用ANR、卡死,会耗费大量资源吗?

线程其实就是一段可执行的代码,当可执行的代码执行完成后,线程的生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出

  • ANR 产生的原因是主线程没有及时响应用户的操作。也就是主线程执行某个耗时操作来不及处理UI消息。
  • 而Looper一直循环,就是在不断的检索消息,与主线程无法响应用户操作没有任何冲突
  • Android是基于消息处理机制的,用户的行为都在这个Looper循环中,正是有了主线程Looper的不断循环,才有app的稳定运行。
  • 简单来说looper的阻塞表明没有事件输入,而ANR是由于有事件没响应导致,所以looper的死循环并不会导致应用卡死。

主线程的死循环并不消耗 CPU 资源,这里就涉及到 Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

Handler同步屏障了解吗

同步屏障是为了保证异步消息的优先执行,一般是用于UI绘制消息,避免主线程消息太多,无法及时处理UI绘制消息,导致卡顿。

  • 同步消息 一般的handler发送的消息都是同步消息
  • 异步消息 Message标记为异步的消息
    • 可以调用 Message#setAsynchronous() 直接设置为异步 Message
    • 可以用异步 Handler 发送 使用 Handler.createAsync() 创建异步Handler
    public static Handler createAsync(@NonNull Looper looper) {if (looper == null) throw new NullPointerException("looper must not be null");return new Handler(looper, null, true);}
  • 同步屏障 在 MessageQueue 的 某个位置放一个 target 属性为 null 的 Message ,确保此后的非异步 Message 无法执行,只能执行异步 Message。

当 Looper轮循MessageQueue 遍历 Message发现建立了同步屏障的时候,会去跳过其他Message,读取下个 async 的 Message 并执行,屏障移除之前同步 Message 都会被阻塞。
比如屏幕刷新 Choreographer 就使用到了同步屏障 ,确保屏幕刷新事件不会因为队列负荷影响屏幕及时刷新。
注意: 同步屏障的添加或移除 API MessageQueue.postSyncBarrier并未对外公开,App 需要使用的话需要依赖反射机制

    try {MessageQueue queue=handler.getLooper().getQueue();Method method=MessageQueue.class.getDeclaredMethod("postSyncBarrier");method.setAccessible(true);token= (int) method.invoke(queue);} catch (Exception e) {e.printStackTrace();}try {MessageQueue queue=handler.getLooper().getQueue();Method method=MessageQueue.class.getDeclaredMethod("removeSyncBarrier",int.class);method.setAccessible(true);method.invoke(queue,token);} catch (Exception e) {e.printStackTrace();}        

Handler 为什么可能导致内存泄露?如何避免?

持有 Activity 实例的匿名内部类或内部类的 生命周期 应当和 Activity 保持一致,否则产生内存泄露的风险。

如果 Handler 使用不当,将造成不一致,表现为:匿名内部类或内部类写法的 Handler、Handler$Callback、Runnable,或者Activity 结束时仍有活跃的 Thread 线程或 Looper 子线程

具体在于:异步任务仍然活跃或通过发送的 Message 尚未处理完毕,将使得内部类实例的 生命周期被错误地延长 。造成本该回收的 Activity 实例 被别的 Thread 或 Main Looper 占据而无法及时回收 (活跃的 Thread 或 静态属性 sMainLooper 是 GC Root 对象)
建议的做法:

  • 无论是 Handler、Handler$Callback 还是 Runnable,尽量采用 静态内部类 + 弱引用 的写法,确保尽管发生不当引用的时候也可以因为弱引用能清楚持有关系
  • 另外在 Activity 销毁的时候及时地 终止 Thread、停止子线程的 Looper 或清空 Message ,确保彻底切断 Activity 经由 Message 抵达 GC Root 的引用源头(Message 清空后会其与 Handler 的引用关系,Thread 的终止将结束其 GC Root 的源头)

Handler是如何实现线程间通讯的

  • handler是消息的发送者也是处理者。发送消息时,msg.target会标记为自身。插入MessageQueen后,被Looper取出后会通过msg.target.dispatchMessage去分发给对应的Handler去处理。

Handler消息处理的优先级

    public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}

可以看出优先级是Message.CallBack->Handler.callback->Handler.handleMessage
有时候面试官也会问Runnable->Callable->handleMessage
post方法就是runnable
Handler构造传入Callback就是Callable
send方法是handleMessage

如何正确或Message实例

  • 通过 Message 的静态方法 Message.obtain() 获取;
  • 通过 Handler 的公有方法 handler.obtainMessage()
  • 默认大小是50

Message使用享元设计模式,里面有一个spool指向一个Message对象,还有一个next指向下一个Message,维护了一个链表实现的对象池,obtain的时候在表头头取Message,在Message回收的时候在表头添加一个Message。

Android 为什么不允许并发访问 UI?

Android 中 UI 非线程安全,并发访问的话会造成数据和显示错乱。
此限制的检查始于ViewRootImpl#checkThread(),其会在刷新等多个访问 UI 的时机被调用,去检查当前线程,非主线程的话抛出异常。(实际上并不是检查主线程。而是检查UI的更新线程是否与UI的创建线程一致,因为UI是在主线程创建的,所以也只能在主线程更新)
而 ViewRootImpl 的创建在 onResume() 之后,也就是说如果在 onResume() 执行前启动线程访问 UI 的话是不会报错的。

了解ThreadLocal吗

  • Thread中会维护一个类似HashMap的东西,然后用ThreadLocal对象作为key,value就是要存储的变量值,这样就保证了存储数据的唯一性)
  • ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
  • ThreadLocal 内部通过 ThreadLocalMap 持有 Looper,key 为 ThreadLocal 实例本身,value 即为 Looper 实例
    每个 Thread 都有一个自己的 ThreadLocalMap,这样可以保证每个线程对应一个独立的 Looper 实例,进而保证 myLooper() 可以获得线程独有的 Looper。让每个线程方便程获取自己的 Looper 实例
    在这里插入图片描述

ThreadLocal与内存泄漏

  • 在线程池中使用ThreadLocal可能会导致内存泄漏,原因是线程池中线程的存活时间太长,往往和程序都是同生共死的,这就意味着Thread持有的ThreadLocalMap一直都不会被回收,

http://www.mrgr.cn/news/29376.html

相关文章:

  • STM32F1学习——PWM波(OC输出比较)
  • Nop平台的定位及发展规划
  • Tofu AI视频处理模块视频输入配置方法
  • Spring Validation参数校验
  • 无人机飞手执照处处需要,森林、石油管道、电力巡检等各行业都需要
  • Llama架构及代码详解
  • Node.js 学习
  • TCRT5000红外循迹模块简介
  • C语言中的输入输出艺术:深入解析puts/printf与gets/scanf
  • springboot整合mybatis(使用druid线程池)
  • 宝兰德MCP系列介绍 ①:中间件管理能力全线升级,驱动企业数字化管理效能提升
  • 6.Java高级编程 输入和输出处理一
  • 魅思-视频管理系统 getOrderStatus SQL注入漏洞复现
  • 【系统架构设计师】软件架构的风格(经典习题)
  • 5.内容创作的未来:ChatGPT如何辅助写作(5/10)
  • FreeRTOS单多核调度
  • Shell脚本编程基础(二)
  • 【Linux】Linux的基本指令(1)
  • 国外问卷调查怎么做的,新手怎么开始?
  • 分布式光伏发电站数据采集设备管理硬件解决方案
  • 搭建一个基于角色的权限验证框架
  • Qt快捷键说明与用法
  • 【大模型技术教程】FastGPT一站式解决方案[1-部署篇]:轻松实现RAG-智能问答系统
  • 网络安全中GET和POST区别在哪?
  • 虚拟私有云VPC详解和设置方法
  • 抖音生活服务常见玩法及收益情况详解!普通人如何把握机会?