我正在参与「启航计划」

———————————————————————————————————

以下主要针对往期录入的面试题进行一个分类归纳整理,便利咱们一致回忆和参考。本篇是第三集~

着重一下:【因篇幅问题:文中只放部分内容,悉数文档需求的可找作者获取。

第一篇面试题在这: Android中高级进阶开发面试题冲刺合集(一)

第二篇面试题在这: Android中高级进阶开发面试题冲刺合集(二)

Android 方面

Android 调查点比较纷杂,以下针对之前录入的面试题做一个大约的划分:

Android 四大组件相关

1.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毁掉重建后还能获取到数据

2.谈谈 Android 中几种 LaunchMode 的特色和运用场景?

参考答案:

LaunchMode 有四种,分别为 StandardSingleTopSingleTaskSingleInstance,每种办法的完结原理一楼都做了较详细阐明,下面说一下详细运用场景:

  • Standard: Standard 办法是体系默许的发动办法,一般咱们 app 中大部分页面都是由该办法的页面构成的,比较常见的场景是:交际运用中,点击检查用户A信息->检查用户A粉丝->在粉丝中选择检查用户B信息->检查用户A粉丝… 这种状况下一般咱们需求保留用户操作 Activity 栈的页面全部履行次序。
  • SingleTop: SingleTop 办法一般常见于交际运用中的告诉栏行为功用,例如:App 用户收到几条老友恳求的推送音讯,需求用户点击推送告诉进入到恳求者个人信息页,将信息页设置为 SingleTop 办法就能够增强复用性。
  • SingleTask: SingleTask 办法一般用作运用的首页,例如浏览器主页,用户或许从多个运用发动浏览器,但主界面仅仅发动一次,其他状况都会走onNewIntent,并且会清空主界面上面的其他页面。
  • SingleInstance: SingleInstance 办法常运用于独立栈操作的运用,如闹钟的提示页面,当你在A运用中看视频时,闹钟响了,你点击闹钟提示告诉后进入提示详情页面,然后点击回来就再次回到A的视频页面,这样就不会过多搅扰到用户先前的操作了。

3.BroadcastReceiver 与 LocalBroadcastReceiver 有什么差异?

参考答案:

  • BroadcastReceiver 是跨运用播送,运用Binder机制完结,支撑动态和静态两种办法注册办法。
  • LocalBroadcastReceiver 是运用内播送,运用Handler完结,运用了IntentFilter的match功用,供给音讯的发布与接纳功用,完结运用内通讯,功率和安全性比较高,仅支撑动态注册。

4.关于 Context,你了解多少?

参考答案:

  1. 在 Android 平台上 , Context 是一个基本的概念,它在逻辑上表明一个运转期的“上下文”
  2. Context 表现到代码上来说,是个笼统类,其主要表达的行为有:运用体系供给的服务、拜访资源 、信息 存储相关以及AMS的交互
  3. 在 Android 平台上,Activity、Service 和 Application 在本质上都是个 Context
  4. Android 中上下文拜访运用资源或体系服务的动作都被一致封装进 ContextImpl 类中.Activity、 Service、Application 内部都含有自己的 ContextImpl,每逢自己需求拜访运用资源或体系服务时,便是 把恳求托付给内部的 ContextImpl
  5. ContextWrapper 为 Context 的包装类, 它在做和上下文相关的动作时,基本上都是托付给 ContextImpl 去做
  6. ContextImpl 为一个上下文的核心部件,其担任和Android平台进行通讯. 就以发动 activity 动作来说,终究 会走到 ContextImpl 的 startActivity(),而这个函数内部大体上是进一步调用 mMainThread.getInstrumentation().execStartActivity(),然后将语义发送给Android体系
  7. Context数量 = Activity数量 + Service数量 + 1 上面的1表明 Application 数量,一个运用程序里边能够有多个 Application,可是在装备文件 AndroidManifest.xml中只能注册一个, 只要注册的这个 Application 才是真实的 Application

5.IntentFilter 是什么?有哪些运用场景?匹配机制是怎样的?

参考答案:

1.IntentFilter是目的过滤器,常用于Intent的隐式调用匹配。 2.IntentFilter有3种匹配规矩,分别是action、categroy、data。

action的匹配准则: IntentFilter能够有多个action,Intent最多能有1个。 1.如果IntentFilter中不存在action,那么全部的intent都无法经过。 2.如果IntentFilter存在action。 a.如果intent不存在,那么能够经过。 b.如果intent存在,那么intent中的action有必要是IntentFilter中的其间一个,比照差异巨细写。

category的匹配准则: IntentFilter能够有多个category,Intent也能够有多个。 1.如果IntentFilter不存在category,那么全部的intent都无法经过,由于隐式调用的时分,体系默许给Intent附加了“android.intent.category.DEFAULT”。 2.如果IntentFilter存在category a.如果intent不存在,能够经过。 b.如果intent存在,那么intent中的全部category都包含在IntentFilter中,才干够经过。

data的匹配准则: IntentFilter能够有多个data,Intent最多能有1个。 IntentFilter和Intent完全匹配才干经过,也适用于通配符

匹配规矩: Intent需求匹配多组intent-fliter中的任意一组,每一组包含action、data、category,即Intent有必要一起满意这三者的过滤规矩。 在同一个运用中,尽量运用显现目的,由于显现目的比隐式目的的功率高。

6.谈一谈 startService 和 bindService 办法的差异,生命周期以及运用场景?

参考答案:

1、生命周期上的差异

履行startService时,Service会阅历onCreate->onStartCommand。当履行stopService时,直接调用onDestroy办法。调用者如果没有stopService,Service会一向在后台运转,下次调用者再起来仍然能够stopService。
​
履行bindService时,Service会阅历onCreate->onBind。这个时分调用者和Service绑定在一同。调用者调用unbindService办法或许调用者Context不存在了(如Activity被finish了),Service就会调用onUnbind->onDestroy。这里所谓的绑定在一同便是说两者共存亡了。
​
屡次调用startService,该Service只能被创立一次,即该Service的onCreate办法只会被调用一次。可是每次调用startService,onStartCommand办法都会被调用。Service的onStart办法在API 5时被抛弃,代替它的是onStartCommand办法。
​
第一次履行bindService时,onCreate和onBind办法会被调用,可是屡次履行bindService时,onCreate和onBind办法并不会被屡次调用,即并不会屡次创立服务和绑定服务。

2、调用者怎样获取绑定后的Service的办法

onBind回调办法将回来给客户端一个IBinder接口实例,IBinder答应客户端回调服务的办法,比方得到Service运转的状况或其他操作。咱们需求IBinder方针回来详细的Service方针才干操作,所以说详细的Service方针有必要首要完结Binder方针。

3、既运用startService又运用bindService的状况

如果一个Service又被发动又被绑定,则该Service会一向在后台运转。首要不论怎样调用,onCreate始终只会调用一次。对应startService调用多少次,Service的onStart办法便会调用多少次。Service的中止,需求unbindService和stopService一起调用才行。不论startService与bindService的调用次序,如果先调用unbindService,此刻服务不会主动中止,再调用stopService之后,服务才会中止;如果先调用stopService,此刻服务也不会中止,而再调用unbindService或许之前调用bindService的Context不存在了(如Activity被finish的时分)之后,服务才会主动中止。
​
那么,什么状况下既运用startService,又运用bindService呢?
​
如果你仅仅想要发动一个后台服务长时刻进行某项使命,那么运用startService便能够了。如果你还想要与正在运转的Service取得联络,那么有两种办法:一种是运用broadcast,另一种是运用bindService。前者的缺陷是如果交流较为频频,简略形成功用上的问题,而后者则没有这些问题。因而,这种状况就需求startService和bindService一同运用了。
​
其他,如果你的服务仅仅揭露一个长途接口,供衔接上的客户端(Android的Service是C/S架构)长途调用履行办法,这个时分你能够不让服务一开端就运转,而仅仅bindService,这样在第一次bindService的时分才会创立服务的实例运转它,这会节省许多体系资源,特别是如果你的服务是长途服务,那么作用会越显着(当然在Servcie创立的是偶会花去必定时刻,这点需求留意)。  

4、本地服务与长途服务

本地服务依附在主进程上,在必定程度上节省了资源。本地服务由于是在同一进程,因而不需求IPC,也不需求AIDL。相应bindService会便利许多。缺陷是主进程被kill后,服务变会中止。
​
长途服务是独立的进程,对应进程名格式为地点包名加上你指定的android:process字符串。由于是独立的进程,因而在Activity地点进程被kill的是偶,该服务仍然在运转。缺陷是该服务是独立的进程,会占用必定资源,并且运用AIDL进行IPC稍微费事一点。
​
关于startService来说,不论是本地服务仍是长途服务,咱们需求做的作业都一样简略。

7.Service 怎样进行保活?

参考答案:

1:跟各大体系厂商树立合作联络,把App参加体系内存整理的白名单

2:白色保活

用startForeground()发动前台服务,这是官方供给的后台保活办法,缺乏的便是告诉栏会常驻一条告诉,像360的状况栏。

3:灰色保活

开启前台Service,开启另一个Service将告诉栏移除,其oom_adj值仍是没变的,这样用户就发觉不到app在后台保活。 用播送唤醒自启,像开机播送、网络切换播送等,但在国产Rom中简直都被堵上了。 多个app相关唤醒,就像BAT的全家桶,翻开一个App的时分会发动、唤醒其他App,包含一些第三方推送也是,关于大大都独自app,比较难以完结。

4:黑色保活

1 像素activity保活计划,监听息屏事情,在息屏时发动个一像素的activity,提高自身优先级; Service中循环播映一段无声音频,假装音乐app,播映音乐中的app优先级仍是蛮高的,也能很大程度保活作用较好,但耗电量高,慎重运用; 双进程守护,这在国产rom中简直没用,由于划掉app会把全部相关进程都杀死。 3、完结进程:

1)、用startForeground()发动前台服务

前台Service,运用startForeground这个Service尽量要轻,不要占用过多的体系资源,不然体系在资源紧张时,照样会将其杀死。

DaemonService.java

能够参考下面的 Android完结进程保活计划解析

8.简略介绍下 ContentProvider 是怎样完结数据同享的?

参考答案:

当一个运用程序要把自己的数据露出给其他程序时,能够经过ContentProvider来完结。 其他运用能够经过ContenrResolver来操作ContentProvider露出的数据。

如果运用程序A经过ContentProvider露出自己的数据操作接口,那么不论A 是否发动,其他程序都能够经过该接口来操作A的内部数据,常有增、删、查、改。

ContentProvider是以Uri的办法对外供给数据,ContenrResolver是依据Uri来拜访数据。

步骤

界说自己的ContentProvider类,该类需求承继Android体系供给的ContentProvider基类。

在Manifest.xml 文件中注册ContentProvider,(四大组件的运用都需求在Manifest文件中注册) 注册时需求绑定一个URL。

例如: android:authorities=”com.myit.providers.MyProvider” 阐明:authorities就相当于为该ContentProvider指定URL。 注册后,其他运用程序就能够经过该Uri来拜访MyProvider所露出的数据了。 其他程序运用ContentResolver来操作。

调用Activity的ContentResolver获取ContentResolver方针 调用ContentResolver的insert(),delete(),update(),query()进行增删改查。 一般来说,ContentProvider是单例办法,也便是说,当多个运用程序经过ContentResolver来操作ContentProvider供给的数据时,ContentResolver调用的数据操作将会托付给同一个ContentResolver。

9.说下切换横竖屏时 Activity 的生命周期改动?

参考答案:

竖屏: 发动:onCreat->onStart->onResume. 切换横屏时: onPause-> onSaveInstanceState ->onStop->onDestory

onCreat->onStart->onSaveInstanceState->onResume.

可是,咱们在如果装备这个特点:android:configChanges=”orientation|keyboardHidden|screenSize” 就不会在调用Activity的生命周期,只会调用onConfigurationChanged办法

10.Activity 中 onNewIntent 办法的调用机遇和运用场景?

参考答案:

Activity 的 onNewIntent办法的调用可总结如下:

  在该Activity的实例现已存在于Task和Back stack中(或许浅显的说能够经过按回来键回来到该Activity )时,当运用intent来再次发动该Activity的时分,如果此次发动不创立该Activity的新实例,则体系会调用原有实例的onNewIntent()办法来处理此intent.

  且在下面状况下体系不会创立该Activity的新实例:

  1,如果该Activity在Manifest中的android:launchMode界说为singleTask或许singleInstance.

  2,如果该Activity在Manifest中的android:launchMode界说为singleTop且该实例坐落Back stack的栈顶.

  3,如果该Activity在Manifest中的android:launchMode=“singleInstance”,或许intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)标志.

  4,如果上述intent中包含 Intent.FLAG_ACTIVITY_CLEAR_TOP 标志和且包含 Intent.FLAG_ACTIVITY_SINGLE_TOP 标志.

  5,如果上述intent中包含 Intent.FLAG_ACTIVITY_SINGLE_TOP 标志且该实例坐落Back stack的栈顶.

  上述状况满意其一,则体系将不会创立该Activity的新实例.

  依据现有实例地点的状况不同onNewIntent()办法的调用机遇也不同,总的说如果体系调用onNewIntent()办法则体系会在onResume()办法履行之前调用它.这也是官方API为什么只说”you can count on onResume() being called after this method”,而不详细阐明调用机遇的原因.

11.Intent 传输数据的巨细有限制吗?怎样解决?

参考答案:

先说定论:

有巨细限制

再说原因:

Intent 是音讯传递方针,用于各组件间通讯。各组件以及个程序间通讯都用到了进程间通讯。因而 Intent 的数据传递是根据 Binder 的,Intent 中的数据会存储在 Bundle 中,然后 IPC 进程中会将各个数据以 Parcel 的办法存储在 Binder 的事物缓冲区(Binder transaction buffer)进程传递,而 Binder 的事物缓冲区有个固定的巨细,巨细在 1M 附近。由于这 1M 巨细是当时进程同享的,Intent 中也会带有其他相关的必要信息,所以实践运用中比这个数字要小许多。

解决办法:

  1. 下降传递数据的巨细,或考虑其他办法,见2;
  2. IPC: 将大数据缓存到文件,或许存入数据库,或许图片运用 id 等;运用 Socket;
  3. 非 IPC:能够考虑同享内存,EventBus 等

12.说说 ContentProvider、ContentResolver、ContentObserver 之间的联络?

参考答案:

ContentProvider * 内容供给者, 用于对外供给数据,比方联络人运用中便是用了ContentProvider, * 一个运用能够完结ContentProvider来供给给其他运用操作,经过ContentResolver来操作其他运用数据

ContentResolver * 内容解析者, 用于获取内容供给者供给的数据 * ContentResolver.notifyChange(uri)宣布音讯

ContentObserver * 内容监听者,能够监听数据的改动状况 * 调查(捕捉)特定的Uri引起的数据库的改动 * ContentResolver.registerContentObserver()监听音讯

归纳: 运用ContentResolver来获取ContentProvider供给的数据, 一起注册ContentObserver监听数据的改动

13.说说 Activity 加载的流程?

参考答案:

App 发动流程(根据Android8.0):

  • 点击桌面 App 图标,Launcher 进程选用 Binder IPC(详细为ActivityManager.getService 获取 AMS 实例) 向 system_server 的 AMS 发起 startActivity 恳求
  • system_server 进程收到恳求后,向 Zygote 进程发送创立进程的恳求;
  • Zygote 进程 fork 出新的子进程,即 App 进程
  • App 进程创当即初始化 ActivityThread,然后经过 Binder IPC 向 system_server 进程的 AMS 发起 attachApplication 恳求
  • system_server 进程的 AMS 在收到 attachApplication 恳求后,做一系列操作后,告诉 ApplicationThread bindApplication,然后发送 H.BIND_APPLICATION 音讯
  • 主线程收到 H.BIND_APPLICATION 音讯,调用 handleBindApplication 处理后做一系列的初始化操作,初始化 Application 等
  • system_server 进程的 AMS 在 bindApplication 后,会调用 ActivityStackSupervisor.attachApplicationLocked,之后经过一系列操作,在 realStartActivityLocked 办法经过 Binder IPC 向 App 进程发送 scheduleLaunchActivity 恳求;
  • App进程的 binder 线程(ApplicationThread)在收到恳求后,经过 handler 向主线程发送 LAUNCH_ACTIVITY 音讯;
  • 主线程收到 message 后经过 handleLaunchActivity,performLaunchActivity 办法,然后经过反射机制创立方针 Activity;
  • 经过 Activity attach 办法创立 window 并且和 Activity 相关,然后设置 WindowManager 用来办理 window,然后告诉 Activity 已创立,即调用 onCreate
  • 然后调用 handleResumeActivity,Activity 可见

弥补:

  • ActivityManagerService 是一个注册到 SystemServer 进程并完结了 IActivityManager 的 Binder,能够经过 ActivityManager 的 getService 办法获取 AMS 的代理方针,进而调用 AMS 办法
  • ApplicationThread 是 ActivityThread 的内部类,是一个完结了 IApplicationThread 的 Binder。AMS经过 Binder IPC 经 ApplicationThread 对运用进行操控
  • 一般的 Activity 发动和本流程差不多,至少不需求再创立 App 进程了
  • Activity A 发动 Activity B,A 先 pause 然后 B 才干 resume,因而在 onPause 中不能做耗时操作,不然会影响下一个 Activity 的发动

Android 异步使命和音讯机制

1.HandlerThread 的运用场景和完结原理?

参考答案:

HandlerThread 是 Android 封装的一个线程类,将 Thread 跟 Handler 封装。运用步骤如下:

  1. 创立 HandlerThread 实例方针
   HandlerThread mHandlerThread = new HandlerThread("mHandlerThread");
  1. 发动线程
   mHandlerThread .start();
  1. 创立Handler方针,重写handleMessage办法
   Handler mHandler= new Handler( mHandlerThread.getLooper() ) {
      @Override
      public boolean handleMessage(Message msg) {
        //音讯处理
        return true;
      }
   });
  1. 运用作业线程Handler向作业线程的音讯行列发送音讯:
   Message  message = Message.obtain();
   message.what = “2message.obj = "骚风"
   mHandler.sendMessage(message);
  1. 结束线程,即中止线程的音讯循环
   mHandlerThread.quit();

2.IntentService 的运用场景和内部完结原理?

参考答案:

IntentServiceService 的子类,默许为咱们开启了一个作业线程,运用这个作业线程逐一处理全部发动恳求,在使命履行结束后会主动中止服务,运用简略,只需完结一个办法 onHandleIntent,该办法会接纳每个发动恳求的 Intent,能够履行后台作业和耗时操作。能够发动 IntentService 屡次,而每一个耗时操作会以行列的办法在 IntentService 的 onHandlerIntent 回调办法中履行,并且,每一次只会履行一个作业线程,履行完第一个再履行第二个。并且等候全部音讯都履行完后才中止服务。

IntentService 适用于 APP 在不影响当时用户的操作的条件下,在后台默默的做一些操作。

IntentService源码:

  1. 经过 HandlerThread 独自开启一个名为 IntentService 的线程
  2. 创立一个名叫 ServiceHandler 的内部 Handler
  3. 把内部Handler与HandlerThread所对应的子线程进行绑定
  4. 经过 onStartCommand() 传递给服务 intent,顺次刺进到作业行列中,并逐一发送给 onHandleIntent()
  5. 经过 onHandleIntent() 来顺次处理全部 Intent 恳求方针所对应的使命

运用示例:

public class MyIntentService extends IntentService {
   public static final String TAG ="MyIntentService";
   public MyIntentService() {
     super("MyIntentService");
   }
   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
    boolean isMainThread =  Thread.currentThread() == Looper.getMainLooper().getThread();
     Log.i(TAG,"is main thread:"+isMainThread); // 这里会打印false,阐明不是主线程
     // 模仿耗时操作
     download();
   }
   /**
   * 模仿履行下载
   */
   private void download(){
    try {
      Thread.sleep(5000);
      Log.i(TAG,"下载完结...");
    }catch (Exception e){
      e.printStackTrace();
    }
   }
}

3.AsyncTask 的长处和缺陷?内部完结原理是怎样的?

参考答案:

长处:运用便利 缺陷:默许运用串行使命履行功率低,不能充分运用多线程加快履行速度;如果运用并行使命履行,在使命特别多的时分会堵塞UI线程取得CPU时刻片,后续做线程收敛需求自界说AsynTask,将其设置为全局一致的线程池,改动量比较大

AsyncTask的完结原理: 1.AsyncTask是一个笼统类,主要由Handler+2个线程池构成,SERIAL_EXECUTOR是使命行列线程池,用于调度使命,按次序排列履行,THREAD_POOL_EXECUTOR是履行线程池,真实履行详细的线程使命。Handler用于作业线程和主线程的异步通讯。

2.AsyncTask<Params,Progress,Result>,其间Params是doInBackground()办法的参数类型,Result是doInBackground()办法的回来值类型,Progress是onProgressUpdate()办法的参数类型。

3.当履行execute()办法的时分,其实便是调用SERIAL_EXECUTOR的execute()办法,便是把使命增加到行列的尾部,然后从头开端取出行列中的使命,调用THREAD_POOL_EXECUTOR的execute()办法顺次履行,当行列中没有使命时就中止。

4.AsyncTask只能履行一次execute(params)办法,不然会报错。可是SERIAL_EXECUTOR和THREAD_POOL_EXECUTOR线程池都是静态的,所以能够形成行列。

Q:AsyncTask只能履行一次execute()办法,那么为什么用线程池行列办理 ? 由于SERIAL_EXECUTOR和THREAD_POOL_EXECUTOR线程池都是静态的,全部的AsyncTask实例都同享这2个线程池,因而形成了行列。

Q:AsyncTask的onPreExecute()、doInBackground()、onPostExecute()办法的调用流程? AsyncTask在创立方针的时分,会在结构函数中创立mWorker(workerRunnable)和mFuture(FutureTask)方针。 mWorker完结了Callable接口的call()办法,在call()办法中,调用了doInBackground()办法,并在终究调用了postResult()办法,也便是经过Handler发送音讯给主线程,在主线程中调用AsyncTask的finish()办法,决议是调用onCancelled()仍是onPostExecute(). mFuture完结了Runnable和Future接口,在创立方针时,初始化成员变量mWorker,在run()办法中,调用mWorker的call()办法。 当asyncTask履行execute()办法的时分,会先调用onPreExecute()办法,然后调用SERIAL_EXECUTOR的execute(mFuture),把使命参加到行列的尾部等候履行。履行的时分调用THREAD_POOL_EXECUTOR的execute(mFuture).

4.谈谈你对 Activity.runOnUiThread 的了解?

参考答案:

一般是用来将一个runnable绑定到主线程,在runOnUiThread源码里边会判别当时runnable是否是主线程,如果是直接run,如果不是,经过一个默许的空结构函数handler将runnable post 到looper里边,创立结构函数handler,会默许绑定一个主线程的looper方针

5.Android 的子线程能否做到更新 UI?

参考答案:

子线程是不能直接更新UI的

留意这句话,是不能直接更新,不是不能更新(极点状况下可更新)

制作进程要坚持同步(不然页面不流通),而咱们的主线程担任制作ui,极点状况便是,在Activity的onResume(含)之前的生命周期中子线程都能够进行更新ui,也便是 onCreate,onStart和onResume,此刻主线程的制作还没开端。

6.谈谈 Android 中音讯机制和原理?

参考答案:

首要在主线程创立一个 Handler 方针 ,并重写 handleMessage() 办法。然后当在子线程中需求进行更新UI的操作,咱们就创立一个 Message 方针,并经过 Handler 发送这条音讯出去。之后这条音讯被参加到 MessageQueue 行列中等候被处理,经过 Looper 方针会一向尝试从 Message Queue 中取出待处理的音讯,终究分发回 HandlerhandleMessage() 办法中。

7.为什么在子线程中创立 Handler 会抛反常?

参考答案:

子线程创立Handler会抛出反常的原因是由于在looper里边ThreadLocal sThreadLocal = new ThreadLocal() ThreadLocal 便是为了保存的Looper只能在指定线程中获取Looper 由于子线程创立new Handler()并没有指定Looper 所以它就去获取ActivityThread的main办法中创立的looper 而此刻的这个looper 是受线程维护的 所以子线程是无法获取的 因而抛出反常所以在子线程中没有looper 如果需求在子线程中开启handle要手动创立looper

8.试从源码角度分析 Handler 的 post 和 sendMessage 办法的差异和运用场景?

参考答案:

post是将一个Runnbale封装成Message, 并赋值给callback参数,从这个进程之后就和sendMessge没有任何差异,会接着履行sendMessageDelayed->sendMessageAtTime,然后进入音讯行列等候履行,抵达Message履行时刻时调用Handler的dispatchMessage办法, 其间有逻辑判别: 如果Message的callback不为空,就会履行callback的run办法,如果Message的callback为null,就会判别Handler的callback是否为空,不为空的话会履行Handler的callback的handleMessage办法,如果Handler的callback为空,则会履行Handler的handleMessage办法。 所以:

  1. post是归于sendMessage的一种赋值callback的特例
  2. post和sendMessage本质上没有差异,两种都会涉及到内存走漏的问题
  3. post办法合作lambda表达式写法更精简

话外:

  1. 现在都是运用rxjava或许其他构建好的线程切换逻辑,以前有一段时刻我是手写handle的主线程和子线程切换,如果遇到这种常常需求切换线程的逻辑时,我觉得或许sendMessage办法更合适一些,举个栗子: post办法: Thread1.post(() -> (Thread2.post(() -> Thread1.post(…….);,这种写法嵌套如同有点儿多,可是线程切换清晰一点儿 sendMessage写法你懂得我就不写了,只需构建好两个Handler, 代码更简洁一点儿 这两种哪个比较好仍是看个人习惯吧。
  2. 涉及到内存走漏问题时无法直接运用lambda表达式,如果有多个不同的Message需求处理的话我觉得大都场景下sendMessage更好一点儿,毕竟写一个弱引证就行了

9.Handler 中有 Loop 死循环,为什么没有堵塞主线程,原理是什么?

参考答案:

主线程挂起

Looper 是一个死循环, 不断的读取MessageQueue中的音讯, loop 办法会调用 MessageQueue 的 next 办法来获取新的音讯, next 操作是一个堵塞操作,当没有音讯的时分 next 办法会一向堵塞, 进而导致 loop 一向堵塞, 理论上 messageQueue.nativePollOnce 会让线程挂起-堵塞-block 住, 可是为什么, 在发送 delay 10s 的音讯, 假定音讯行列中, 目前只要这一个音讯; 那么为什么在这 10s 内, UI是可操作的, 或许列表页是可滑动的, 或许动画仍是能够履行的? 先不讲 nativePollOnce 是怎样完结的堵塞, 咱们还知道, 其他一个 nativeWake, 是完结线程唤醒的; 那么什么时分会, 触发这个办法的调用呢, 便是在有新音讯增加进来的时分, 可是并没有手动增加音讯啊? display 每隔16.6秒, 改写一次屏幕; SurfaceFlingerVsyncChoreographer 每隔16.6秒, 发送一个 vSync 信号; FrameDisplayEventReceiver 收到信号后, 调用 onVsync 办法, 经过 handler 音讯发送到主线程处理, 所以就会有音讯增加进来, UI线程就会被唤醒; 事实上, 安卓体系, 不止有一个屏幕改写的信号, 还有其他的机制, 比方输入法和体系播送, 也会往主线程的 MessageQueue 增加音讯; 所以, 能够了解为, 主线程也是随时挂起, 随时被堵塞的;

体系怎样完结的堵塞与唤醒

这种机制是经过pipe(管道)机制完结的; 简略来说, 管道便是一个文件 在管道的两头, 分别是两个翻开文件的, 文件描述符, 这两个翻开文件描述符, 都是对应同一个文件, 其间一个是用来读的, 别一个是用来写的; 一般的运用办法便是, 一个线程经过读文件描述符, 来读管道的内容, 当管道没有内容时, 这个线程就会进入等候状况, 而其他一个线程, 经过写文件描述符, 来向管道中写入内容, 写入内容的时分, 如果另一规矩有线程, 正在等候管道中的内容, 那么这个线程就会被唤醒; 这个等候和唤醒的操作是怎样进行的呢, 这就要凭借 Linux 体系中的 epoll 机制了, Linux 体系中的 epoll 机制为处理大批量句柄而作了改善的 poll, 是 Linux 下多路复用 IO 接口 select/poll 的增强版本, 它能明显削减程序, 在很多并发衔接中, 只要少数活跃的状况下的体系 CPU 运用率;

即当管道中有内容可读时, 就唤醒当时正在等候管道中的内容的线程;

怎样证明, 线程被挂起了

@Override
public void onCreateData(@Nullable Bundle bundle) {
​
   new Thread() {
     @SuppressLint("HandlerLeak")
     @Override
     public void run() {
       super.run();
       LogTrack.v("thread.id = " + Thread.currentThread().getId());
       Looper.prepare();
       Handler handler = new Handler(Looper.getMainLooper()) {
         @Override
         public void handleMessage(Message msg) {
           super.handleMessage(msg);
           LogTrack.v("thread.id = " + Thread.currentThread().getId() + ", what = " + msg.what);
         }
       };
       LogTrack.w("loop.之前");  // 履行了
       Looper.loop();  // 履行了
       LogTrack.w("loop.之后");  // 无法履行
     }
   }.start();
​
}

Android UI 制作相关

此类主要包含 Android 的 View 制作进程、常见 UI 组件、自界说 View、动画等。

1.Android 补间动画和特点动画的差异?

参考答案:

特性 补间动画 特点动画
view 动画 支撑 支撑
非view动画 不支撑 支撑
可扩展性和灵活性
view特点是否改动 无改动 产生改动
杂乱动画才能 局限 良好
场景运用规模 一般 满意大部分运用场景

2.Window 和 DecorView 是什么?DecorView 又是怎样和 Window 树立联络的?

参考答案:

WindowWindowManager 最顶层的视图,它担任背景(窗口背景)、Title之类的规范的UI元素,Window 是一个笼统类,整个Android体系中, PhoneWindowWindow 的仅有完结类。至于 DecorView,它是一个尖端 View,内部会包含一个竖直方向的LinearLayout,这个 LinearLayout 有上下两部分,分为 titlebar 和 contentParent 两个子元素,contentParent 的 id 是 content,而咱们自界说的 Activity 的布局便是 contentParent 里边的一个子元素。View 层的全部事情都要先经过 DecorView 后才传递给咱们的 ViewDecorViewWindow 的一个变量,即 DecorView 作为全部视图的根布局,被 Window 所持有,咱们自界说的 View 会被增加到 DecorView ,而DecorView 又会被增加到 Window 中加载和烘托显现。此处放一张它们的简略内部层次结构图:

Android中高级进阶开发面试题冲刺合集(三)

3.简述一下 Android 中 UI 的改写机制?

参考答案:

界面改写的本质流程

  1. 经过ViewRootImplscheduleTraversals()进行界面的三大流程。
  2. 调用到scheduleTraversals()时不会当即履行,而是将该操作保存到待履行行列中。并给底层的改写信号注册监听。
  3. VSYNC信号到来时,会从待履行行列中取出对应的scheduleTraversals()操作,并将其参加到主线程音讯行列中
  4. 主线程音讯行列中取出并履行三大流程: onMeasure()-onLayout()-onDraw()

同步屏障的作用

  1. 同步屏障用于堵塞住全部的同步音讯(底层VSYNC的回调onVsync办法提交的音讯是异步音讯)
  2. 用于确保界面改写功用的performTraversals()的优先履行。

同步屏障的原理?

  1. 主线程的Looper会一向循环调用MessageQueuenext办法并且取出行列头部的Message履行,遇到同步屏障(一种特殊音讯)后会去寻觅异步音讯履行。如果没有找到异步音讯就会一向堵塞下去,除非将同步屏障取出,不然永远不会履行同步音讯
  2. 界面改写操作是异步音讯,具有最高优先级
  3. 咱们发送的音讯是同步音讯,再多耗时操作也不会影响UI的改写操作

4.你以为 LinearLayout、FrameLayout 和 RelativeLayout 哪个功率高, 为什么?

参考答案:

关于比较三者的功率那肯定是要在相同布局条件下比较制作的流通度及制作进程,在这里流通度不好表达,并且受其他外部因素搅扰比较多,比方CPU、GPU等等,我说下在制作进程中的比较:

1、Fragment是从上到下的一个堆叠的办法布局的,那当然是制作速度最快,只需求将自身制作出来即可,可是由于它的制作办法导致在杂乱场景中直接是不能运用的,所以作业功率来说Fragment仅运用于单一场景

2、LinearLayout 在两个方向上制作的布局,在作业中运用页比较多,制作的时分只需求按照指定的方向制作,制作功率比Fragment要慢,但运用场景比较多

3、RelativeLayout 它的没个子控件都是需求相对的其他控件来核算,按照View树的制作流程、在不同的分支上要进行核算相对应的方位,制作功率最低,可是一般作业中的布局运用较多,所以说这三者之间功率分开来讲个有优势、缺乏,那一同来讲也是有优势、缺乏,所以不能肯定的差异三者的功率。

5.说一下 Android 中的事情分发机制?

参考答案:

1.触发进程:Activity->Window->DocerView->ViewGroup->View,View不触发再回来由父级处理顺次向上推。 2.在每个阶段都要经过三个办法 dispatchTouchEvent(分发)、onInterceptTouchEvent(阻拦)、onTouch(处理)

大体流程: Activity中走了Window 的 dispatch,Window 的 dispatch 办法直接走了 DocerView 的 dispatch 办法,DocerView 又直接分发给了 ViewGroup,ViewGroup 中走的是 onInterce 判别是否阻拦,阻拦的话会走 onTouch 来处理,不阻拦则持续下发给 View。到 View 这里现已是最底层了,View 若持续不处理,那就调用上层的 onTouch 处理,上层不处理持续往上推。

6.有针对 RecyclerView 做过哪些优化?

参考答案:

RecyclerView作为android的重要View,有很大的灵活性,能够代替ListView GridView ScrollView,所以需求深入了解一下Rv的功用以及怎样去处理优化,完结愈加流通体验,这点是毋庸置疑的,所谓的RV优化其实也是对适配器以及改写数据的,还有资源复用的优化,下面是本人对RV的一点点优化处理:

1 onBindViewHolder 这个办法含义应该都知道是绑定数据,并且是在UI线程,所以要尽量在这个办法中少做一些事务处理 2 数据优化 选用android Support 包下的DIffUtil调集工具类结合RV分页加载会愈加友好,节省功用 3item优化 削减item的View的层级,(pps:当然引荐把一个item自界说成一个View,如果有才能的话),如果item的高度固定的话能够设置setHasFixedSize(true),防止requestLayout浪费资源 4 运用RecycledViewPool RecycledViewPool是对item进行缓存的,item相同的不同RV能够才运用这种办法进行功用提高 5 Prefetch预取 这是在RV25.1.0及以上增加的新功用 6 资源收回 经过重写RecyclerView.onViewRecycled(holder)来合理的收回资源。

7.谈谈你是怎样优化 ListView 的?

参考答案:

下面是优化主张: 怎样最大化的优化ListView的功用?

  • 1.在adapter中的getView办法中尽量少运用逻辑
  • 2.尽最大或许防止GC
  • 3.滑动的时分不载入图片
  • 4.将ListView的scrollingCache和animateCache设置为false
  • 5.item的布局层级越少越好
  • 6.运用ViewHolder

1.在adapter中的getView办法中尽量少运用逻辑

不要在你的getView()中写过多的逻辑代码,咱们能够将这些代码放在其他当地。比方:

优化前的getView():

@Override
public View getView(int position, View convertView, ViewGroup paramViewGroup) {
     Object current_event = mObjects.get(position);
     ViewHolder holder = null;
     if (convertView == null) {
         holder = new ViewHolder();
         convertView = inflater.inflate(R.layout.row_event, null);
         holder.ThreeDimension = (ImageView) convertView.findViewById(R.id.ThreeDim);
         holder.EventPoster = (ImageView) convertView.findViewById(R.id.EventPoster);
         convertView.setTag(holder);
​
     } else {
         holder = (ViewHolder) convertView.getTag();
     }
​
    //在这里进行逻辑推断。这是有问题的 
     if (doesSomeComplexChecking()) {
         holder.ThreeDimention.setVisibility(View.VISIBLE);
     } else {
         holder.ThreeDimention.setVisibility(View.GONE); 
     }
​
     // 这是设置image的參数,每次getView办法运转时都会运转这段代码。这显然是有问题的
     RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(measuredwidth, rowHeight);
     holder.EventPoster.setLayoutParams(imageParams);
​
     return convertView;
}

优化后的getView():

@Override
public View getView(int position, View convertView, ViewGroup paramViewGroup) {
   Object object = mObjects.get(position);
   ViewHolder holder = null;
​
   if (convertView == null) {
       holder = new ViewHolder();
       convertView = inflater.inflate(R.layout.row_event, null);
       holder.ThreeDimension = (ImageView) convertView.findViewById(R.id.ThreeDim);
       holder.EventPoster = (ImageView) convertView.findViewById(R.id.EventPoster);
       //设置參数说到这里,仅仅有第一次的时分会运转,之后会复用 
       RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(measuredwidth, rowHeight);
       holder.EventPoster.setLayoutParams(imageParams);
       convertView.setTag(holder);
   } else {
       holder = (ViewHolder) convertView.getTag();
   }
​
   // 咱们直接经过方针的getter办法替代刚才那些逻辑推断。那些逻辑推断放到其他当地去运转了
   holder.ThreeDimension.setVisibility(object.getVisibility());
​
   return convertView;
}

2.GC 垃圾收回器

当你创立了很多的方针的时分。GC就会频频的运转。所以在getView()办法中不要创立十分多的方针。最好的优化是,不要在ViewHolder以外创立不论什么方针。假定你的你的log里边发现“GC has freed some memory”频频出现的话。那你的程序肯定有问题了。

你能够检查一下: a) item布局的层级是否太深 b) getView()办法中是否有很多方针存在 c) ListView的布局特点

3.载入图片

假定你的ListView中需求显现从网络上下载的图片的话。咱们不要在ListView滑动的时分载入图片,那样会使ListView变得卡顿,所以咱们需求再监听器里边监听ListView的状况。假定滑动的时分,中止载入图片,假定没有滑动,则開始载入图片

listView.setOnScrollListener(new OnScrollListener() {
​
       @Override
       public void onScrollStateChanged(AbsListView listView, int scrollState) {
           //中止载入图片 
           if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
               imageLoader.stopProcessingQueue();
           } else {
           //開始载入图片
               imageLoader.startProcessingQueue();
           }
       }
​
       @Override
       public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
           // TODO Auto-generated method stub
​
       }
   });

4.将ListView的scrollingCache和animateCache设置为false

scrollingCache: scrollingCache本质上是drawing cache,你能够让一个View将他自己的drawing保存在cache中(保存为一个bitmap),这样下次再显现View的时分就不必重画了,而是从cache中取出。默许状况下drawing cahce是禁用的。由于它太耗内存了,可是它的确比重画来的愈加平滑。

而在ListView中,scrollingCache是默许开启的,咱们能够手动将它封闭。

animateCache: ListView默许开启了animateCache,这会消耗很多的内存,因而会频频调用GC,咱们能够手动将它封闭掉

优化前的ListView

<ListView
     android:id="@android:id/list"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:cacheColorHint="#00000000"
     android:divider="@color/list_background_color"
     android:dividerHeight="0dp"
     android:listSelector="#00000000"
     android:smoothScrollbar="true"
     android:visibility="gone" /> 

优化后的ListView

<ListView
     android:id="@android:id/list"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:divider="@color/list_background_color"
     android:dividerHeight="0dp"
     android:listSelector="#00000000"
     android:scrollingCache="false"
     android:animationCache="false"
     android:smoothScrollbar="true"
     android:visibility="gone" />

5.下降item的布局的深度

咱们应该尽量下降item布局深度,由于当滑动ListView的时分,这回直接导致測量与制作,因而会浪费很多的时刻。所以咱们应该将一些不必要的布局嵌套联络去掉。下降item布局深度

6.运用ViewHolder

这个咱们应该十分了解了,可是不要小看这个ViewHolder,它能够大大提高咱们ListView的功用

再次主张运用RecyclerView

8.谈一谈自界说 RecyclerView.LayoutManager 的流程?

参考答案:

1.确认Itemview的LayoutParams generateDefaultLayoutParams

2.确认全部itemview在recyclerview的方位,并且收回和复用itemview onLayoutChildren

3.增加滑动 canScrollVertically

重点其实是 收回和复用itemview,你需求判别什么时分收回itemview,什么时分复用itemview

9.什么是 RemoteViews?运用场景有哪些?

参考答案:

咱们的告诉和桌面小组件分别是由NotificationManager和AppWidgetManager办理的,而NotificationManager和AppWidgetManager又经过Binder分别和SystemServer进程中的NotificationManagerServer和AppWidgetService进行通讯,这就构成了跨进程通讯的场景。

  RemoteViews完结了Parcelable接口,因而它能够在进程间进行传输。

  首要,RemoteViews经过Binder传递到SystemServer进程中,体系会依据RemoteViews供给的包名等信息,去项目包中找到RemoteViews显现的布局等资源,然后经过LayoutInflator去加载RemoteViews中的布局文件,接着,体系会对RemoteViews进行一系列的更新操作,这些操作都是经过RemoteViews方针的set办法进行的,这些更新操作并不是当即履行的,而是在RemoteViews被加载完结之后才履行的,详细流程是:咱们调用了set办法后,经过NotificationManager和AppWidgetManager来提交更新使命,然后在SystemServer进程中进行详细的更新操作。

10.谈一谈获取View宽高的几种办法?

参考答案:

1.OnGlobalLayoutListener获取 2.OnPreDrawListener获取 3.OnLayoutChangeListener获取 4.重写View的onSizeChanged() 5.运用View.post()办法

11.View.post() 为什么能够获取到宽高信息?

参考答案:

View.post办法调用时,如果在 View 还没开端制作时( Activity 的 onResume办法还没回调之前 或许onResume办法履行了,可是 ViewRootImpl 的 performTraversals 还没开端履行)就会用一个初始长度为 4 的数组缓存起来(Runnable 数量大于4时会进行扩容),ViewRootImpl 在初始化时创立了一个 View.AttchInfo 方针并绑定 ViewRootImpl 的 Handler ,该 Handler 也用于发送 View 制作相关的 msg ;

比及 ViewRootImpl履行 performTraversals办法时(此刻 Activity 现已回调了onResume),会装备 View 的 AttchInfo 方针并且经过 View 的 dispatchAttachedToWindow 办法传入到 View 里边完结绑定,在该办法中会取出 View 缓存的 Runnable 并用 View.AttchInfo 的 Handler 来进行 post 办法,这姿态就会参加到 MessageQueue 里边进行排队,比及这些缓存 Runnable 履行时,主线程里边 View的制作流程也就结束了,所以这时分 Looper 取出这些缓存的Runnable 履行时就能够拿到 View 的宽高

那么,什么状况下View.post 办法不会履行呢?如果 Activity 由于某些原因没有履行到 onResume 的话,无法顺利调用 ViewRootImpl 的 performTraversals 的话,View.post 办法就不会履行

提示:如果 View 是 new 出来的,并且没有经过 addView 等办法依靠到 DecorView 上面,它的 post 办法也是不会履行的,由于它没有机会和 ViewRootImpl 进行互动了

12.谈一谈特点动画的插值器和估值器?

参考答案:

1、插值器,依据时刻(动画时常)消逝的百分比来核算特点改动的百分比。体系默许的有匀速,加减速,减速插值器。 2、估值器,经过上面插值器得到的百分比核算出详细改动的值。体系默许的有整型,浮点型,颜色估值器 3、自界说只需求重写他们的evaluate办法就能够了。

13.getDimension、getDimensionPixelOffset 和 getDimensionPixelSize 三者的差异?

参考答案:

相同点: 单位为dp/sp时,都会乘以density,单位为px则不乘 不同点: 1、getDimension回来的是float值 2、getDimensionPixelSize,回来的是int值,float转成int时,四舍五入 3、getDimensionPixelOffset,回来的是int值,float转int时,向下取整(即忽略小数值)

14.请谈谈源码中 StaticLayout 的用法和运用场景?

参考答案:

结构办法:

public StaticLayout(CharSequence source, int bufstart, int bufend,
   TextPaint paint, int outerwidth,
   Alignment align,
   float spacingmult, float spacingadd,
   boolean includepad,
   TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
     this(source, bufstart, bufend, paint, outerwidth, align,
     TextDirectionHeuristics.FIRSTSTRONG_LTR,
     spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
}

阐明参数的作用:

CharSequence source 需求分行的字符串
int bufstart 需求分行的字符串从第几的方位开端
int bufend 需求分行的字符串到哪里结束
TextPaint paint 画笔方针
int outerwidth layout的宽度,超出时换行
Alignment align layout的对其办法,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三种
float spacingmult 相对行间距,相对字体巨细,1.5f表明行间距为1.5倍的字体高度。
float spacingadd 在根底行距上增加多少
boolean includepad,
TextUtils.TruncateAt ellipsize 从什么方位开端省掉
int ellipsizedWidth 超越多少开端省掉

弥补:TextView其实也是运用 StaticLayout

15.有用过ConstraintLayout吗?它有哪些特色?

参考答案:

Android中高级进阶开发面试题冲刺合集(三)

16.关于LayoutInflater,它是怎样经过 inflate 办法获取到详细View的?

参考答案:

体系经过LayoutInflater.from创立出布局结构器,inflate办法中,终究会掉用createViewFromTag 这里他会去判别 两个参数 factory2 和factory 如果都会空就会体系自己去创立view, 并且经过一个xml解析器,获取标签名字,然后判别是<Button仍是xxx.xxx.xxView. 然后走createView 经过拼接得到全类名路径,反射创立出类。

17.谈一谈怎样完结 Fragment 懒加载?

参考答案:

原本Fragment的 onResume()表明的是当时Fragment处于可见且可交互状况,但由于ViewPager的缓存机制,它现已失去了含义,也便是说咱们仅仅翻开了“福利”这个Fragment,但其实“歇息视频”和“拓宽资源”这两个Fragment的数据也都现已加载好了。

Fragment里setUserVisibleHint办法 在这里判别 是否可见 是否第一次加载

还有一种办法 在viewpager2 中设置 setMaxLifeCycler(START) 使得预加载后的fragment 最多生命周期走到start 就能够在onresume中去做网络恳求

18.谈谈 RecyclerView的缓存机制?

参考答案:

RecyclerView的缓存机制有四层 1,mChangedScrap和mAttachedScrap 用来缓存屏幕内的ViewHolder 2,mCachedViews(size=2先进先出) 用来缓存移除屏幕外的ViewHolder 默许size=2 如果再增加时size>2 取出第0个存入mRecyclerPool 然后移除mCachedViews第0个 再增加新的ViewHolder 3,mViewCacheExtension 自界说缓存 供给开发者 自界说缓存 4,mRecyclerPool(size=5) recyclerPool 缓存池 以type分块每块size=5 mCachedViews空间满时增加到mRecyclerPool缓存 mCachedViews找不到ViewHoler时去mRecyclerPool获取

19.请说说 View.inflate 和 LayoutInflater.inflate 的差异?

参考答案:

  1. 实践上没有差异,View.inflate实践上是对LayoutInflater.inflate做了一层包装,在功用上,LayoutInflate功用愈加强壮。
  2. View.inflate实践上终究调用的仍是LayoutInflater.inflate(@LayoutRes int resource, @nullable ViewGroup root)三个参数的办法,这里如果传入的root如果不为空,那么解析出来的View会被增加到这个ViewGroup当中去。
  3. 而LayoutInflater.inflate办法则能够指定当时View是否需求增加到ViewGroup中去。

总结一下:

  • 如果root为null,attachToRoot将失去作用,设置任何值都没有含义。
  • 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。
  • 如果root不为null,attachToRoot设为false,则会将布局文件最外层的全部layout特点进行设置,当该view被增加到父view当中时,这些layout特点会主动生效。
  • 在不设置attachToRoot参数的状况下,如果root不为null,attachToRoot参数默许为true。

不论调用的几个参数的办法,终究都会调用如下办法:

   /**
   * Inflate a new view hierarchy from the specified XML node. Throws
   * {@link InflateException} if there is an error.
   * <p>
   * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
   * reasons, view inflation relies heavily on pre-processing of XML files
   * that is done at build time. Therefore, it is not currently possible to
   * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
   *
   * @param parser    XML dom node containing the description of the view
   *           hierarchy.
   * @param root     Optional view to be the parent of the generated hierarchy (if
   *           <em>attachToRoot</em> is true), or else simply an object that
   *           provides a set of LayoutParams values for root of the returned
   *           hierarchy (if <em>attachToRoot</em> is false.)
   * @param attachToRoot Whether the inflated hierarchy should be attached to
   *           the root parameter? If false, root is only used to create the
   *           correct subclass of LayoutParams for the root view in the XML.
   * @return The root View of the inflated hierarchy. If root was supplied and
   * attachToRoot is true, this is root; otherwise it is the root of
   * the inflated XML file.
   */
   public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
     synchronized (mConstructorArgs) {
       Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
​
       final Context inflaterContext = mContext;
       final AttributeSet attrs = Xml.asAttributeSet(parser);
       Context lastContext = (Context) mConstructorArgs[0];
       mConstructorArgs[0] = inflaterContext;
       //终究回来的View
       View result = root;
​
       try {
         // Look for the root node.
         int type;
         while ((type = parser.next()) != XmlPullParser.START_TAG &&
           type != XmlPullParser.END_DOCUMENT) {
           // Empty
         }
​
         if (type != XmlPullParser.START_TAG) {
           throw new InflateException(parser.getPositionDescription()
             + ": No start tag found!");
         }
​
         final String name = parser.getName();
​
         if (DEBUG) {
           System.out.println("**************************");
           System.out.println("Creating root view: "
             + name);
           System.out.println("**************************");
         }
​
         if (TAG_MERGE.equals(name)) {
           if (root == null || !attachToRoot) {
             throw new InflateException("<merge /> can be used only with a valid "
               + "ViewGroup root and attachToRoot=true");
           }
​
           rInflate(parser, root, inflaterContext, attrs, false);
         } else {
           // Temp is the root view that was found in the xml
           final View temp = createViewFromTag(root, name, inflaterContext, attrs);
​
           ViewGroup.LayoutParams params = null;
​
           //root不为空,并且attachToRoot为false时则给当时View设置LayoutParams
           if (root != null) {
             if (DEBUG) {
               System.out.println("Creating params from root: " +
                 root);
             }
             // Create layout params that match root, if supplied
             params = root.generateLayoutParams(attrs);
             if (!attachToRoot) {
               // Set the layout params for temp if we are not
               // attaching. (If we are, we use addView, below)
               temp.setLayoutParams(params);
             }
           }
​
           if (DEBUG) {
             System.out.println("-----> start inflating children");
           }
​
           // Inflate all children under temp against its context.
           rInflateChildren(parser, temp, attrs, true);
​
           if (DEBUG) {
             System.out.println("-----> done inflating children");
           }
​
           // We are supposed to attach all the views we found (int temp)
           // to root. Do that now.
           //如果root不为空,并且attachToRoot为ture,那么将解析出来当View增加到当时到root当中,终究回来root
           if (root != null && attachToRoot) {
             root.addView(temp, params);
           }
​
           // Decide whether to return the root that was passed in or the
           // top view found in xml.
           //如果root等于空,那么将解析完的布局赋值给result终究回来,大部分用的都是这个。
           if (root == null || !attachToRoot) {
             result = temp;
           }
         }
       } catch (XmlPullParserException e) {
         final InflateException ie = new InflateException(e.getMessage(), e);
         ie.setStackTrace(EMPTY_STACK_TRACE);
         throw ie;
       } catch (Exception e) {
         final InflateException ie = new InflateException(parser.getPositionDescription()
           + ": " + e.getMessage(), e);
         ie.setStackTrace(EMPTY_STACK_TRACE);
         throw ie;
       } finally {
         // Don't retain static reference on context.
         mConstructorArgs[0] = lastContext;
         mConstructorArgs[1] = null;
​
         Trace.traceEnd(Trace.TRACE_TAG_VIEW);
       }
​
       return result;
     }
   }

20.请谈谈 invalidate() 和 postInvalidate() 办法的差异和运用场景?

参考答案:

  1. invalidate()用来重绘UI,需求在UI线程调用。
  2. postInvalidate()也是用来从头制作UI,它能够在UI线程调用,也能够在子线程中调用,postInvalidate()办法内部经过Handler发送了一个音讯将线程切回到UI线程告诉从头制作,并不是说postInvalidate()能够在子线程更新UI,本质上仍是在UI线程产生重绘,只不过咱们运用postInvalidate()它内部会帮咱们切换线程
 /**
   * <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
   * Use this to invalidate the View from a non-UI thread.</p>
   *
   * <p>This method can be invoked from outside of the UI thread
   * only when this View is attached to a window.</p>
   *
   * @see #invalidate()
   * @see #postInvalidateDelayed(long)
   */
   public void postInvalidate() {
     postInvalidateDelayed(0);
   }
  /**
   * <p>Cause an invalidate to happen on a subsequent cycle through the event
   * loop. Waits for the specified amount of time.</p>
   *
   * <p>This method can be invoked from outside of the UI thread
   * only when this View is attached to a window.</p>
   *
   * @param delayMilliseconds the duration in milliseconds to delay the
   *     invalidation by
   *
   * @see #invalidate()
   * @see #postInvalidate()
   */
   public void postInvalidateDelayed(long delayMilliseconds) {
     // We try only with the AttachInfo because there's no point in invalidating
     // if we are not attached to our window
     final AttachInfo attachInfo = mAttachInfo;
     if (attachInfo != null) {
       attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
     }
   }
  public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
     Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
     mHandler.sendMessageDelayed(msg, delayMilliseconds);
   }

21.谈一谈 SurfaceView 与 TextureView 的运用场景和用法?

参考答案:

一、SurfaceView SurfaceView 是一个能够在子线程中更新 UI 的 View,且不会影响到主线程。它为自己创立了一个窗口(window),就如同在视图层次(View Hierarchy)上穿了个“洞”,让绘图层(Surface)直接显现出来。可是,和惯例视图(view)不同,它没有动画或许变形特效,一些 View 的特性也无法运用。

归纳:

SurfaceView 独立于视图层次(View Hierarchy),具有自己的绘图层(Surface),但也没有一些惯例视图(View)的特性,如动画等。 SurfaceView 的完结中具有两个绘图层(Surface),即咱们所说的双缓冲机制。咱们的制作产生在后台画布上,并经过交换前后台画布来改写画面,可防止局部改写带来的闪耀,也提高了烘托功率。 SurfaceView 中的 SurfaceHolder 是 Surface 的持有者和办理操控者。 SurfaceHolder.Callback 的各个回调产生在主线程。 二、GLSurfaceView GLSurfaceView 承继 SurfaceView,除了具有 SurfaceView 全部特性外,还参加了 EGL(EGL 是 OpenGL ES 和原生窗口体系之间的桥梁) 的办理,并自带了一个独自的烘托线程。

归纳: 承继自 SurfaceView,具有其全部特性。 参加了 EGL 办理,是 SurfaceView 运用 OpenGL ES 的典型场景。 有独自的烘托线程 GLThread。 独自出了 Renderer 接口担任实践烘托,不同的 Renderer 完结相当于不同的烘托策略,运用办法灵活(策略办法)。 三、SurfaceTexture Android 3.0(API 11)新参加的一个类,不同于 SurfaceView 会将图画显现在屏幕上,SurfaceTexture 对图画流的处理并不直接显现,而是转为 GL 外部纹路。 归纳: SurfaceTexture 能够从图画流(相机、视频)中捕获帧数据用作 OpenGL ES 外部纹路(GL_TEXTURE_EXTERNAL_OES),完结无缝衔接。 咱们能够很便利的对这个外部纹路进行二次处理(如增加滤镜等)。 输出方针是 Camera 或 MediaPlayer 时,能够用 SurfaceTexture 代替 SurfaceHolder,这样图画流将把全部帧传给 SurfaceTexture 而不是显现在设备上。 运用 updateTexImage() 办法更新最新的图画。 四、TextureView TextureView 是 Android 4.0(API 14)引入,它有必要运用在开启了硬件加速的窗体中。除了具有 SurfaceView 的特性外,它还能够进行像惯例视图(View)那样进行平移、缩放等动画。

22.谈一谈 RecyclerView.Adapter 的几种数据改写办法有何不同?

参考答案:

1、notifyDataSetChanged()

  • 可见item前面5个的onCreateViewHolder不会履行,也便是说从第6个开端运用缓存中的ViewHolder
  • 可见item的onBindViewHolder都会履行

2、notifyItemChanged(int position)

条件:一屏幕只显现20条数据,调用notifyItemChanged(0)

  • position为21的onCreateViewHolder优先履行
  • 接着position为0的onCreateViewHolder开端履行

3、notifyItemChanged(int position, @nullable Object payload) 4、notifyItemRangeChanged(int positionStart, int itemCount) 条件:一屏幕只显现20条数据,调用notifyItemRangeChanged(0,5)

  • position为21,22,23,24,25的onCreateViewHolder依序履行
  • 接着position为0,1,2,3,4的onCreateViewHolder依序履行
  • 从数据调集中拿数据时,或许会产生数据越界反常

5、notifyItemRangeChanged(int positionStart, int itemCount, @nullable Object payload) 6、notifyItemInserted(int position)

条件:一屏幕只显现20条数据,调用notifyItemInserted(1)

  • 在position为1的这个当地,多了一个ItemView,数据是mDatas.get(1)中的数据

总结:不论数据调集是否改动,调用此办法时,都在在指定的方位刺进ItemView,从数据调集中的position这个当地拿数据填充

7、notifyItemMoved(int fromPosition, int toPosition)

条件:一屏幕只显现20条数据,调用notifyItemMoved(3,10)

  • 把方位为3的视图移动到方位为10这个当地,4~10这些方位的item都往前移动一个方位

8、notifyItemRangeInserted(int positionStart, int itemCount)

条件:一屏幕只显现20条数据,调用notifyItemRangeInserted(3,10)

  • 在方位为3处开端刺进10个视图,数据也是从3开端
  • 从数据调集中拿数据时,或许会产生数据越界反常
  • 会产生数组越界反常

9、notifyItemRemoved(int position)

条件:数据调集有50条数据,调用notifyItemRemoved(51)

  • 不会产生数组反常
  • 如果数据调集中未移除第0条数据,就掉调用了notifyItemRemoved(0),此刻会移除视图,可是上下翻滚时,仍是会出现

10、notifyItemRangeRemoved(int positionStart, int itemCount) 条件:数据调集有50条数据,调用notifyItemRangeRemoved(0,100)

  • 不会产生数组反常

payload此参数不知道有何用

23.说说你对 Window 和 WindowManager 的了解?

参考答案:

Window 和 WindowManager 是什么联络? Widow 是个笼统类,在 Android 中全部的视图都是经过 Window 来出现的,包含 Activity、Dialog、Toast,它们的视图实践上都是附加在 Window 上的。Window 的详细完结类是 PhoneWindow。而 WindowManager 是外界拜访 Window 的入口,WindowManager 和 WindowManagerService 之间经过 IPC 进行通讯,然后完结对 Window 的拜访和操作。 Window 和 View 是什么联络? Window 是 View 的承载者,而 View 是 Window 的表现者。两者之间经过 ViewRootImpl 树立联络。 怎样了解这句话呢? Window 是 View 的承载者:Android 中的全部视图都是附加在 Window 上出现出来的 。 View 是 Window 的表现者:由于 Window 是个笼统的概念,并不实践存在,View 才是 Window 存在的实体。 而 ViewRootImpl 是用来树立 Window 和 View 之间的联络的,是两者之间的纽带。 WindowManager 和 View 是什么联络? WindowManager 是 View 的直接办理者,对 View 的增加、删除、更新操作都是经过 WindowManager 来完结的,对应于 WindowManager 的 addView、removeView、updateViewLayout 三个办法。

24.谈一谈 Activity、View 和 Window 三者的联络?

参考答案:

首要先说下 Window:

Window 表明窗口,是个笼统的概念,每个 Window 都对应着一个 View 和一个 ViewRootImpl,Window 经过 ViewRootImpl 和 View 树立联络,因而 Window 并不是实践存在的,他是以 View 的办法存在的。也便是说 View 才是 Window 存在的实体。实践运用中无法直接拜访 Window,对 Window 的拜访有必要经过 WindowManager。

再说 View:

View 是 Android 中视图的出现办法,可是 View 不能独自存在,它有必要附着在 Window 这个笼统的概念上,因而有视图的当地就有 Window。

终究 Activity:

Android 中的四大组件之一 Activity 便是用来显现视图的,因而一个 Activity 就对应着一个 Window,不止 Activity,其实 Dialog,Toast,PopUpWindow,菜单等都对应着一个 Window。

Activity,View,Window 相关连的详细完结:

在 Activity 发动时 经过 attach 办法,创立 Window 的实例即 PhoneWindow 然后绑定 Activity,Activity 完结了 Window 的 Callback 等接口,当 Window 接纳到外界的状况改动时就会回调给 Activity,比方 onAttachToWindow、onDetacheFromWindow、dispatchTouchEvent 等。 PhoneWindow 初始化时会创立 DecorView (也便是尖端 View),然后初始化 DecorView 的结构,加载布局文件到 DecorView,之后再将 Activity 的视图增加到 DecorView 的 mContentParent 中。

总结:

Activity 经过 Window 完结对视图 View 的办理,一个 Activity 对应一个 Window,每个 Window 又对应一个 View。

25.有了解过WindowInsets吗?它有哪些运用场景?

参考答案:ViewRootImpl在performTraversals时会调dispatchApplyInsets,内调DecorView的dispatchApplyWindowInsets,进行WindowInsets的分发。

26.Android 中 View 的几种位移办法的差异?

参考答案:

  • setTranslationX/Y
  • scrollBy/scrollTo
  • offsetTopAndBottom/offsetLeftAndRight
  • 动画
  • margin
  • layout

这些位移的差异

27.为什么 ViewPager 嵌套 ViewPager,内部的 ViewPager 翻滚没有被阻拦?

参考答案:

被外部的ViewPager阻拦了,需求做滑动抵触处理。重写子View的 dispatchTouchEvent办法,在子View需求阻拦的时分进行阻拦,不然交给父View处理。

Android中高级进阶开发面试题冲刺合集(三)

28.请谈谈 Fragment 的生命周期?

参考答案:

1,onAttach:fragment 和 activity 相关时调用,且调用一次。在回调中能够将参数 content 转换为 Activity保存下来,防止后期频频获取 activity。

2,onCreate:和 activity 的 onCreate 相似

3,onCreateView:预备制作 fragment 界面时调用,回来值为根视图,留意运用 inflater 构建 View时 必定要将 attachToRoot 指明为 false。

4,onActivityCreated:activity 的onCreated 履行完时调用

5,onStart:可见时调用,条件是 activity 现已 started

6,onResume:交互式调用,条件是 activity 现已 resumed

7,onPause:不行交互时调用

8,onStop:不行见时调用

9,onDestroyView:移除 fragment 相关视图时调用

10,onDestroy:铲除 fragmetn 状况是调用

11,onDetach:和 activity 免除相关时调用

从生命周期能够看出,他们是两两对应的,如 onAttach 和 onDetach ,

onCreate 和 onDestory ,onCreateView 和 onDestroyView等

ragment 在 ViewPager中的生命周期

ViewPager 有一个预加载机制,他会默许加载旁边的页面,也便是说在显现第一个页面的时分 旁边的页面现已加载完结了。这个能够设置,但不能为0,可是有些需求则不需求这个作用,这时分就能够运用懒加载了:懒加载的完结

1,当 ViewPager 能够左右滑动时,他左右两头的 fragment 现已加载完结,这便是预加载机制

2,当 fragment 不处于 ViewPager 左右两头时,就会履行 onPause,onStop,OnDestroyView 办法。

fragment 之间传递数据办法

1,运用 bundle,有些数据需求被序列化

2,接口回调

3,在创立的时分经过结构直接传入

4,运用 EventBus 等

单 Activity 多 fragment 的长处,fragment 的优缺陷

fragment 比 activity 占用更少的资源,特别在中低端手机,fragment 的响应速度十分快,如丝般的顺滑,更简略操控每个场景的生命周期和状况

优缺陷:十分流通,节省资源,灵活性高,fragment 有必要赖于acctivity,并且 fragment 的生命周期直接受地点的 activity 影响。

29.请谈谈什么是同步屏障?

参考答案:

handler.getLooper().getQueue().postSyncBarrier()参加同步屏障后,Message.obtain()获取一个target为null的msg,并依据当时时刻将该msg刺进到链表中。 在Looper.loop()循环取音讯中 Message msg = queue.next(); target为空时,取链表中的异步音讯。 经过setAsynchronous(true)来指定为异步音讯

运用场景:ViewRootImpl scheduleTraversals中参加同步屏障 并在view的制作流程中post异步音讯,确保view的制作音讯优先履行

30.有了解过 ViewDragHelper 的作业原理吗?

参考答案:

ViewDragHelper类,是用来处理View鸿沟拖动相关的类,比方咱们这里要用的比方—侧滑拖动封闭页面(相似微信),该功用很显着是要处理在View上的接触事情,记载接触点、核算间隔、翻翻滚画、状况回调等,如果咱们自己手动完结自然会很费事还或许出错,而这个类会协助咱们大大简化作业量。 该类是在Support包中供给,所以不会有体系适配问题,下面咱们就来看看他的原理和运用吧。

1.初始化

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
     ...
     mParentView = forParent;//BaseView
     mCallback = cb;//callback
     final ViewConfiguration vc = ViewConfiguration.get(context);
     final float density = context.getResources().getDisplayMetrics().density;
     mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);//鸿沟拖动间隔规模
     mTouchSlop = vc.getScaledTouchSlop();//拖动间隔阈值
     mScroller = new OverScroller(context, sInterpolator);//翻滚器
   }
  • mParentView是指根据哪个View进行接触处理
  • mCallback是接触处理的各个阶段的回调
  • mEdgeSize是指在鸿沟多少间隔内算作拖动,默许为20dp
  • mTouchSlop指滑动多少间隔算作拖动,用的体系默许值
  • mScroller是View翻滚的Scroller方针,用于处理释接触放后,View的翻滚行为,比方翻滚回原始方位或许翻滚出屏幕

2.阻拦事情处理 该类供给了boolean shouldInterceptTouchEvent(MotionEvent)办法,一般咱们需求这么写:

override fun onInterceptTouchEvent(ev: MotionEvent?) =
       dragHelper?.shouldInterceptTouchEvent(ev) ?: super.onInterceptTouchEvent(ev)

该办法用于处理mParentView是否阻拦此次事情

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
     ...
     switch (action) {
       ...
       case MotionEvent.ACTION_MOVE: {
         if (mInitialMotionX == null || mInitialMotionY == null) break;
         // First to cross a touch slop over a draggable view wins. Also report edge drags.
         final int pointerCount = ev.getPointerCount();
         for (int i = 0; i < pointerCount; i++) {
           final int pointerId = ev.getPointerId(i);
           // If pointer is invalid then skip the ACTION_MOVE.
           if (!isValidPointerForActionMove(pointerId)) continue;
           final float x = ev.getX(i);
           final float y = ev.getY(i);
           final float dx = x - mInitialMotionX[pointerId];
           final float dy = y - mInitialMotionY[pointerId];
           final View toCapture = findTopChildUnder((int) x, (int) y);
           final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
           ...
           //判别pointer的拖动鸿沟
           reportNewEdgeDrags(dx, dy, pointerId);
           ...
         }
         saveLastMotion(ev);
         break;
       }
       ...
     }
     return mDragState == STATE_DRAGGING;
}

阻拦事情的条件是mDragState为STATE_DRAGGING,也便是正在拖动状况下才会阻拦,那么什么时分会变为拖动状况呢?当ACTION_MOVE时,调用reportNewEdgeDrags办法:

private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
     int dragsStarted = 0;
            //判别是否在Left边际进行滑动
     if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
       dragsStarted |= EDGE_LEFT;
     }
     if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
       dragsStarted |= EDGE_TOP;
     }
     ...
     if (dragsStarted != 0) {
       mEdgeDragsInProgress[pointerId] |= dragsStarted;
        //回调拖动的边
       mCallback.onEdgeDragStarted(dragsStarted, pointerId);
     }
}
​
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
     final float absDelta = Math.abs(delta);
     final float absODelta = Math.abs(odelta);
                //是否支撑edge的拖动以及是否满意拖动间隔的阈值
     if ((mInitialEdgesTouched[pointerId] & edge) != edge  || (mTrackingEdges & edge) == 0
         || (mEdgeDragsLocked[pointerId] & edge) == edge
         || (mEdgeDragsInProgress[pointerId] & edge) == edge
         || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
       return false;
     }
     if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
       mEdgeDragsLocked[pointerId] |= edge;
       return false;
     }
     return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}

能够看到,当ACTION_MOVE时,会尝试找到pointer对应的拖动鸿沟,这个鸿沟能够由咱们来制定,比方侧滑封闭页面是从左边开端的,所以咱们能够调用setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)来设置只支撑左边滑动。而一旦有翻滚产生,就会回调callback的onEdgeDragStarted办法,交由咱们做如下操作:

override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {
         super.onEdgeDragStarted(edgeFlags, pointerId)
         dragHelper?.captureChildView(getChildAt(0), pointerId)
       }

咱们调用了ViewDragHelper的captureChildView办法:

public void captureChildView(View childView, int activePointerId) {
     mCapturedView = childView;//记载拖动view
     mActivePointerId = activePointerId;
     mCallback.onViewCaptured(childView, activePointerId);
     setDragState(STATE_DRAGGING);//设置状况为开端拖动
}

此刻,咱们就记载了拖动的View,并将状况置为拖动,那么在下次ACTION_MOVE的时分,该mParentView就会阻拦事情,交由自己的onTouchEvent办法处理拖动了!

3.拖动事情处理 该类供给了void processTouchEvent(MotionEvent)办法,一般咱们需求这么写:

override fun onTouchEvent(event: MotionEvent?): Boolean {
     dragHelper?.processTouchEvent(event)//交由ViewDragHelper处理
     return true
}

该办法用于处理mParentView阻拦事情后的拖动处理:

public void processTouchEvent(MotionEvent ev) {
     ...
     switch (action) {
       ...
       case MotionEvent.ACTION_MOVE: {
         if (mDragState == STATE_DRAGGING) {
           // If pointer is invalid then skip the ACTION_MOVE.
           if (!isValidPointerForActionMove(mActivePointerId)) break;
           final int index = ev.findPointerIndex(mActivePointerId);
           final float x = ev.getX(index);
           final float y = ev.getY(index);
           //核算间隔前次的拖动间隔
           final int idx = (int) (x - mLastMotionX[mActivePointerId]);
           final int idy = (int) (y - mLastMotionY[mActivePointerId]);
           dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//处理拖动
           saveLastMotion(ev);//记载当时接触点
         }...
         break;
       }
       ...
       case MotionEvent.ACTION_UP: {
         if (mDragState == STATE_DRAGGING) {
           releaseViewForPointerUp();//开释拖动view
         }
         cancel();
         break;
       }...
     }
}

(1)拖动 ACTION_MOVE时,会核算出pointer间隔前次的位移,然后核算出capturedView的方针方位,进行拖动处理

private void dragTo(int left, int top, int dx, int dy) {
     int clampedX = left;
     int clampedY = top;
     final int oldLeft = mCapturedView.getLeft();
     final int oldTop = mCapturedView.getTop();
     if (dx != 0) {
       clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);//经过callback获取真实的移动值
       ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);//进行位移
     }
     if (dy != 0) {
       clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
       ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
     }
​
     if (dx != 0 || dy != 0) {
       final int clampedDx = clampedX - oldLeft;
       final int clampedDy = clampedY - oldTop;
       mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
           clampedDx, clampedDy);//callback回调移动后的方位
     }
}

经过callback的clampViewPositionHorizontal办法决议实践移动的水平间隔,一般都是回来left值,即拖动了多少就移动多少

经过callback的onViewPositionChanged办法,能够对View拖动后的新方位做一些处理,如:

override fun onViewPositionChanged(changedView: View?, left: Int, top: Int, dx: Int, dy: Int) {
  super.onViewPositionChanged(changedView, left, top, dx, dy)
   //当新的left方位抵达width时,即滑动除了界面,封闭页面
   if (left >= width && context is Activity && !context.isFinishing) {
    context.finish()
   }
}

(2)开释 而ACTION_UP动作时,要开释拖动View

private void releaseViewForPointerUp() {
     ...
     dispatchViewReleased(xvel, yvel);
}
​
private void dispatchViewReleased(float xvel, float yvel) {
     mReleaseInProgress = true;
     mCallback.onViewReleased(mCapturedView, xvel, yvel);//callback回调开释
     mReleaseInProgress = false;
     if (mDragState == STATE_DRAGGING) {
       // onViewReleased didn't call a method that would have changed this. Go idle.
       setDragState(STATE_IDLE);//重置状况
     }
}

一般在callback的onViewReleased办法中,咱们能够判别当时开释点的方位,然后决议是要回弹页面仍是滑出屏幕:

override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
  super.onViewReleased(releasedChild, xvel, yvel)
   //滑动速度抵达必定值时直接封闭
   if (xvel >= 300) {//滑动页面到屏幕外,封闭页面
    dragHelper?.settleCapturedViewAt(width, 0)
   } else {//回弹页面
    dragHelper?.settleCapturedViewAt(0, 0)
   }
  //改写,开端封闭或重置动画
  invalidate()
}

如滑动速度大于300时,咱们调用settleCapturedViewAt办法将页面翻滚出屏幕,不然调用该办法进行回弹

(3)翻滚 ViewDragHelper的settleCapturedViewAt(left,top)办法,用于将capturedView翻滚到left,top的方位

public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
  return forceSettleCapturedViewAt(finalLeft, finalTop,
                  (int) mVelocityTracker.getXVelocity(mActivePointerId),
                  (int) mVelocityTracker.getYVelocity(mActivePointerId));
}
​
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
  //当时方位
  final int startLeft = mCapturedView.getLeft();
  final int startTop = mCapturedView.getTop();
  //偏移量
  final int dx = finalLeft - startLeft;
  final int dy = finalTop - startTop;
  ...
  final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
  //运用Scroller方针开端翻滚
  mScroller.startScroll(startLeft, startTop, dx, dy, duration);
    //重置状况为翻滚
  setDragState(STATE_SETTLING);
  return true;
}

其内部运用的是Scroller方针:是View的翻滚机制,其回调是View的computeScroll()办法,在其内部经过Scroller方针的computeScrollOffset办法判别是否翻滚结束,如仍需翻滚,需求调用invalidate办法进行改写

ViewDragHelper据此供给了一个相似的办法continueSettling,需求在computeScroll中调用,判别是否需求invalidate

public boolean continueSettling(boolean deferCallbacks) {
  if (mDragState == STATE_SETTLING) {
   //是否翻滚结束
   boolean keepGoing = mScroller.computeScrollOffset();
   //当时翻滚值
   final int x = mScroller.getCurrX();
   final int y = mScroller.getCurrY();
   //偏移量
   final int dx = x - mCapturedView.getLeft();
   final int dy = y - mCapturedView.getTop();
        //廉价操作
   if (dx != 0) {
    ViewCompat.offsetLeftAndRight(mCapturedView, dx);
   }
   if (dy != 0) {
    ViewCompat.offsetTopAndBottom(mCapturedView, dy);
   }
        //回调
   if (dx != 0 || dy != 0) {
    mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
   }
   //翻滚结束状况
   if (!keepGoing) {
    if (deferCallbacks) {
     mParentView.post(mSetIdleRunnable);
    } else {
     setDragState(STATE_IDLE);
    }
   }
  }
  return mDragState == STATE_SETTLING;
}

在咱们的View中:

override fun computeScroll() {
  super.computeScroll()
   if (dragHelper?.continueSettling(true) == true) {
    invalidate()
   }
}