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

简介: 回来啦,《Android面试题思考与解答21年3月刊》送给大家。

前言


回来啦,《Android面试题思考与解答21年3月刊》送给大家。


介绍


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


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


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


以下为2021年3月刊内容。


Activity、PhoneWindow、DecorView、ViewRootImpl 之间的关系?


  • PhoneWindow:是Activity和View交互的中间层,帮助Activity管理View。
  • DecorView:是所有View的最顶层View,是所有View的parent。
  • ViewRootImpl:用于处理View相关的事件,比如绘制,事件分发,也是DecorView的parent。


四者的创建时机?


  • Activity创建于performLaunchActivity方法中,在startActivity时候触发。
  • PhoneWindow,同样创建于performLaunchActivity方法中,再具体点就是Activity的attach方法。
  • DecorView,创建于setContentView->PhoneWindow.installDecor。
  • ViewRootImpl,创建于handleResumeActivity方法中,最后通过addView被创建。


View的第一次绘制发生在什么时候?


第一次绘制就是发生在handleResumeActivity方法中,通过addView方法,创建了ViewRootImpl,并调用了其setView方法。


最后调用到requestLayout方法开始了布局、测量、绘制的流程。


线程更新UI导致崩溃的原因?


在触发绘制方法requestLayout中,有个checkThread方法:


void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }


其中对mThread和当前线程进行了比较。而mThread是在ViewRootImpl实例化的时候赋值的。


所以崩溃的原因就是 view被绘制到界面时候的线程(也就是ViewRootImpl被创建时候的线程)和进行UI更新时候的线程不是同一个线程。


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没关系了呢?


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


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


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


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


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


为什么限制在应用间共享文件


打个比方,应用A有一个文件,绝对路径为file:///storage/emulated/0/Download/photo.jpg


现在应用A想通过其他应用来完成一些需求,比如拍照,就把他的这个文件路径发给了照相应用B,然后应用B照完相就把照片存储到了这个绝对路径。


看起来似乎没有什么问题,但是如果这个应用B是个“坏应用”呢?


  • 泄漏了文件路径,也就是应用隐私。


如果这个应用A是“坏应用”呢?


  • 自己可以不用申请存储权限,利用应用B就达到了存储文件的这一危险权限。


可以看到,这个之前落伍的方案,从自身到对方,都是不太好的选择。


所以Google就想了一个办法,把对文件的访问限制在应用内部。


  • 如果要分享文件路径,不要分享file:// URI这种文件的绝对路径,而是分享content:// URI,这种相对路径,也就是这种格式:content://com.jimu.test.fileprovider/external/photo.jpg


  • 然后其他应用可以通过这个绝对路径来向文件所属应用 索要 文件数据,所以文件所属的应用本身必须拥有文件的访问权限。


也就是应用A分享相对路径给应用B,应用B拿着这个相对路径找到应用A,应用A读取文件内容返给应用B。


介绍下FileProvider


涉及到应用间通信的问题,还记得IPC的几种方式吗?


  • 文件
  • AIDL
  • ContentProvider
  • Socket
  • 等等。


从易用性,安全性,完整度等各个方面考虑,Google选择了ContentProvider为这次限制应用分享文件的 解决方案。于是,FileProvider诞生了。


具体做法就是:


<!-- 配置FileProvider-->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external" path="."/>
</paths>


//修改文件URL获取方式
Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", createImageFile());


这样配置之后,就能生成content:// URI,并且也能通过这个URI来传输文件内容给外部应用。


FileProvider这些配置属性也就是ContentProvider的通用配置:


  • android:name,是ContentProvider的类路径。
  • android:authorities,是唯一标示,一般为包名+.provider
  • android:exported,表示该组件是否能被其他应用使用。
  • android:grantUriPermissions,表示是否允许授权文件的临时访问权限。


其中要注意的是android:exported正常应该是true,因为要给外部应用使用。


但是FileProvider这里设置为false,并且必须为false。


这主要为了保护应用隐私,如果设置为true,那么任何一个应用都可以来访问当前应用的FileProvider了,对于应用文件来说肯定是不可取的,所以Android7.0以上会通过其他方式让外部应用安全的访问到这个文件,而不是普通的ContentProvider访问方式,后面会说到。


当然,也正是因为这个属性为true,所以在Android7.0以下,Android默认是将它当成一个普通的ContentProvider,外部无法通过content:// URI来访问文件。所以一般要判断下系统版本再确定传入的Uri到底是File格式还是content格式。


Service与子线程


关于Service,我的第一反应是运行在后台的服务。


关于后台,我的第一反应又是子线程。


那么Service和子线程到底是什么关系呢?


Service有两个比较重要的元素:


  • 长时间运行。Service可以在Activity被销毁,程序被关闭之后都可以继续运行。
  • 不提供界面的应用组件。这其实解释了后台的意义,Service的后台指的是不和界面交互,不依赖UI元素。


而且比较关键的点是,Service也是运行在主线程之中。


所以运行在后台的Service和运行在后台的线程区别还是挺大的。


  • 首先,所运行的线程不同。Service还是运行在主线程,而子线程肯定是开辟了新的线程。
  • 其次,后台的概念不同。Service的后台指的是不与界面交互,子线程的后台指的是异步运行。
  • 最后,Service作为四大组件之一,控制它也更方便,只要有上下文就可以对其进行控制。


当然,虽然两者概念不同,但是还是有很多合作之处。


Service作为后台运行的组件,其实很多时候也会被用来做耗时操作,那运行在主线程的Service肯定不能直接进行耗时操作,这就需要子线程了。


开启一个后台Service,然后在Service里面进行子线程操作,这样的结合给项目带来的可能性就更大了。


Google也是考虑到这一点,设计出了IntentService这种已经结合好的组件供我们使用。


后台和前台Service


这就涉及到Service的分类了。


如果从是否无感知来分类,Service可以分为前台和后台。前台Service会通过通知的方式让用户感知到,后台有这么一个玩意在运行。


比如音乐类APP,在后台播放音乐的同时,可以发现始终有一个通知显示在前台,让用户知道,后台有一个这么音乐相关的服务。


Android8.0,Google要求如果程序在后台,那么就不能创建后台服务,已经开启的后台服务会在一定时间后被停止。


所以,建议使用前台Service,它拥有更高的优先级,不易被销毁。使用方法如下:


startForegroundService(intent);
    public void onCreate() {
        super.onCreate();
        Notification notification = new Notification.Builder(this)
                .setChannelId(CHANNEL_ID)
                .setContentTitle("主服务")//标题
                .setContentText("运行中...")//内容
                .setSmallIcon(R.mipmap.ic_launcher)
                .build();
        startForeground(1,notification);
    }  
    <!--android 9.0上使用前台服务,需要添加权限-->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />


那后台任务该怎么办呢?官方建议使用 JobScheduler 。


说说JobScheduler


任务调度JobSchedulerAndroid5.0被推出。(可能有的朋友感觉比较陌生,其实他也是通过Service实现的,这个待会再说)


它能做的工作就是可以在你所规定的要求下进行自动任务执行。比如规定时间、网络为WIFI情况、设备空闲、充电时等各种情况下后台自动运行。


所以Google让它来替代后台Service的一部分功能,使用:


  • 首先,创建一个JobService:


public class MyJobService extends JobService {
    @Override
    public boolean onStartJob(JobParameters params) {
        return false;
    }
    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}


  • 然后,注册这个服务(因为JobService也是Service)


<service android:name=".MyJobService"
    android:permission="android.permission.BIND_JOB_SERVICE" />


  • 最后,创建一个JobInfo并执行


JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);  
 ComponentName jobService = new ComponentName(this, MyJobService.class);
 JobInfo jobInfo = new JobInfo.Builder(ID, jobService) 
         .setMinimumLatency(5000)// 任务最少延迟时间 
         .setOverrideDeadline(60000)// 任务deadline,当到期没达到指定条件也会开始执行 
         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)// 网络条件,默认值NETWORK_TYPE_NONE
         .setRequiresCharging(true)// 是否充电 
         .setRequiresDeviceIdle(false)// 设备是否空闲
         .setPersisted(true) //设备重启后是否继续执行
         .setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR) //设置退避/重试策略
         .build();  
 scheduler.schedule(jobInfo);


简单说下原理:


JobSchedulerService是在SystemServer中启动的服务,然后会遍历没有完成的任务,通过Binder找到对应的JobService,执行onStartJob方法,完成任务。具体可以看看参考链接的分析。


所以也就知道了,在5.0之后,如果有需要后台任务执行,特别是需要满足一定条件触发的任务,比如网络电量等等情况,就可以使用JobScheduler。


有的人可能要问了,5.0之前怎么办呢?


  • 可以使用GcmNetworkManager或者BroadcastReceiver等处理部分情况下的任务需求。


Google也是考虑到了这一点,所以将5.0之后的JobScheduler和5.0之前的GcmNetworkManager、GcmNetworkManager、AlarmManager等和任务相关的API相结合,设计出了WorkManager


说说WorkManager


WorkManager 是一个 API,可供您轻松调度那些即使在退出应用或重启设备后仍应运行的可延期异步任务。


作为Jetpack的一员,并不算很新的内容,它的本质就是结合已有的任务调度相关的API,然后根据版本需求等来执行这些任务,官网有一张图:


24.png


所以WorkManager到底能做什么呢?


  • 1、对于一些任务约束能很好的执行,比如网络、设备空闲状态、足够存储空间等条件下需要执行的任务。
  • 2、可以重复、一次性、稳定的执行任务。包括在设备重启之后都能继续任务。
  • 3、可以定义不同工作任务的衔接关系。比如设定一个任务接着一个任务。


总之,它是后台执行任务的一大利器。


onStart可见的解释?onStart和onResume两种状态的设计。


首先,科普官方定义的两个状态。


  • onStart到onStop中间的状态叫做“已开始”状态。
  • onResume到onPause中间的状态叫做“已恢复”状态。


然后我们做个小实验,定义ActivityAActivityBActivityB为Dialog主题,ActivityA中点击可以跳转到B:


image.setOnClickListener {
        startActivity(Intent(this, ActivityB::class.java))
    }
    <activity android:name=".activity.ActivityB"
        android:theme="@style/Theme.AppCompat.Light.Dialog"
        android:launchMode="standard">
    </activity>


进入ActivityA后,点击按钮,跳转到B,这时候A的生命周期走到了onPause,也就是回到了已开始状态。


23.png


这个时候,界面是这个样子:


22.png


ActivityA处在已开始状态,对用户可见。


这里的可见是不是就很好理解了,确实对我们可见了,只不过 不在前台,不能交互

所以延伸到普通的Activity,这个可见,并不是表示用户能用肉眼看到了,而是想表达:


Activity已经显示出来了,但是还不在前台,所以只是可见,但不可交互。


这个可见状态是从onStart开始,onStop结束,我们可以分为两个阶段:


  • onStart到onResume。这个阶段,Activity被创建,布局已加载,但是界面还没绘制,可以说界面都不存在。
  • onPause到onStop。这个阶段,就是我们刚才所做的实验,Activity有界面,只是被新的界面所遮挡,也就是不在前台。


所以综合两个阶段,我们把这种Activity被创建或已经显示出来,但是不在前台,介于两者之间的状态叫做 可见 状态。


到此,我们知道了可见的意思,其实也就知道了另外一个问题,也就是为什么要设计出onStart和onResume这两种状态。


  • onStart和onStop,是从Activity是否可见的角度设计的。
  • onResume和onPause,是从Activity是否位于前台的角度设计的。


所以Activity的生命周期又可以解释为:


被创建(onCreate)——> 可见(onStart)——> 位于前台(onResume)——> 可见但不在前台(onPause)


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