发布于 

Android面试题整理1(开发)

1.1 Activity 与 Fragment 之间常见的几种通信方式?

  1. 使用Intent:可以使用Intent来在Activity和Fragment之间传递数据,Intent还可以用来启动新的Activity或Fragment。

  2. 使用Bundle:Bundle是一种将数据封装成key-value形式的存储方式,可以在Activity和Fragment之间进行数据传递。

  3. 使用接口回调:可以定义一个接口,在Activity中实现接口,在Fragment中调用接口,从而实现Activity和Fragment之间的通信。

  4. 使用观察者模式:可以使用观察者模式,Fragment监听来自Activity的变化,从而实现Activity和Fragment之间的通信。

  5. 使用ViewModel:可以使用ViewModel来管理数据,从而实现Activity和Fragment之间的通信。

1.2 LaunchMode 的应用场景?

LaunchMode 有四种,分别为Standard,SingleTop,SingleTask 和 SingleInstance,每种模式的实现原理一楼都做了较详细说明,下面说一下具体使用场景:

  • Standard:
    Standard模式是系统默认的启动模式,一般我们 app 中大部分页面都是由该模式的页面构成的,比较常见的场景是:社交应用中,点击查看用户A信息->查看用户A粉丝->在粉丝中挑选查看用户B信息->查看用户A粉丝… 这种情况下一般我们需要保留用户操作 Activity栈的页面所有执行顺序。
  • SingleTop:
    SingleTop 模式一般常见于社交应用中的通知栏行为功能,例如:App 用户收到几条好友请求的推送消息,需要用户点击推送通知进入到请求者个人信息页,将信息页设置为 SingleTop 模式就可以增强复用性。SingleTask:
  • SingleTask 模式一般用作应用的首页,例如浏览器主页,用户可能从多个应用启动浏览器,但主界面仅仅启
    动一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
  • SingleInstance:
    SingleInstance 模式常应用于独立栈操作的应用,如闹钟的提醒页面,当你在A应用中看视频时,闹钟响了, 你点击闹钟提醒通知后进入提醒详情页面,然后点击返回就再次回到A的视频页面,这样就不会过多干扰到用户先前的操作了。

1.3 BroadcastReceiver 与LocalBroadcastReceiver 有什么区别?

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

1.4 对于 Context,你了解多少?

Context也叫上下文,是有关应用程序环境的全局信息的接口。这是一个抽象类, 它允许访问特定于应用程序的资源和类,以及对应用程序级操作的调用,比如启动活动,发送广播和接收意图等;
Activity, Service, Application都是 Context的子类。
Context 的具体实现类是 ContextImpl, 还有一个包装类ContextWrapper, ContextWrapper 的 子 类 有 Service ,Application,ContextThemeWrapper, Activity 又是
ContextThemeWrapper 的子类,ContextThemeWrapper 也可以叫 UI Context,跟UI 操作相关的最好使用此类 Context。

ContextWrapper 中有个 mBase,这个 mBase 其实是ContextImpl,它是在Activity, Service, Application 创建时通过attachBaseContext()方法将各自对对应ContextImpl 赋值的。对 context 的操作,最终实现都是在ContextImpl。

对于 startActivity操作

  • 当为Activity Context则可直接使用;
  • 当为其他Context, 则必须带上FLAG_ACTIVITY_NEW_TASK flags才能使用;因为非 Activitycontext启动 Activity没有 Activity栈,则无法启动,因此需要加开启新的栈;
  • 另外UI相关要Activity中使用.

getApplication()和getApplicationContext()区别?

  1. 对于Activity/Service来说,getApplication()和getApplicationContext()的返回值完全相同; 除非厂商修改过接口;
  2. BroadcastReceiver在onReceive的过程, 能使用getBaseContext().getApplicationContext获取所在Application, 而无法使用getApplication;
  3. ContentProvider能使用getContext().getApplicationContext()获取所在Application. 绝大多数情况下没有问题, 但是有可能会出现空指针的问题, 情况如下:
    当同一个进程有多个apk的情况下, 对于第二个apk是由provider方式拉起的, 前面介绍过provider创建过程并不会初始化所在application, 此时执行getContext().getApplicationContext()返回的结果便是NULL. 所以对于这种情况要做好判空.

1.5 IntentFilter是什么?有哪些使用场景?

IntentService是什么

IntentService是Service的子类,继承与Service类,用于处理需要异步请求。用户通过调用Context.StartService(Intent)发送请求,服务根据需要启 动,使用工作线程依次处理每个Intent,并在处理完所有工 作后自身停止服务。

使用时,扩展IntentService并实现onHandleIntent(android.content.Intent)。IntentService接收Intent,启动工作线程,并在适当时机停止服务。所有的请求都在同一个工作线程上处理,一次处理一个请求,所以处理完所以的请求可能会花费很长的时间,但由于IntentService是另外了线程来工作,所以保证不会阻止App的主线程。

IntentService与Service的区别
从何时使用,触发方法,运行环境,何时停止四个方面分析。

何时使用
Service用于没有UI工作的任务,但不能执行长任务(长时间的任务),如果需要Service来执行长时间的任务,则必须手动开店一个线程来执行该Service。IntentService可用于执行不与主线程沟通的长任务。

触发方法
Service通过调用 startService() 方法来触发。而IntentService通过Intent来触发,开启一个新的工作线 程,并在线程上调用 onHandleIntent() 方法。

运行环境
Service 在App主线程上运行,没有与用户交互,即在后台运行,如果执行长时间的请求任务会阻止主线程工作。IntentService在自己单独开启的工作线程上运行,即使执行长时间的请求任务也不会阻止主线程工作。

何时停止
如果执行了Service,我们是有责任在其请求任务完成后关 闭服务,通过调用 stopSelf() 或 stopService()来结束服务。IntentService会在执行完所有的请求任务后自行关闭服务,所以我们不必额外调用 stopSelf() 去关闭它。

1.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来说,不管是本地服务还是远程服务,我们需要做的工作都一样简单。

1.7 Service如何进行保活?

利用系统广播拉活
利用系统service拉活
利用Native进程拉活 fork进行监控主进程,利用native拉活
利用JobScheduler机制拉活 利用账号同步机制拉活

1.8简单介绍下ContentProvider是如何实现数据共享的?

ContentProvider(内容提供者):对外提供了统一的访问数据的接口。
ContentResolver(内容解析者):通过URI的不同来操作不同的ContentProvider中的数据。
ContentObserver(内容观察者):观察特定URI引起的数据库的变化。通过ContentResolver进行注册,观察数据是否发生变化及时通知刷新页面(通过Handler通知主线程更 新UI)

1.9说下切换横竖屏时Activity的生命周期?

AndroidManifest没有设置configChanges属性竖屏启动:

onCreate -->onStart–>onResume

切换横屏:

onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart–>onRestoreInstanceState–>onResume -->onPause–>onStop -->onDestroy

(Android 6.0 Android 7.0 Android8.0) 横屏启动:
onCreate -->onStart–>onResume

切换竖屏

onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart–>onRestoreInstanceState–>onResume -->onPause–>onStop -->onDestroy

(Android 6.0 Android 7.0 Android 8.0)

总结:没有设置configChanges属性Android 6.0 7.0 8.0系统手机 表现都是一样的,当前的界面调用
onSaveInstanceState走一遍流程,然后重启调用onRestoreInstanceState再走一遍完整流程,最终destory。

2.AndroidManifest设置了configChanges

android:configChanges=“orientation” 竖屏启动:

onCreate -->onStart–>onResume

(Android 6.0)切换横屏:

onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart–>onRestoreInstanceState–>onResume -->onPause–>onStop -->onDestroy

(Android 7.0)onConfigurationChanged

onConfigurationChanged–>onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart -->onRestoreInstanceState–onResume -->onPause -->onStop -->onDestroy>

Android 8.0)
onConfigurationChanged

总结:设置了configChanges属性为orientation之后,Android6.0 同没有设置configChanges情况相同,完整的走完了两个生命周期,调用了onSaveInstanceState和onRestoreInstanceState
方法;

Android 7.0则会先回调onConfigurationChanged方法,剩下的流程跟Android6.0 保持一致Android 8.0 系统更是简单,
只是回调了onConfigurationChanged方法,并没有走Activity的生命周期方法。

3.AndroidManifest 设 置 了 configChangesandroid:configChanges=“orientation|keyboardHidden|screenSize”

竖(横)屏启动:onCreate–>onStart–>onResume

切换横(竖)屏:onConfigurationChanged
(Android 6.0 Android7.0 Android 8.0)

总结:设置android:configChanges=“orientation|keyboardHidden|screenSize” 则都不会调用Activity的其他生命周期方法, 只会调用onConfigurationChanged方法。

adroid:configChanges=“orientation|screenSize”

竖(横)屏启动:onCreate -->onStart–>onResume
切换横(竖)屏:onConfigurationChanged(Android6.0Android 7.0 Android 8.0)

总结:没有了keyboardHidden跟3是相同的,orientation 代表横竖屏切换 screenSize代表屏幕大小发生了改变,设置了这两项就不会回调Activity的生命周期的方法,只会回调onConfigurationChanged 。

5.AndroidManifest 设 置 了 configChanges android:configChanges=“orientation|keyboardHidden”

总结:跟只设置了orientation属性相同,Android6.0 Android7.0会回调生命周期的方法,Android8.0则只回调 onConfigurationChanged。说明如果设置了orientation和 screenSize都不会走生命周期的方法,keyboardHidden不影响。

1.不设置configChanges属性不会回调onConfigurationChanged,且切屏的时候会回调生命周期方
法。

2.只有设置了orientation 和 screenSize 才会保证都不会走生命周期,且切屏只回调onConfigurationChanged。设置orientation,没有设置screenSize,切屏会回调

3.onConfigurationChanged,但是还会走生命周期方法。

注:这里只选择了Android部分系统的手机做测试,由于不 同系统的手机品牌也不相同,可能略微会有区别。

另:
代码动态设置横竖屏状态(onConfigurationChanged当屏幕 发 生 变 化 的 时 候 回 调 )
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

获取屏幕状态(int ORIENTATION_PORTRAIT = 1; 竖屏 int ORIENTATION_LANDSCAPE = 2; 横屏) int screenNum = getResources().getConfiguration().orientation;

configChanges属性

  1. orientation 屏幕在纵向和横向间旋转
  2. keyboardHidden 键盘显示或隐藏
  3. screenSize 屏幕大小改变了
  4. fontScale 用户变更了首选的字体大小
  5. locale 用户选择了不同的语言设定
  6. keyboard 键盘类型变更,例如手机从12键盘切换到全键盘
  7. touchscreen或navigation 键盘或导航方式变化,一般不会发生这样的事件

常用的包括:orientation、keyboardHidden、screenSize,设置这三项界面不会走Activity的生命周期,只会回调onConfigurationChanged方法。

screenOrientation属性

  1. unspecified 默认值,由系统判断状态自动切换
  2. landscape 横屏
  3. portrait 竖屏
  4. user 用户当前设置的orientation值
  5. behind 下一个要显示的Activity的orientation值
  6. sensor 使用传感器 传感器的方向
  7. nosensor 不使用传感器 基本等同于unspecified仅landscape和portrait常用,代表界面默认是横屏或者竖屏,还可以再代码中更改。

1.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定义为singleTop,且上述intent包含
    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 afterthis method",而不具体说明调用时机的原因.

1.11 Intent传输数据的大小有限制吗?如何解决?

Intent 中的 Bundle 是使用 Binder 机制进行数据传送的,数据会写到内核空间, Binder 缓冲区域;Binder 的缓冲区是有大小限制的, 有些 ROM 是 1M, 有些ROM 是 2M;
这个限制定义在frameworks/native/libs/binder/processState.cpp 类 中 , 如果超过这个限制, 系统就会报错;

#
*
define BINDER_VM_SIZE ((1*1024*1024) - (4096
2)) ;

因为 Binder 本身就是为了进程间频繁-灵活的通信所设计的, 并不是为了拷贝大量数据;
如果非 ipc 就很简单了, static 变量, eventBus 之类的都可以;
如果是 ipc, 一定要一次性传大文件, 可以用 file 或者socket;

1.12 说说ContentProvider、ContentResolver、ContentObserver 之间的关系?

ContentProvider

  • 内容提供者, 用于对外提供数据,比如联系人应用中就是用了

ContentProvider,

  • 一个应用可以实现ContentProvider来提供给别的应用操作,通过ContentResolver来操作别的应用数据ContentResolver 内容解析者, 用于获取内容提供者提供的数ContentResolver.notifyChange(uri)发出消息

ContentObserver

  • 内容监听者,可以监听数据的改变状态
  • 观察(捕捉)特定的Uri引起的数据库的变化
  • ContentResolver.registerContentObserver()监听消息
    概括:
    使用ContentResolver来获取ContentProvider提供的数据, 同时注册ContentObserver监听数据的变化

1.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 异步任务和消息机制

2.1 HandlerThread 的使用场景和用法?

HandlerThread 本质上是一个在子线程的handler(HandlerThread=Handler+Thread);

它的使用:
步骤1:创建HandlerThread实例对象

HandlerThread mHandlerThread = new
HandlerThread("handlerThread");

步骤2:启动线程

mHandlerThread.start();

步骤3:创建工作线程Handler&复写handleMessage

Handler workHandler = new Handler( handlerThread.getLooper() ) {
@
OverRide
public boolean handleMessage(Message msg) {

    ...//消息处理
    return true;
    }
}

步骤4:使用工作线程Handler向工作线程的消息队列发送消息

Message msg = Message.obtain();
msg.what = 2; //消息的标识
msg.obj = "B"; // 消息的存放
// b. 通过Handler发送消息到其绑定的消息队列
workHandler.sendMessage(msg);

步骤5:结束线程,即停止线程的消息循环

mHandlerThread.quit();

优势:

  1. 将loop运行在子线程中处理,减轻了主线程的压力,使主线
    程更流畅
  2. 串行执行,开启一个线程起到多个线程的作用
  3. 有自己的消息队列,不会干扰UI线程

劣势:
1.由于每一个任务队列逐步执行,一旦队列耗时过长,消息延时
2.对于IO等操作,线程等待,不能并发

2.2 IntentService 的应用场景和使用姿势?

IntentService 是 Service 的子类,默认为我们开启了一个工作线程,使用这个工作线程逐一处理所有启动请求,在任务执行完毕后会自动停止服务,使用简单,只要实现一个方法 onHandleIntent,该方法会接收每个启动请求的 Intent,能够执行后台工作和耗时操作。可以启动多次,而每一个耗时操作会以队列的方式IntentServiceIntentService 的回调方法中执行,onHandlerIntent并且,每一次只会执行一个工作线程,执行完第一个再执行第二个。并且等待所有消息都执行完后才终止服务。

适用于 APP 在不影响当前用户的操作IntentService的前提下,在后台默默的做一些操作。
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();
            }
        }
    }
}

2.3 AsyncTask的优点和缺点?

AsyncTask 的 实 现 原 理 :

  1. AsyncTask是一个抽象类,主要由Handler+2个线程池构 成,SERIAL_EXECUTOR是任务队列线程池,用于调度任务,按顺序排列执行,THREAD_POOL_EXECUTOR是执行 线程池,真正执行具体的线程任务。Handler用于工作线程 和主线程的异步通信。
  2. AsyncTaskParams,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).

2.4 谈谈你对 Activity.runOnUiThread 的理解?

一般是用来将一个runnable绑定到主线程,在runOnUiThread源码里面会判断当前runnable是否是主线程,如果是直接run,如果不是,通过一个默认的空构造函数handler将runnable post 到looper里面,创建构造函数handler,会默认绑定一个主线程的looper对象

2.5 子线程能否更新UI?为什么?

子线程是不能直接更新UI的注意这句话,是不能直接更新,不是不能更新(极端情况下可更新)

绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制ui,极端情况就是,在Activity的onResume(含)之前的生命周期中子线程都可以进行更新ui,也就是onCreate,onStart和onResume,此时主线程的绘制还没开始。

2.6 谈谈 Handler 机制和原理?

Handler机制是Android中消息处理机制的核心,用于实现线程间的通信,保证UI线程的稳定运行。

Handler机制的原理:

  1. Android中的每一个线程都会创建一个Looper对象,用于接收和处理消息。

  2. 当前线程创建Handler对象时,会关联一个Looper对象,如果当前线程已经创建了Looper对象,则Handler对象会关联到此Looper对象,如果当前线程没有创建Looper对象,则Handler会创建一个新的Looper对象。

  3. 当消息循环开始时,Looper对象会从消息队列中获取消息,然后将消息转发给Handler对象,Handler对象根据消息类型,调用相应的回调函数处理消息,然后将处理结果返回给Looper对象。

  4. 消息处理完毕后,Looper对象会继续从消息队列中获取消息,重复上述过程,直到消息队列中没有消息为止。

2.7 为什么在子线程中创建Handler会抛异常?

不能在还没有调用Handler。Looper.prepare()
因为抛出异常的地方,在mLooper 对象为null的时候,会抛出异常。说明这里Looper.myLooper(); 的返回值是null。 只有调用了Looper.prepare()方法,才会构造一个Looper对象并在 ThreadLocal 存储当前线程的Looper 对象。
这样在调用 Looper.myLooper() 时,获取的结果就不会为null。

2.8 试从源码角度分析Handler的post和sendMessage方法的区别和应用场景?

  1. post方法接收的参数是一个Runnable对象,sendMessage方法接收的是一个Message对象。

  2. post方法是将Runnable对象放入消息队列中,而sendMessage方法是将Message对象放入消息队列中。

  3. post方法可以更快地处理消息,因为它不需要创建Message对象,但是post方法不能携带任何信息,而sendMessage方法可以携带任何信息和数据,包括附加消息和处理结果。

应用场景:

  1. 如果只需要执行某个任务,post方法更适合,因为它不需要创建Message对象,处理消息的速度更快。

  2. 如果需要从其他线程传递数据给UI线程,或者从UI线程传递数据给其他线程,sendMessage方法更适合,因为它除了可以执行任务外,还可以携带数据和附加信息。

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

Handler中的Loop死循环不会阻塞主线程,因为它是在一个单独的线程中运行的,而主线程是另外一个线程,互不影响。

Loop死循环的原理:

  1. Loop死循环是在一个单独的线程中运行的,即Looper线程,它会一直不断地从消息队列中取出消息,并将消息转发给Handler对象。

  2. 当消息队列中没有消息时,Loop死循环会等待,直到收到新的消息,然后继续取出消息,直到消息队列中没有消息为止。

第三节 Android UI 绘制相关

3.1 Android 补间动画和属性动画的区别?

特性 补间动画 属性动画
view 动画 支持 支持
非view动画 不支持 支持
可扩展性和灵活性
view属性是否变化 无变化 发生变化

3.2 Window和DecorView是什么?DecorView又是如何和Window建立联系的?

Window 是最顶层的视图,它负责背景WindowManager(窗口背景)、Title之类的标准的UI元素, Window是一个抽象类,整个Android系统中, PhoneWindow是 Window的唯一实现类。

DecorView,它是一个顶级 View,内部会包含一个竖直方向的LinearLayout,这个有上下两部分,分为 titlebar 和LinearLayoutcontentParent两个子元素,contentParent的 id是content,而我们自定义的的布局就是ActivitycontentParent 里面的一个子元素。View 层的所有事件都要先经过后才传递给我们的 View。

DecorView是 Window 的一个变量,即 DecorView 作为DecorView一切视图的根布局,被 Window 所持有,我们自定义的View 会被添加到 DecorView,而DecorView又会被添加到Window 中加载和渲染显示。

此处放一张它们的简单内部层次结构图:

3.3 简述一下 Android 中 UI 的刷新机制

Android UI的刷新机制是指在Android应用程序中,当UI组件的内容发生变化时,系统会自动调用View的onDraw()方法来重新绘制UI,以保证UI的内容能够及时反映当前的状态,这就是Android UI的刷新机制。

界面刷新的本质流程

  1. 通过ViewRootImpl的 scheduleTraversals()进行界面的三大流程。
  2. 调用到scheduleTraversals()时不会立即执行,而是将该操作保存到待执行队列中。并给底层的刷新信号注册监听。
  3. 当 VSYNC信号到来时,会从待执行队列中取出对应的scheduleTraversals()操作,并将其加入到主线程 的消息队列中。
  4. 主线程从 消息队列中取出并执行三大流程:onMeasure()-onLayout()-onDraw()

同步屏障的作用

  1. 同步屏障用于阻塞住所有的同步消息(底层VSYNC的回调onVsync方法提交的消息是异步消息)
  2. 用于保证界面刷新功能的performTraversals()的优先执行。

同步屏障的原理?

  1. 主线程的Looper会一直循环调用MessageQueue的next方法并且取出队列头部的Message执行,遇到同步屏障(一种特殊消息)后会去寻找异步消息执行。如果没有找到异步消息就会一直阻塞下去,除非将同步屏 障取出,否则永远不会执行同步消息。
  2. 界面刷新操作是异步消息,具有最高优先级
  3. 我们发送的消息是同步消息,再多耗时操作也不会影响UI的刷新操作

3.4 LinearLayout, FrameLayout,RelativeLayout 哪个效率高, 为什么?

对于比较三者的效率那肯定是要在相同布局条件下比较绘制的流畅度及绘制过程,在这里流畅度不好表达,并且受其他外部因素干扰比较多,比如CPU、GPU等等,我说下在绘制过程中的比较

  1. Fragment是从上到下的一个堆 叠的方式布局的,那当然是绘制速度最快,只需要将本身绘制出来
    即可,但是由于它的绘制方式导致在复杂场景中直接是不能使用的,所以工作效率来说Fragment仅使用于单一场景.

  2. LinearLayout 在两个方向上绘制的布局,在工作中使用页比较多,绘制的时候只需要按照指定的方向绘制,绘制效率比Fragment要慢,但使用场景比较多,

  3. RelativeLayout 它的没个子控件都是需要相对的其他控件来计算,按照View树的绘制流程、在不同的分支上要进行计算相对应的位置,绘制效率最低,但是一般工作中的 布局使用较多.

所以说这三者之间效率分开来讲个有优势、不足,那一起来讲也是有优势、不足,所以不能绝对的区分三者的效率,好不好用按需求来说!

3.5 谈谈Android的事件分发机制?

当点击的时候,会先调用顶级viewgroup的dispatchTouchEvent,如果顶级的viewgroup拦截了此事件**(onInterceptTouchEvent返回true**),则此事件序列 由顶级viewgroup处理。如果顶级viewgroup设置setOnTouchListener,则会回调接口中的onTouch,此时顶级的viewgroup中的onTouchEvent不再回调,如果不设 置setOnTouchListener则onTouchEvent会回调。如果顶级
viewgroup设置setOnClickListener,则会回调接口中的onClick。如果顶级viewgroup不拦截事件,事件就会向下传递给他的子view,然后子view就会调用它的dispatchTouchEvent方法。

3.6谈谈自定义View的流程?

  1. 安卓View的绘制流程(比较简单,想要深入的可以去看源码)
  2. 安卓自定义View的绘制步骤
    自定义View是一个老生常谈的问题,对于一个Android开发者来说是必须掌握的知识点,也是Android开发进阶的必经之路。
    要想安卓理解自定义View的流程,首先我们要了解View的绘制流程。
    分析之前,我们先来看底下面这张图:

View的绘制流程

DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容 的容器)。

关于ContentView,它是一个FrameLayout android.R.id.content),我们平常用的setContentView 就是设置它的子View。上图还表达了每个Activity都与一个Window
具体来说是PhoneWindow)相关联,用户界面则由Window所承载。

ViewRoot
在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot 来负责的。
每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。

那么decorView与ViewRoot的关联关系是在什么时候建立 的呢?

答案是Activity启动时,
ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。下面我们 直入主题,

分析一下ViewRoot是如何完成View的绘制的

View绘制的起点
当 建 立 好 了 decorView 与 ViewRoot 的 关 联 后 , ViewRoot 类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        /
        / 检查发起布局请求的线程是否为主线程
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

上面的方法中调用了scheduleTraversals()方法来调度一次 完成的绘制流程,该方法会向主线程发送一个“遍历”消息, 最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

三个阶段

View的整个绘制流程可以分为以下三个阶段:
measure: 判断是否需要重新计算View的大小,需要的话则计算;
layout: 判断是否需要重新计算View的位置,需要的话则计算;
draw: 判断是否需要重新绘制View,需要的话则重绘制。

这三个子阶段可以用下图来描述:

measure阶段
此阶段的目的是计算出控件树中的各个控件要显示其内容的话,需要多大尺寸。起点是ViewRootImpl的
measureHierarchy()方法,这个方法的源码如下:

private boolean measureHierarchy(final View
host, final WindowManager.LayoutParams lp, final
Resources res,
final int desiredWindowWidth, final int
desiredWindowHeight) {
/
/ 传入的desiredWindowXxx为窗口尺寸
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
boolean goodMeasure = false;
if (!goodMeasure)
{
    childWidthMeasureSpec =  getRootMeasureSpec(desiredWindowWidth,
    lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,
    lp.height);
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
}

if (mWidth != host.getMeasuredWidth() || mHeight !=
    host.getMeasuredHeight()) {
    windowSizeMayChange = true;
    }
}
return windowSizeMayChange; 
}

上面的代码中调用getRootMeasureSpec()方法来获取根MeasureSpec,这个根MeasureSpec代表了对decorView 的宽高的约束信息。具体的内部方法您可以直接再AS进行查看,不再赘述。

layout阶段
layout阶段的基本思想也是由根View开始,递归地完成整个控件树的布局(layout)工作。

View.layout()
我们把对decorView的layout()方法的调用作为布局整个控件树的起点,实际上调用的是View类的layout()方法,源码如下:

public void layout(int l, int t, int r, int
b) {
    // l为本View左边缘与父View左边缘的距离
    // t为本View上边缘与父View上边缘的距离
    // r为本View右边缘与父View左边缘的距离
    // b为本View下边缘与父View上边缘的距离
    
    boolean changed =
    isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r,
    b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags &
    PFLAG_LAYOUT_REQUIRED) ==
    PFLAG_LAYOUT_REQUIRED) {
    onLayout(changed, l, t, r, b);
    
    }

}

这个方法会调用setFrame()方法来设置View的mLeft、mTop、mRight和mBottom四个参数,这四个参数描述了View相对其父View的位置(分别赋值为l, t, r, b),在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View进行重新布局,对子View的局部是通
过onLayout()方法实现了。由于普通View( 非ViewGroup)不含子View,所以View类的onLayout()方法为空。
因此接下来,您可以通过源码查看ViewGroup类的onLayout()方法的实现,不再赘述。

draw阶段

对于本阶段的分析,我们以decorView.draw()作为分析的起点,也就是View.draw()方法,它的源码如下:

public void draw(Canvas canvas) {
    
    // 绘制背景,只有dirtyOpaque为false时才进行绘
    制,下同
    int saveCount;
    if (!dirtyOpaque)
    {
        drawBackground(canvas);
    }
    
    // 绘制自身内容
    if (!dirtyOpaque) onDraw(canvas);
    // 绘制子View
    dispatchDraw(canvas);
    
    // 绘制滚动条等
    onDrawForeground(canvas);

}

简单起见,在上面的代码中我们省略了实现滑动时渐变边框效果相关的逻辑。实际上,View类的onDraw()方法为 空,因为每个View绘制自身的方式都不尽相同,对于decorView来说,由于它是容器View,所以它本身并没有什么要绘制的。dispatchDraw()方法用于绘制子View,显 然普通
View(非ViewGroup)并不能包含子View,所以View类中这个方法的实现为空。

ViewGroup类的dispatchDraw()方法中会依次调用drawChild()方法来绘制子View,drawChild()方法的源码如下:

protected boolean drawChild(Canvas canvas,View child, long drawingTime) {
    return child.draw(canvas, this,drawingTime);
}

这个方法调用了View.draw(Canvas, ViewGroup,long)方法来对子View进行绘制。在draw(Canvas,ViewGroup,long)方法中,首先对canvas进行了一系列变换,以变换到将要
被绘制的View的坐标系下。完成对canvas的变换后, 便会调用View.draw(Canvas)方法进行实际的绘制工作,此时传入的canvas为经过变换的,在将被绘制View的坐标系下的canvas。

进入到View.draw(Canvas)方法后,会向之前介绍的一样, 执行以下几步:

  • 绘制背景;
  • 通过onDraw()绘制自身内容;
  • 通过dispatchDraw()绘制子View; 绘制滚动条
    至此,整个View的绘制流程我们就分析完了。

Android自定义View / ViewGroup的步骤大致如下:

  1. 自定义属性;
  2. 选择和设置构造方法;
  3. 重写onMeasure()方法;
  4. 重写onDraw()方法;
  5. 重写onLayout()方法;
  6. 重写其他事件的方法(滑动监听等);

自定义属性
Android自定义属性主要有定义、使用和获取三个步骤。
定义自定义属性
参 考 :
我们通常将自定义属性定义在/values/attr.xml文件中attr.xml文件需要自己创建)。
先来看一段示例代码:

<?xml version="1.0" encoding="utf-8"?>
resources>
<attr name="rightPadding"
format="dimension" />
<declare-styleable name="CustomMenu">
attr name="rightPadding" />
</declare-styleable>
</resources>

可以看到,我们先是定义了一个属性rightPadding,然后又在CustomMenu中引用了这个属性。

下面说明一下:

  • 首先,我们可以在declare-stylable标签中直接定义属性而不需要引用外部定义好的属性,但是为了属性的重用,我们可以选择上面的这种方法:先定义,后引用;
  • declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能又一个attr.xml 文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称;
  • 所谓的在declare-stylable标签中的引用,就是去掉了外部定义的format属性,如果没有去掉format,则会报错;如果外部定义中没有format而在内部引用中又format,也一样会报错。

常用的format类型:

  1. string:字符串类型;
  2. integer:整数类型;
  3. float:浮点型;
  4. dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
  5. Boolean:布尔值;
  6. reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
  7. color:颜色,必须是“#”符号开头;
  8. fraction:百分比,必须是“%”符号结尾;
  9. enum:枚举类型

下面对format类型说明几点:

  • format中可以写多种类型,中间使用“|”符号分割开,表 示这几种类型都可以传入这个属性;
  • enum类型的定义示例如下代码所示:
</resources>
<attr name="orientation">
<enum name="horizontal" value="0"/>
<enum name="vertical" value="1" />
</attr>
<declare-styleable name="CustomView">
<attr name="orientation" />
/declare-styleable>
</resources>

使用时通过getInt()方法获取到value并判断,根据不同的value进行不同的操作即可。

使用自定义属性

在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace。Android中默认的namespac是android,因此我们通常可以使用“android:xxx”的格式去设置一个控件的某个属性,android这个namespace的定义是在XML文件的头标签中定义的,通常是这样的:

xmlns:android="http://schemas.android.com/apk/res/android"

我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。
自定义属性的命名空间如下:

xmlns:app="http://schemas.android.com/apk/res-auto"

可以看出来,除了将命名空间的名称从android改成app之外,就是将最后的“res/android”改成了“res-auto”。注意:自定义namespace的名称可以自己定义,不一定非得是app。

获取自定义属性
在自定义View/ViewGroup中,我们可以通过TypedArray获取到自定义的属性。示例代码如下:

public CustomMenu(Context context,
AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

TypedArray a = context.getTheme().obtainStyledAttributes(att rs,
R.styleable.CustomMenu, defStyleAttr, 0);
int indexCount = a.getIndexCount(); 
for (int i = 0;i < indexCount; i++) {
    int attr = a.getIndex(i); 
    switch
        (attr) {
            case
            R.styleable.CustomMenu_rightPadding:
            mMenuRightPadding =
            a.getDimensionPixelSize(attr, 0);
            break;
        }
    }
  a.recycle();
}
  • 获取自定义属性的代码通常是在三个参数的构造方法中
    编写的(具体为什么是三个参数的构造方法,下面的章节中会有解释);
  • 在获取TypedArray对象时就为其绑定了该自定义View的自定义属性集(CustomMenu),通过getIndexCount()方法获取到自定义属性的数量,通过getIndex()方法获取到某一个属性,最后通过switch语 句判断属性并进行相应的操作;
  • 在TypedArray使用结束后,需要调用recycle()方法回收 它。

构造方法
当我们定义一个新的类继承了View或ViewGroup时, 系统都会提示我们重写它的构造方法。View / ViewGroup 中有四个构造方法可以重写,它们分别有一、二、三、四个参数。四个参数的构造方法我们通常用不到,因此这个章节中我们主要介绍一个参数、两个参数和三个参数的构造方法(这里以
CustomMenu控件为例)。

一个参数的构造方法
public CustomMenu(Context context) { …… }
这个构造方法只有一个参数Context上下文。当我们在JAVA代码中直接通过new关键在创建这个控件时,就会调用这个方法。

两个参数的构造方法

public CustomMenu(Context context,
AttributeSet attrs) { …… }

这个构造方法有两个参数:Context上下文和AttributeSet属性集。当我们需要在自定义控件中获取属性时,就默认调用这个构造方法。AttributeSet对象就是这个控件中定义的所有属性。

我们可以通过AttributeSet对象的getAttributeCount() 方法获取属性的个数,通过getAttributeName()方法获取到某条属性的名称,通过getAttributeValue()方法获取到某条属性的值。

注意:不管有没有使用自定义属性,都会默认调用这个构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。

三个参数的构造方法

public CustomMenu(Context context,
AttributeSet attrs, int defStyleAttr) { …… }

这个构造方法中有三个参数:Context上下文、AttributeSet属性集和defStyleAttr自定义属性的引用。这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。

defStyleAttr指向的是自定义属性的标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr。

三个构造方法的整合
一般情况下,我们会将这三个构造方法串联起来,即层层调用,让最终的业务处理都集中在三个参数的构造方法。我们让一参的构造方法引用两参的构造方法,两参的构造方法引用三参的构造方法。示例代码如下:

public CustomMenu(Context context)
this(context, null);
{
}
public CustomMenu(Context context,
AttributeSet attrs) {
    this(context, attrs, 0);
}
public CustomMenu(Context context,
AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    // 业务代码
}

这样一来,就可以保证无论使用什么方式创建这个控件,最终都会到三个参数的构造方法中处理,减少了重复代码。

onMeasure()

onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提 供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。

widthMeasureSpec和heightMeasureSpec虽然只是int类型的值,但它们是通过MeasureSpec类进行了编码处理的,其中封装了测量模式和测量值,因此我们可以分别通过
MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)来获取到控件或其子View的测量模式和测量值。

测量模式分为以下三种情况:

  1. EXACTLY:当宽高值设置为具体值时使用,如100DIP、
    match_parent等,此时取出的size是精确的尺寸;
  2. AT_MOST:当宽高值设置为wrap_content时使用,此
    时取出的size是控件最大可获得的空间;
  3. UNSPECIFIED:当没有指定宽高值时使用(很少见)

onMeasure()方法中常用的方法:

  1. getChildCount():获取子View的数量;
  2. getChildAt(i):获取第i个子控件;
  3. subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
  4. measureChild(child, widthMeasureSpec,heightMeasureSpec):测量子View的宽高;
  5. child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
  6. getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
  7. setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super.onMeasure(widthMeasureSpec,heightMeasureSpec);”这行代码。

注意:onMeasure()方法可能被调用多次,这是因为控件中的
内容或子View可能对分配给自己的空间“不满意”,因此向
父空间申请重新分配空间。

onDraw()

onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自 己绘制我们的自定义控件的显示效果。

要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。

注意:每次触摸了自定义View/ViewGroup时都会触发onDraw()方法。

Paint类
Paint画笔对象,这个类中包含了如何绘制几何图形、 文字和位图的样式和颜色信息,指定了如何绘制文本和图形。
画笔对象右很多设置方法,大体上可以分为两类:一类与图形绘制有关,一类与文本绘制有关。

Paint类中有如下方法:

1、图形绘制:

  1. setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
  2. setAlpha(int a):设置绘制的图形的透明度;
  3. setColor(int color):设置绘制的颜色;
  4. setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
  5. setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
  6. setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
  7. setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
  8. setColorFilter(ColorFilter cf):设置颜色过滤器,可以在绘制颜色时实现不同颜色的变换效果;
  9. setPathEffect(PathEffect pe):设置绘制的路径的效果;
  10. setShader(Shader s):设置Shader绘制各种渐变效果;
  11. setShadowLayer(float r, int x, int y, intc):在图形下面设置阴影层,r为阴影角度,x和y为阴影在x轴和y轴上的距离,c为阴影的颜色;
  12. setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
  13. setStrokeCap(Paint.Cap c):当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式;
  14. setStrokeJoin(Paint.Join j):设置绘制时各图形的结合方式;
  15. setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
  16. setXfermode(Xfermode m):设置图形重叠时的处理方式;

2、文本绘制:

  1. setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
  2. setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
  3. setTextSize(float s):设置字号;
  4. setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
  5. setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
  6. setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
  7. setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
  8. setFakeBoldText(boolean b):模拟实现粗体文字,如果设置在小字体上效果会非常差;
  9. setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;

3、其他方法:

  1. getTextBounds(String t, int s, int e, Rectb):将页面中t文本从s下标开始到e下标结束的所有字符所占的区域宽高封装到b这个矩形中;
  2. clearShadowLayer():清除阴影层;
  3. measureText(String t, int s, int e):返回t文本中从s下标开始到e下标结束的所有字符所占的宽度;
  4. reset():重置画笔为默认值。

这里需要就几个方法解释一下:

  1. setPathEffect(PathEffect pe):设置绘制的路径的效果:
    常见的有以下几种可选方案:
  1. CornerPathEffect:可以用圆角来代替尖锐的角;
  2. DathPathEffect:虚线,由短线和点组成;
  3. DiscretePathEffect:荆棘状的线条;
  4. PathDashPathEffect:定义一种新的形状并将其作为原始路径的轮廓标记;
  5. SumPathEffect:在一条路径中顺序添加参数中的效果;
  6. ComposePathEffect:将两种效果组合起来,先使用第一种效果,在此基础上应用第二种效果。
  1. setXfermode(Xfermode m):设置图形重叠时的处理方式:
    关于Xfermode的多种效果,我们可以参考下面一张图:

在使用的时候,我们需要通过paint.setXfermode(newPorterDuffXfermode(PorterDuff.Mode.XXX))来设置,XXX是上图中的某种模式对应的常量参数,如DST_OUT。

这16中情况的具体解释如下:

  1. PorterDuff.Mode.CLEAR:所绘制不会提交到画布上。

  2. PorterDuff.Mode.SRC:显示上层绘制图片

  3. PorterDuff.Mode.DST:显示下层绘制图片

  4. PorterDuff.Mode.SRC_OVER:正常绘制显示,上下层绘制叠盖。

  5. PorterDuff.Mode.DST_OVER:上下层都显示。下层居上显示。

  6. PorterDuff.Mode.SRC_IN:取两层绘制交集。显示上层。

  7. PorterDuff.Mode.DST_IN:取两层绘制交集。显示下层。

  8. PorterDuff.Mode.SRC_OUT:上层绘制非交集部分。

  9. PorterDuff.Mode.DST_OUT:取下层绘制非交集部分。

  10. PorterDuff.Mode.SRC_ATOP:取下层非交集部分与上层交集部分

  11. PorterDuff.Mode.DST_ATOP:取上层非交集部分与下层交集部分

  12. PorterDuff.Mode.XOR:异或:去除两图层交集部分

  13. PorterDuff.Mode.DARKEN:取两图层全部区域,交集部分颜色加深

  14. PorterDuff.Mode.LIGHTEN:取两图层全部,点亮交集部分颜色

  15. PorterDuff.Mode.MULTIPLY:取两图层交集部分叠加后颜色

  16. PorterDuff.Mode.SCREEN:取两图层全部区域,交集部
    分变为透明色

Canvas类
Canvas即画布,其上可以使用Paint画笔对象绘制很多东西。
Canvas**对象中可以绘制:

  1. drawArc():绘制圆弧;
  2. drawBitmap():绘制Bitmap图像;
  3. drawCircle():绘制圆圈;
  4. drawLine():绘制线条;
  5. drawOval():绘制椭圆;
  6. drawPath():绘制Path路径;
  7. drawPicture():绘制Picture图片;
  8. drawRect():绘制矩形;
  9. drawRoundRect():绘制圆角矩形;
  10. drawText():绘制文本;
  11. drawVertices():绘制顶点。

Canvas**对象的其他方法:

  1. canvas.save():把当前绘制的图像保存起来,让后续的操作相当于是在一个新图层上绘制;
  2. canvas.restore():把当前画布调整到上一个save()之前的状态;
  3. canvas.translate(dx, dy):把当前画布的原点移到(dx, dy)点,后续操作都以(dx, dy)点作为参照;
  4. canvas.scale(x, y):将当前画布在水平方向上缩放x倍,竖直方向上缩放y倍;
  5. canvas.rotate(angle):将当前画布顺时针旋转angle度。

onLayout()

onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。
onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;
l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。

以下是onLayout()方法中常用的方法:

  1. getChildCount():获取子View的数量;
  2. getChildAt(i):获取第i个子View
  3. getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;
  4. child.getLayoutParams():获取到子View的LayoutParams对象;
  5. child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;
  6. getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
  7. child.layout(l, t, r, b):设置子View布局的上下左右边的坐标。

其他方法
generateLayoutParams()
generateLayoutParams()方法用在自定义ViewGroup 中,用来指明子控件之间的关系,即与当前的ViewGroup 对应的LayoutParams。我们只需要在方法中返回一个我们想要使用的LayoutParams类型的对象即可。

在generateLayoutParams()方法中需要传入一个AttributeSet对象作为参数,这个对象是这个ViewGroup的 属性集,系统根据这个ViewGroup的属性集来定义子View 的布局规则,供子View使用。

例如,在自定义流式布局中,我们只需要关心子控件之间的间隔关系,因此我们需要在
generateLayoutParams()方法中返回一个newMarginLayoutParams()即可。

onTouchEvent()

onTouchEvent()方法用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。

onScrollChanged()

如果我们的自定义View / ViewGroup是继承自 ScrollView / HorizontalScrollView等可以滚动的控件,就可以通过重写onScrollChanged()方法来监听控件的滚动事件。

这个方法中有四个参数:
l和t分别表示当前滑动到的点在水平和竖直方向上的坐标;
oldl和oldt分别表示上次滑动 到的点在水平和竖直方向上的坐标。我们可以通过这四个 值对滑
动进行处理,如添加属性动画等。

invalidate()

invalidate()方法的作用是请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
一般会引起invalidate()操作的函数如下:

  1. 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身;
  2. 调用setSelection()方法,请求重新draw(),但只会绘制调用者本身;
  3. 调用setVisibility()方法,会间接调用invalidate()方法,继而绘制该View;
  4. 调用setEnabled()方法,请求重新draw(),但不会重新绘制任何视图,包括调用者本身。

postInvalidate()

功能与invalidate()方法相同,只是postInvalidate()方法是异步请求重绘视图。

requestLayout()

requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会 调用draw()过程,即不会重新绘制任何视图,包括该调用 者本身。

requestFocus()

请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。

总结
最后,让我们来总览一下自定义View / ViewGroup时调用的各种函数的顺序,如下图所示:

3.7 针对RecyclerView你做了哪些优化?

  1. onBindViewHolder
    这个方法含义应该都知道是绑定数据,并且是在UI线程,所以要尽量在这个方法中少做一些业务处理
  2. 数据优化
    采用android Support 包下的DIffUtil集合工具类结合RV分页加载会更加友好,节省性能
  3. item优化
    减少item的View的层级,(pps:当然推荐把一个item自定义成一个View,如果有能力的话),如果item的高度固定的话可以设置setHasFixedSize(true),避免requestLayout浪费资源
  4. 使用RecycledViewPool
    RecycledViewPool是对item进行缓存的,item相同的不同
    RV可以才使用这种方式进行性能提升
  5. 资源回收
    通过重写RecyclerView.onViewRecycled(holder)来合理的 回收资
    源。

3.8 谈谈如何优化ListView?

  1. 采用ViewHolder模式,避免重复创建和销毁View,提高效率。

  2. 采用复用机制,只有当ListView滑动到可视范围外时,才会将外部的View复用到可视范围内。

  3. 采用异步加载图片,只加载当前可视范围内的图片,避免一次性加载所有图片,提高效率。

  4. 采用缓存机制,将已经加载的图片缓存起来,当加载相同的图片时,可以直接从缓存中读取,

3.9 谈谈自定义LayoutManager的流程?

1、继承RecyclerView.LayoutManager类,实现其中的方法,完成自定义LayoutManager的主体框架。
2、实现generateDefaultLayoutParams()方法,为item添加默认的LayoutParams。
3、实现onLayoutChildren()方法,完成对item的布局操作。
4、根据需要,继续实现滚动操作相关的函数,如scrollVerticallyBy(), scrollHorizontallyBy()等。
5、根据需要,实现对item的额外操作,比如添加分割线、滑动动画等。

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

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

用法
通常是在通知栏

//1.创建RemoteViews实例
RemoteViews mRemoteViews=new
RemoteViews("com.example.remoteviewdemo",
R.layout.remoteview_layout);

//2.构建一个打开Activity的PendingIntent Intent
intent=new
Intent(MainActivity.this,MainActivity.class);
PendingIntent
mPendingIntent=PendingIntent.getActivity(Main
Activity.this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);

//3.创建一个Notification
mNotification = new
Notification.Builder(this)
setSmallIcon(R.drawable.ic_launcher)
setContentIntent(mPendingIntent)

setContent(mRemoteViews)
build();
//4. 获 取 NotificationManager
    manager = (NotificationManager)
    getSystemService(Context.NOTIFICATION_SERVICE
);
Button button1 = (Button)findViewById(R.id.button1);
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//弹出通知
    manager.notify(1,mNotification);
}

3.11 谈一谈获取View宽高的几种方法?

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

3.12谈一谈插值器和估值器?

1、插值器,根据时间(动画时常)流逝的百分比来计算属性变化的百分比。系统默认的有匀速,加减速,减速插值器。
2、估值器,通过上面插值器得到的百分比计算出具体变化的值。系统默认的有整型,浮点型,颜色估值器
3、自定义只需要重写他们的evaluate方法就可以了。

3.13 getDimension、getDimensionPixelOffset 和getDimensionPixelSize 三者的区别?

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

3.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 在基础行距上添加多少
TextUtils.TruncateAt ellipsize 从什么位置开始省略
int ellipsizedWidth 超过多少开始省略

3.15 有用过ConstraintLayout吗?它有哪些特点?

属性图:

3.16 关于LayoutInflater,它是如何通过inflate 方法获取到具体View的?

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

3.17 谈一谈Fragment懒加载?

Fragment懒加载是指在Fragment可见时才加载数据,而不是在Activity创建时就加载数据。优点是可以极大提高程序的性能,因为在Fragment不可见时可以避免加载无用的数据。实现Fragment懒加载的方法有:

1、在Fragment的onCreateView()方法中判断Fragment是否可见,如果可见则加载数据,否则不加载数据;
2、在Fragment的onResume()方法中判断Fragment是否可见,如果可见则加载数据,否则不加载数据;
3、使用ViewPager的setUserVisibleHint()方法,在Fragment可见时加载数据,不可见时不加载数据。

3.18 谈谈RecyclerView的缓存机制?

1、RecyclerView.ViewCacheExtension:该缓存机制主要用于ViewHolder中的item视图的缓存,当item视图重用时,可以节省创建新的ViewHolder的时间。
2、RecyclerView.LayoutManager:该缓存机制主要用于LayoutManager中的状态信息的缓存,当RecyclerView滑动时,可以节省重新计算布局位置的时间。
3、RecyclerView.RecycledViewPool:该缓存机制主要用于缓存RecyclerView中的ViewHolder,当ViewHolder重用时,可以节省创建新的ViewHolder的时间。
4、RecyclerView.ItemAnimator:该缓存机制主要用于缓存RecyclerView中的Item动画,当Item动画重用时,可以节省重新计算动画效果的时间。

3.19 请谈谈View.inflate和LayoutInflater.inflate的区别?

View.inflate:View.inflate方法是View类中定义的,用于将XML布局文件转换成一个View实例。

LayoutInflater.inflate:LayoutInflater.inflate方法是LayoutInflater类中定义的,用于将XML布局文件转换成一个ViewGroup实例,它可以接受两个参数,可以指定ViewGroup的父布局,以及是否将XML布局文件中的视图添加到父布局中。

3.20 请谈谈invalidate()和postInvalidate() 方法的区别和应用场景?

  1. invalidate()用来重绘UI,需要在UI线程调用。
  2. postInvalidate()也是用来重新绘制UI,它可以在UI线程调 用,
    也可以在子线程中调用,postInvalidate()方法内部通过Handler发送了一个消息将线程切回到UI线程通知重新绘制,并不是说postInvalidate()可以在子线程更新UI,本质上还是在UI线程发生重绘,只不过我们使用postInvalidate()它内部会帮我们切换线程

3.21 谈一谈自定义View和自定义ViewGroup?

1、自定义View是基于现有的View类,比如TextView、ImageView等,它们只是对现有的View类进行了扩展和修改,以满足特定的需求;而自定义ViewGroup则是完全自定义,可以实现任何你想要的布局效果。

2、自定义View的实现比自定义ViewGroup要简单,只需要重写onDraw()方法和onMeasure()方法就可以实现;而自定义ViewGroup的实现要复杂一些,除了需要重写onDraw()和onMeasure()方法外,还需要重写onLayout()方法,用于完成子View的布局。

3、自定义View只能显示一个视图,而自定义ViewGroup可以显示多个视图,可以实现复杂的布局。

3.22 谈一谈SurfaceView与TextureView的使用场景和用法?

1、频繁绘制和对帧率要求比较高的需求,比如拍照、视频和游戏等
2、SurfaceView有独立的绘图表面,可以在子线程中进行绘制,缺点是不能够执行平移、缩放、旋转、透明渐变操作,TextureView的出现就是为了解决这些问题
3、SurfaceView的使用方法,大概是获取SurfaceHolder 对象,监听surface创建,更新,销毁,创新一个新的线程,并在其中绘制并提交
4、TextureView并没有独立的绘图表面,在使用过程中, 需要添加监听surfaceTexture是否可用,再做相应的处理

3.23 谈一谈RecyclerView.Adapter的几种刷新方式有何不同?

刷新全部可见的item,notifyDataSetChanged() 刷新指定item,
notifyItemChanged(int)从指定位置开始刷新指定个item,notifyItemRangeChanged(int,int)插入、移动一个并自动刷新,
notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)局部刷新,notifyItemChanged(int,Object)

3.24 谈谈你对Window和WindowManager 的理解?

Window:抽象类,窗体容器.创建DecorView.PhoneWindow:Window实现类.

AppCompatDelegateImpl:AppCompatDeleGate的实现类.在构造方法中传入了Window.该类是Activity中方法的代理实现类.如:setContentView()…
WindowManager:接口类.同时实现了ViewManager.定义了大量Window的状态值
WindowManagerImpl:WindowManager的接口实现类.但具体的方法实现交给了WindowManagerGlobal.
WindowManagerGlobal:真正的WindowManager接口方法的处理类.如:创建ViewRootImpl等…

Window/WindowManager均在Activity的attach中完成

final void attach(Context context, ActivityThread
aThread,
Instrumentation instr, IBinder token, int ident, Application
application, Intent intent, ActivityInfo info, CharSequence
title, Activity parent, String id,
NonConfigurationInstances
lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor
voiceInteractor,
Window window, ActivityConfigCallback
activityConfigCallback) { attachBaseContext(context);
mFragments.attachHost(null /parent/);
mWindow = new PhoneWindow(this, window,
activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory (this);
if (info.softInputMode !=
WindowManager.LayoutParams.SOFT_INPUT_STATE_U
NSPECIFIED) {
    mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0)
{
    mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
.....
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Conte
xt.WINDOW_SERVICE),
mToken,
mComponent.flattenToString(),
(info.flags &
ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager =
mWindow.getWindowManager();
......
}

3.25 谈一谈Activity,View,Window三者的关系?

在activity中调用attach,创建window;
    创建的window是其子类phonewindow,在attach中创建
phonewindow;
    在activity中调用setcontentview (R.layout.xx);其实就是调用
getwindow.setcontentview()
创建parentview;将指定的R.layout.xx布局进行填充调用
viewgroup
调用viewGroup先移除removeAllview();在进行添加新的
view --addview().

3.26 有了解过WindowInsets吗?它有哪些应用?

ViewRootImpl在performTraversals时会调
dispatchApplyInsets,内调DecorView的
dispatchApplyWindowInsets,进行WindowInsets的分发.

3.27 Android中View几种常见位移方式的区别?

1、View.layout():直接改变View对象的位置,可以改变View对象的位置和大小,但是它不会触发任何重绘事件。

2、View.offsetLeftAndRight()和View.offsetTopAndBottom():相对View对象当前位置偏移,它们不会改变View对象的大小,只会改变位置,并且不会触发任何重绘事件。

3、View.scrollTo()和View.scrollBy():它们可以改变View对象的位置,但是它们只能用于滚动视图,而且会触发重绘事件。

4、Animation:它们可以改变View对象的位置,这是一种比较常用的位移方式,它会触发重绘事件。

或者也可以:改变View的layout参数:这种方式会改变View在布局中的位置和大小,需要重新调用requestLayout()方法来重新测量和布局View。

  1. 改变View的translationX和translationY属性:这种方式会改变View在屏幕上的位置,但不会改变View在布局中的位置和大小,也不需要重新测量和布局View。
  2. 改变View的scrollX和scrollY属性:这种方式会改变View内容相对于容器的偏移量,但不会改变View在屏幕上或布局中的位置和大小,也不需要重新测量和布局View。
  3. 使用动画来移动View:这种方式可以使用平移、旋转、缩放等动画效果来移动或变换View,在动画执行过程中,不会改变View在屏幕上或布局中的位置和大小,也不需要重新测量和布局View。

3.28 为什么ViewPager嵌套ViewPager,内部的ViewPager滚动没有被拦截?

被外部的ViewPager拦截了,需要做滑动冲突处理。重写子View的 dispatchTouchEvent方法,在子View需要拦截的时候进行拦截,否则交给父View处理。

3.29 请谈谈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 已经加载完成,这就是预加载机制,当 fragment 不处于 ViewPager 左右两边时,就会执行onPause,onStop,OnDestroyView方法。
2, fragment 之间传递数据方法

1,使用 bundle,有些数据需要被序列化
2,接口回调
3,在创建的时候通过构造直接传入
4,使用 EventBus 等单 Activity 多 fragment 的优点,fragment 的优缺点fragment比activity占用更少的资源,特别在中低端手机,fragment 的响应速度非常快,如丝般的顺滑,更容易控制每个场景的生命周期和状态

优缺点:非常流畅,节省资源,灵活性高,fragment 必须赖于acctivity,而且 fragment 的生命周期直接受所在的activity 影响。

3.30 请谈谈什么是同步屏障?

handler.getLooper().getQueue().postSyncBarrier()加入同步屏障后,Message.obtain()获取一个target为null的msg,并根据当前时间将该msg插入到链表中。

Looper.loop()循环取消息中 Message msg =queue.next(); target为空时,取链表中的异步消息。通过setAsynchronous(true)来指定为异步消息

应用场景:ViewRootImpl scheduleTraversals中加入同步屏障并在view的绘制流程中post异步消息,保证view的绘制消息优先执行

3.31 谈一谈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().de nsity;
mEdgeSize = (int) (EDGE_SIZE *
density + 0.5f);//边界拖动距离范围
mTouchSlop =
vc.getScaledTouchSlop();// 拖 动 距 离 阈 值 mScroller
=new OverScroller(context,sInterpolator);//滚动器

}

mParentView是指基于哪个View进行触摸处理
mCallback是触摸处理的各个阶段的回调
mEdgeSize是指在边界多少距离内算作拖动,默认为0dp
mTouchSlop指滑动多少距离算作拖动,用的系统默认值
mScroller是View滚动的Scroller对象,用于处理释触摸放后,View的滚动行为,比如滚动回原始位置或者滚动 出屏幕

  1. 拦截事件处理
    该类提供了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
= 0|| (mTrackingEdges & edge)=|| (mEdgeDragsLocked[pointerId] &
edge) == edge| (mEdgeDragsInProgress[pointerId]| (absDelta <= mTouchSlop && edge) == edge absODelta <= mTouchSlop)) {
    return false;
}
if (absDelta < absODelta * 0.5f &&
mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |=
return false;
edge;
}
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方法处理拖动了!

  1. 拖动事件处理 该类提供了voidprocessTouchEvent(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(mActivePointerI d)) break;
final int index =
ev.findPointerIndex(mActivePointerId);
final float x =
ev.getX(index);
final float y =
ev.getY(index);
//计算距离上次的拖动距离final
int idx = (int) (xmLastMotionX[mActivePointerId]);
final int idy = (int) (ymLastMotionY[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(mCaptur
edView, left, dx);//通过callback获取真正的移动值
ViewCompat.offsetLeftAndRight(mCapturedView,
clampedX - oldLeft);//进行位移 
}
if (dy != 0)
{
    clampedY = mCallback.clampViewPositionVertical(mCaptured View, top,dy);
    ViewCompat.offsetTopAndBottom(mCapturedView,clampedY - oldTop);
}
if (dx != 0 || dy != 0)
{
final int clampedDx = clampedX -
oldLeft;
oldTop;
final int clampedDy = clampedY -
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(mActivePointerI d),
(int)
mVelocityTracker.getYVelocity(mActivePointerI d));
}
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);
   }
}

在我们的View中:

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

3.32 谈一谈屏幕刷新机制?

屏幕刷新频率和绘制频率
cpu 负责 measure layout draw => displayList
gpu 负责 display => 位图
每个16ms会发送一次垂直同步信号 vsync 每次信号发送的时候都会从gpu的buffer中取出渲染好的位 图显示在屏幕上
同时如果有需要 还会进行下一次的 cpu计算,计算好后放入buffer中
如果计算时间超过了两次vsync之间的时间 即16ms 则vsync信号会把 上一次gpu buffer中的信息展示出来 这时候就是卡顿

另外如果页面没有变化 屏幕还是一样会去buffer中取出上一次的刷新,只不过cpu不再去计算而已

Android 性能调优相关

1,启动优化
一个应用的启动快慢是能够直接影响用户的使用体验的,如果启动较慢可能会导致用户卸载放弃该应用 程序。

1.1.1 冷启动、热启动和温启动的优化 概念

对于Android应用程序来说,根据启动方式可以分为冷启动,热启动和温启动三种。
冷启动:系统不存在App进程(如APP首次启动或APP被完全杀死)时启动App称为冷启动。热启动:按了Home键或其它情况app被切换到后台,再次启动App的过程。
温启动:温启动包含了冷启动的一些操作,不过App进程依然存在,这代表着它比热启动有更多的 开销。可以看到,热启动是启动最快的,温启动则是介于冷启动和热启动之间的一种启动方式。下而冷启动则 是最慢的,因为它会涉及很多进程的创建,下面是冷启动相关的任务流程

1.1.2 视觉优化

在冷启动模式下,系统会启动三个任务:
加载并启动应用程序。

启动后立即显示应用程序空白的启动窗口。
创建应用程序进程。
一旦系统创建应用程序进程,应用程序进程就会进入下一阶段,并完成如下的一些事情。

  • 创建app对象
  • 启动主线程
  • (main thread)
  • 创建应用入口的
  • Activity对象填充加载布局
    View在屏幕上执行View的绘制过程.measure -> layout ->draw
    应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用 户可以开始使用该应用程序了。因为App应用进程的创建过程是由手机的软硬件决定的,所以我们只能 在这个创建过程中进行一些视觉优化。

1.1.3 启动主题优化

在冷启动的时候,当应用程序进程被创建后,就需要设置启动窗口的主题。目前,大部分的 应用在启动会都会先进入一 个 闪 屏 页 (LaunchActivity) 来展示应用信息,如果在Application 初始化了其它第三方的服务,就会出现启动的白屏问题。

为了更顺滑无缝衔接我们的闪屏页,可以在启动 Activity的 Theme中设置闪屏页图片,这样启动窗口的图片就会是闪屏页图片,而不是白屏。

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/lunch</item> //闪屏页图片
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
</style>

1.2 代码方面的优化

设置主题的方式只能应用在要求不是很高的场景,并且这种优化治标不治本,关键还在于代码的优化。 为了进行优化,我们需要掌握一些基本的数据。

1.2.1 冷启动耗时统计

ADB命令方式
在Android Studio的Terminal中输入以下命令可以查看页面的启动的时间,命令如下:

adb shell am start -W packagename/[packagename].首屏Activity

执行完成之后,会在控制台输出如下的信息:

Starting: Intent { act=android.intent.action.MAIN cat=
[android.intent.category.LAUNCHER] cmp=com.optimize.performance/.MainActivity }
Status: ok
Activity: com.optimize.performance/.MainActivity
ThisTime: 563
TotalTime: 563
WaitTime: 575

Complete
在上面的日志中有三个字段信息,即ThisTime、TotalTime和WaitTime。
ThisTime:最后一个Activity启动耗时
TotalTime:所有Activity启动耗时
WaitTime:AMS启动Activity的总耗时

日志方式
埋点方式是另一种统计线上时间的方式,这种方式通过记录启动时的时间和结束的时间,然后取二者差 值即可。首先,需要定义一个统计时间的工具类:

class LaunchRecord {
companion object {
private var sStart: Long = 0
    fun startRecord() {
    sStart = System.currentTimeMillis()
}
fun endRecord()
{
    endRecord("")
}
fun endRecord(postion: String) {
    val cost = System.currentTimeMillis() - sStart
    println("===$postion===$cost")
    }
  }
}

启动时埋点我们直接在Application的attachBaseContext中进行打点。那么启动结束应该在哪
里打点呢?结束埋点建议是在页面数据展示出来进行埋点。可以使用如下方法:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    mTextView.viewTreeObserver.addOnDrawListener
{
    LaunchRecord.endRecord("onDraw")
    }
}
override fun onWindowFocusChanged(hasFocus: Boolean)
{
    super.onWindowFocusChanged(hasFocus)
    LaunchRecord.endRecord("onWindowFocusChanged")
    }
}

1.2.2 优化检测工具

在做启动优化的时候,可以借助三方工具来帮助我们理清各个阶段的方法或者线程、CPU的执行耗时等 情况。这里主要介绍以下TraceView和SysTrace两款工具。
TraceView
TraceView是以图形的形式展示执行时间、调用栈等信息,信息比较全面,包含所有线程,如下图所 示。

使用TraceView检测生成生成的结果会放在Andrid/data/packagename/files路径下。因为
Traceview收集的信息比较全面,所以会导致运行开销严重,整体APP的运行会变慢,因此我们无法区分是不是Traceview影响了我们的启动时间。

SysTrace
Systrace是结合Android内核数据,生成HTML报告,从报告中我们可以看到各个线程的执行时间以及方 法耗时和CPU执行时间等。

再API 18以上版本,可以直接使用TraceCompat来抓取数据,因为这是兼容的API。

开始:TraceCompat.beginSection("tag ")
结束:TraceCompat.endSection()

然后,执行如下脚本。

Python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view
wm am app

这里可以大家普及下各个字端的含义:
b: 收集数据的大小
t: 时 间
a: 监听的应用包名
o: 生成文件的名称

Systrace开销较小,属于轻量级的工具,并且可以直观反映CPU的利用率。

2. UI渲染优化

Android系统每隔16ms就会重新绘制一次Activity,因此,我们的应用必须在16ms内完成屏幕刷新的全 部逻辑操作,每一帧只能停留16ms,否则就会出现掉帧现象。Android应用卡顿与否与UI渲染有直接的 关系。

2.1 CPU、GPU

对于大多数手机的屏幕刷新频率是60hz,也就是如果在1000/60=16.67ms内没有把这一帧的任务执行 完毕,就会发生丢帧的现象,丢帧是造成界面卡顿的直接原因,渲染操作通常依赖于两个核心组件:
CPU与GPU。CPU负责包括Measure,Layout等计算操作,GPU负责Rasterization(栅格化)操作。

所谓栅格化,就是将矢量图形转换为位图的过程,手机上显示是按照一个个像素来显示的,比如将一个
Button、TextView等组件拆分成一个个像素显示到手机屏幕上。而UI渲染优化的目的就是减轻CPU、
GPU的压力,除去不必要的操作,保证每帧16ms以内处理完所有的CPU与GPU的计算、绘制、渲染等 等操作,使UI顺滑、流畅的显示出来。

2.2 过度绘制

UI渲染优化的第一步就是找到Overdraw(过度绘制),即

描述的是屏幕上的某个像素在同一帧的时间 内被绘制了多次。在重叠的UI布局中,如果不可见的UI也在做绘制的操作或者后一个控件将前一个控件 遮挡,会导致某些像素区域被绘制了多次,从而增加了CPU、GPU的压力。

那么如何找出布局中Overdraw的地方呢?很简单,就是打开手机里开发者选项,然后将调试GPU过度 绘制的开关打开即可,然后就可以看到应用的布局是否被Overdraw,如下图所示。

蓝色、淡绿、淡红、深红代表了4种不同程度的Overdraw情况,1x、2x、3x和4x分别表示同一像素上
同一帧时间内被绘制了多次,1x就表示一次(最理想情况),4x表示次(最差的情况),而我们需要消除 的就是3x和4x。

2.3 解决自定义View的OverDraw

我们知道,自定义View的时候有时会重写onDraw方法,但是Android系统是无法检测onDraw里面具体 会执行什么操作,从而系统无法为我们做一些优化。这样对编程人员要求就高了,如果View有大量重叠 的地方就会造成CPU、GPU资源的浪费,此时我们可以使用canvas.clipRect()来帮助系统识别那些可见 的区域。

这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。下面我们通过 谷歌提供的一个小的Demo进一步说明OverDraw的使用。

在下面的代码中,DroidCard类封装的是卡片的信息,代码如下:

public class DroidCard {
public int x;//左侧绘制起点
public int width;
public int height;
public Bitmap bitmap;
public DroidCard(Resources res,int resId,int
x){
 this.bitmap = BitmapFactory.decodeResource(res,resId); this.x = x;
 this.width = this.bitmap.getWidth();
 this.height = this.bitmap.getHeight();
 }
}

自定义View的代码如下:

public class DroidCardsView extends View {
//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;
private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();
private Paint paint = new Paint();
public DroidCardsView(Context context)
{
    super(context);
    initCards();
}
public DroidCardsView(Context context, AttributeSet attrs) { super(context,
attrs);
    initCards();
}
/
**
*初始化卡片集合
*
/
protected void initCards(){ 
Resources res = getResources();
mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
}
@Override
protected void onDraw(Canvas canvas)
{
    super.onDraw(canvas);
    for (DroidCard c :mDroidCards){ drawDroidCard(canvas,c);
}
    invalidate();
}
/
**
*绘制DroidCard
*
/
private void drawDroidCard(Canvas canvas, DroidCard c)
{
     canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    }
}

然后,我们运行代码,打开手机的overdraw开关,效果如下:

可以看到,淡红色区域明显被绘制了三次,是因为图片的重叠造成的。那怎么解决这种问题呢?其实, 分析可以发现,最下面的图片只需要绘制三分之一即可,保证最下面两张图片只需要回执其三分之一最 上面图片完全绘制出来就可。优化后的代码如下:


public class DroidCardsView extends View {
//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录

private int mCardLeft = 10;
private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();
private Paint paint = new Paint();
public DroidCardsView(Context context)
{
super(context);
initCards();
}
public DroidCardsView(Context context, AttributeSet attrs)
{
super(context, attrs);
initCards();
}
/*初始化卡片集合*/
protected void
initCards(){ Resources res =
getResources();
mDroidCards.add(new DroidCard(res, R.drawable.alex,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.claire,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.kathryn,mCardLeft));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mDroidCards.size() - 1;
i++){ drawDroidCard(canvas, mDroidCards,i);
}
drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1));
invalidate();
}
/**
*绘制最后一个DroidCard
@param canvas
@param c
*/
private void drawLastDroidCard(Canvas canvas,DroidCard c)
{
}
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
/*绘制DroidCard
@param canvas
@param mDroidCards
@param i
**/
private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i)
{
DroidCard c = mDroidCards.get(i);
canvas.save();
canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
canvas.restore();
}
}

在上面的代码中,我们使用Canvas的clipRect方法,绘制之前裁剪出一个区域,这样绘制的时候只在这 区域内绘制,超出部分不会绘制出来。重新运行上面的代码,效果如下图所示:

2.4 Hierarchy Viewer

Hierarchy Viewer 是 Android Device Monitor 中内置的一种工具,可让开发者测量布局层次结构中每个视图的布局速度,以及帮助开发者查找视图层次结构导致的性能瓶颈。Hierarchy Viewer可以通过红、黄、绿三种不同的颜色来区分布局的Measure、Layout、Executive的相对性能表现情况。
打开

  1. 将设备连接到计算机。如果设备上显示对话框提示您允许 USB 调试吗?,请点按确定。
  2. 在 Android Studio 中打开您的项目,在您的设备上构建并运行项目。
  3. 启动 Android Device Monitor。Android Studio 可能会显示 Disable adb integration 对话框,因为一次只能有一个进程可以通过 adb 连接到设备,并且 Android Device Monitor 正在请求连接。因此,请点击 Yes。
  4. 在菜单栏中,依次选择 Window > Open Perspective,然后点击 Hierarchy View。
  5. 在左侧的 Windows 标签中双击应用的软件包名称。这会使用应用的视图层次结构填充相关窗格

提升布局性能的关键点是尽量保持布局层级的扁平化,避免出现重复的嵌套布局。如果我们写的布局层 级比较深会严重增加CPU的负担,造成性能的严重卡顿,关于HierarchyViewer的使用可以参考:使用Hierarchy Viewer 分析布局。

2.5 内存抖动

在我们优化过view的树形结构和overdraw之后,可能还是感觉自己的app有卡顿和丢帧,或者滑动慢等 问题,我们就要查看一下是否存在内存抖动情况了。所谓内存抖动,指的是内存频繁创建和GC造成的UI线程被频繁阻塞的现象。

Android有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里
面创建过多的对象是件需要特别引起注意的事情,在同一帧里创建大量对象可能引起GC的不停操作, 执行GC操作的时候,所有线程的任何操作都会需要暂停,直到GC操作完成。大量不停的GC操作则会显著占用帧间隔时间。如果在帧间隔时间里面做了过多的GC操作,那么就会造成页面卡顿。

在Android开发中,导致GC频繁操作有两个主要原因:

  • 内存抖动,所谓内存抖动就是短时间产生大量对象又在短时间内马上释放。短时间产生大量对象超出阈值,内存不够,同样会触发GC操作。

Android的内存抖动可以使用Android Studio的Profile然后,点击record记录内存信息,查找发生内存抖动位置,

当然也可直接通过Jump to Source定位到代码位置。为了避免发生内存抖动,我们需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,

onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法
避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是 这里需要注意结束使用之后,需要手动释放对象池中的对象。

3,内存优化

3.1 内存管理

在前面Java基础环节,我们对Java的内存管理模型也做了基本的介绍,参考文章:Android 面试之必问Java基础

3.1.1 内存区域

在Java的内存模型中,将内存区域划分为方法区、堆、程序、计数器、本地方法栈、虚拟机栈五个区域, 如下图。

方法区
线程共享区域,用于存储类信息、静态变量、常量、即时编译器编译出来的代码数据。 无法满足内存
分配需求时会发生OOM。


线程共享区域,是JAVA虚拟机管理的内存中最大的一块,在虚拟机启动时创建。存放对象实
例,几乎所有的对象实例都在堆上分配,GC管理的主要区域。

虚拟机栈
线程私有区域,每个java方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态 链接、方法出口等信息。方法从执行开始到结束过程就是栈帧在虚拟机栈中入栈出栈过程。

局部变量表存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会 在编译期间完成分配,进入一个方法时在帧中局部变量表的空间是完全确定的,不需要运行时改 变。

若线程申请的栈深度大于虚拟机允许的最大深度,会抛出SatckOverFlowError错误。 虚拟机
动态扩展时,若无法申请到足够内存,会抛出OutOfMemoryError错误。

本地方法栈
为虚拟机中Native方法服务,对本地方法栈中使用的语言、数据结构、使用方式没有强制规定,虚 拟机可自有实现。
占用的内存区大小是不固定的,可根据需要动态扩展。

程序计数器
一块较小的内存空间,线程私有,存储当前线程执行的字节码行号指示器。
字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、循环、跳转 等。
每个线程都有一个独立的程序计数器唯一一个在java虚拟机中不会OOM的区域

3.1.2 垃圾回收

标记清除算法
标记清除算法主要分为有两个阶段,首先标记出需要回收的对象,然后咋标记完成后统一回收所有标记 的对象;

缺点:
效率问题:标记和清除两个过程效率都不高。
空间问题:标记清除之后会导致很多不连续的内存碎片,会导致需要分配大对象时无法找到足够的 连续空间而不得不触发GC的问题。

复制算法
将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对 象复制到另一块内存上,然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收,不会导 致碎片问题,实现简单且效率高效。

缺点:
需要将内存缩小为原来的一半,空间代价太高。
标记整理算法
标记整理算法标记过程和标记清除算法一样,但清除过程并不是对可回收对象直接清理,而是将所有存 活对象像一端移动,然后集中清理到端边界以外的内存。

分代回收算法
当代虚拟机垃圾回收算法都采用分代收集算法来收集,根据对象存活周期不同将内存划分为新生代和老 年代,再根据每个年代的特点采用最合适的回收算法。

新生代存活对象较少,每次垃圾回收都有大量对象死去,一般采用复制算法,只需要付出复制少量 存活对象的成本就可以实现垃圾回收;

老年代存活对象较多,没有额外空间进行分配担保,就必须采用标记清除算法和标记整理算法进行 回收;

3.2 内存泄漏

所谓内存泄露,指的是内存中存在的没有用的确无法回收的对象。表现的现象是会导致内存抖动,可用 内存减少,进而导致GC频繁、卡顿、OOM。

下面是一段模拟内存泄漏的代码:

/**
*
*
模拟内存泄露的Activity
/
public class MemoryLeakActivity extends AppCompatActivity implements
CallBack{ @Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.mipmap.splash);
imageView.setImageBitmap(bitmap);
// 添加静态类引用
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy()
{
super.onDestroy();
//
CallBackManager.removeCallBack(this);
}

@Override
public void dpOperate() {
// do sth
}

当我们使用Memory Profiler工具查看内存曲线,发现

如果想分析定位具体发生内存泄露位置,我们可以借助MAT工具。首先,使用MAT工具生成hprof文
击dump将当前内存信息转成hprof文件,需要对生成的文件转换成MAT可读取文件。执行一下 转换命令即可完件,点成转换,生成的文件位于Android/sdk/platorm-tools路径下。

hprof-conv 刚刚生成的hprof文件 memory-mat.hprof

使用mat打开刚刚转换的hprof文件,然后使用Android

然后点击面板的【Historygram】,搜索

然后,查看所有引用对象,并得到相关的引用链,如下图

可以看到GC Roots是CallBackManager

所以,我们在Activity销毁时将CallBackManager引用移除即可。

Override
protected void onDestroy()
{
super.onDestroy();
CallBackManager.removeCallBack(this);
}

当然,上面只是一个MAT分析工具使用的示例,其他的内存泄露都可以借助MAT分析工具解决。

3.3 大图内存优化

在Android开发中,经常会遇到加载大图导致内存泄露的问题,对于这种场景,有一个通用的解决方 案,即使用ARTHook对不合理图片进行检测。我们知道,获取Bitmap占用的内存主要有两种方式:

通过getByteCount方法,但是需要在运行时获取一个像素 图片所在资源目录压缩比所占内存

通过ARTHook方法可以优雅的获取不合理图片,侵入性低,
但是因为兼容性问题一般在线下使用。使用ARTHook需要安装以下依赖:
通过ARTHook方法可以优雅的获取不合理图片,侵入性低,但是因为兼容性问题一般在线下使用。使用ARTHook需要安装以下依赖

implementation 'me.weishu:epic:0.3.6'

然后自定义实现Hook方法,如下所示。

public class CheckBitmapHook extends XC_MethodHook {
@
Override protected void afterHookedMethod(MethodHookParam param) throws
Throwable {
super.afterHookedMethod(param);
ImageView imageView = (ImageView)param.thisObject;
checkBitmap(imageView,imageView.getDrawable());
}
private static void checkBitmap(Object o,Drawable drawable) { if(drawable 
instanceof BitmapDrawable && o instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); if(bitmap !=
null) {
final View view = (View)o; int width=view.getWidth();
int height =view.getHeight();
if(width > 0 && height > 0){
if(bitmap.getWidth() > (width <<1) && bitmap.getHeight()>
(height << 1)) {
large"));
warn(bitmap.getWidth(),bitmap.getHeight(),width,height, new
RuntimeException("Bitmap size is too
    }
}
else {
final Throwable stacktrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener()
{
@Override public boolean onPreDraw(){
    int w = view.getWidth(); int h = view.getHeight(); if(w > 0 && h > 0) {
    if (bitmap.getWidth() >= (w << 1) && bitmap.getHeight() >= (h <<1)) {
        warn(bitmap.getWidth(),
        bitmap.getHeight(), w, h, stacktrace);
    }
    view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
    }
});
       }
      }
    }
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int
viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size:
(").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
append("\n desired size:
(").append(viewWidth).append(',').append(viewHeight).append(')')
append("\n call stack trace:
n").append(Log.getStackTraceString(t)).append('\n').toString();
LogUtils.i(warnInfo);

最后,在Application初始化时注入Hook。

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook()
{
@Override protected void afterHookedMethod(MethodHookParam param) throws
Throwable {
super.afterHookedMethod(param);
DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap",
Bitmap.class,
new CheckBitmapHook());
}
});

3.4 线上监控

3.4.1 常规方案

方案一
在特定场景中获取当前占用内存大小,如果当前内存大小超过系统最大内存80%,对当前内存进行一次
Dump(Debug.dumpHprofData()),选择合适时间将hprof文件进行上传,然后通过MAT工具手动分析该文件。

缺点:
Dump文件比较大,和用户使用时间、对象树正相关。文件较大导致上传失败率较高,分析困难。

方案二
将LeakCannary带到线上,添加预设怀疑点,对怀疑点进行内存泄露监控,发现内存泄露回传到服务 端。

缺点:
通用性较低,需要预设怀疑点,对没有预设怀疑点的地方监控不到。

LeakCanary分析比较耗时、耗内存,有可能会发生OOM。

3.4.2 LeakCannary改造

改造主要涉及以下几点:
将需要预设怀疑点改为自动寻找怀疑点,自动将前内存中所占内存较大的对象类中设置怀疑点。
LeakCanary分析泄露链路比较慢,改造为只分析Retain size大的对象。
分析过程会OOM,是因为LeakCannary分析时会将分析对象全部加载到内存当中,我们可以记录 下分析对象的个数和占用大小,对分析对象进行裁剪,不全部加载到内存当中。

完成的改造步骤如下:

  1. 监控常规指标:待机内存、重点模块占用内存、OOM率
  2. 监控APP一个生命周期内和重点模块界面的生命周期内的GC次数、GC时间等
  3. 将定制的LeakCanary带到线上,自动化分析线上的内存泄露

4,网络优化

4.1 网络优化的影响

App的网络连接对于用户来说, 影响很多, 且多数情况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的几点:

流量 :App的流量消耗对用户来说是比较敏感的, 毕竟流量是花钱的嘛. 现在大部分人的手机上都有安装流量监控的工具App, 用来监控App的流量使用. 如果我们的App这方面没有控制好, 会给用户不好的使用体验。

电量 :电量相对于用户来说, 没有那么明显. 一般用户可能不会太注意. 但是如电量优化中的那样, 网络连接(radio)是对电量影响很大的一个因素. 所以我们也要加以注意。

用户等待 :也就是用户体验, 良好的用户体验, 才是我们留住用户的第一步. 如果App请求等待时间长, 会给用户网络卡, 应用反应慢的感觉, 如果有对比, 有替代品, 我们的App很可能就会被用户无情抛弃。

4.2 网络分析工具

网络分析可以借助的工具有Monit
or、代理工具等。

4.2.1 Network Monitor

Android Studio内置的Monitor工具提供了一个Network Monitor,可以帮助开发者进行网络分析,下面是一个典型的Network Monitor示意图。

Rx — R(ecive) 表示下行流量,即下载接收。
Tx — T(ransmit) 表示上行流量,即上传发送。
Network Monitor实时跟踪选定应用的数据请求情况。我们可以连上手机,选定调试应用进程, 然后在App上操作我们需要分析的页面请求。

4.2.2 代理工具

网络代理工具有两个作用,一个是截获网络请求响应包, 分析网络请求;另一个设置代理网络, 移动App 开发中一般用来做不同网络环境的测试, 例如Wifi/4G/3G/弱网等。
现在,可以使用的代理工具有很多, 诸如Wireshark,Fiddler, Charles等。

4.3 网络优化方案

对于网络优化来说,主要从两个方面进行着手进行优化:

  1. 减少活跃时间:减少网络数据获取的频次,从而就减少了radio的电量消耗以及控制电量使用。
  2. 压缩数据包的大小:压缩数据包可以减少流量消耗,也可以让每次请求更快,基于上面的方案,可以得到以下一些常见的解决方案:

4.3.1 接口设计

1,API设计
App与服务器之间的API设计要考虑网络请求的频次,资源的状态等。以便App可以以较少的请求来完成 业务需求和界面的展示。

例如, 注册登录. 正常会有两个API, 注册和登录, 但是设计API时我们应该给注册接口包含一个隐式的登录. 来避免App在注册后还得请求一次登录接口。

2,使用Gzip压缩
使用Gzip来压缩request和response, 减少传输数据量,从而减少流量消耗。使用Retrofit等网络请求框架 进 行 网络请求时,默认进行了Gzip的压缩。

3,使用Protocol Buffer
以前,我们传输数据使用的是XML, 后来使用JSON代替了XML, 很大程度上也是为了可读性和减少数据量。而在游戏开发中,为了保证数据的准确和及时性,Google推出了Protocol Buffer数据交换格式。

4,依据网络情况获取不同分辨率的图片我们使用淘宝或者京东的时候,会看到应用会根据网络情
况,获取不同分辨率的图片,避免流量的浪费 以及提升用户的体验。

4.3.2 合理使用网络缓存

适当的使用缓存, 不仅可以让我们的应用看起来更快, 也能避免一些不必要的流量消耗,带来更好的用户体验。

1,打包网络请求
当接口设计不能满足我们的业务需求时。例如,可能一个界面需要请求多个接口,或是网络良好,处于
Wifi状态下时我们想获取更多的数据等。这时就可以打包一些网络请求, 例如请求列表的同时, 获取
Header点击率较高的的item项的详情数据。

2,监听设备状态
为了提升用户体验,我们可以对设备的使用状态进行监听,然后再结合JobScheduler来执行网络请
求.。比方说Splash闪屏广告图片, 我们可以在连接到Wifi时下载缓存到本地; 新闻类的App可以在充电,Wifi状态下做离线缓存。

4.3.3 弱网测试&优化

1,弱网测试

Android Emulator

通常,我们创建和启动Android模拟器可以设置网络速度
然后,我们在启动时使用的emulator命令如下。

$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22

2.网络代理工具
使用网络代理工具也可以模拟网络情况。以Charles为例,保持手机和PC处于同一个局域网, 在手机端
wifi设置高级设置中设置代理方式为手动, 代理ip填写PC端ip地址, 端口号默认8888。有几种方式来模拟弱网进行测试:

5,耗电优化

事实上,如果我们的应用需要播放视频、需要获取 GPS 信息,亦或者是游戏应用,耗电都是比较严重的。如何判断哪些耗电是可以避免,或者是需要去优化的呢?我们可以打开手机自带的耗电排行榜,发 现“王者荣耀”使用了 7 个多小时,这时用户对“王者荣耀”的耗电是有预期的。

5.1 优化方向

假设这个时候发现某个应用他根本没怎么使用,但是耗电却非常多,那么就会被系统无情的杀掉。所以 耗电优化的第一个方向是优化应用的后台耗电。

知道了系统是如何计算耗电的,我们也就可以知道应用在后台不应该做什么,例如长时间获取WakeLock、WiFi 和蓝牙的扫描等,以及后台服务。为什么说耗电优化第一个方向就是优化应用后台耗电,因为大
部分厂商预装项目要求最严格的正是应用后台待机耗电。

当然前台耗电我们不会完全不管,但是标准会放松很多。再来看看下面这张图,如果系统对你的应用弹 出这个对话框,可能对于微信来说,用户还可以忍受,但是对其他大多数的应用来说,可能很多用户就 直接把你加入到后台限制的名单中了。

耗电优化的第二个方向是符合系统的规则,让系统认为你耗电是正常的。而 Android P 及以上版本是通过 Android Vitals 监控后台耗电,所以我们需要符合 Android Vitals 的规则,目前它的具体规则如下。
可以看到,Android系统目前比较关心是后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间WakeLock 阻止系统后台休眠,因为这些都有可能导致耗电问题。

5.2 耗电监控

5.2.1 Android Vitals

Android Vitals 的几个关于电量的监控方案与规则,可以帮助我们进行耗电监测。
Alarm Manager wakeup 唤醒过多频繁使用局部唤醒锁
后台网络使用量过高后台WiFi Scans过多

在使用了一段时间之后,我发现它并不是那么好用。以Alarm wakeup 为例,Vitals 以每小时超过 10 次作为规则。由于这个规则无法做修改,很多时候我们可能希望针对不同的系统版本做更加细致的区 分。其次跟 Battery Historian 一样,我们只能拿到 wakeup 的标记的组件,拿不到申请的堆栈,也拿不到当时手机是否在充电、剩余电量等信息。 下图是wakeup拿到的信息。


对于网络、WiFi scans 以及 WakeLock 也是如此。虽然Vitals 帮助我们缩小了排查的范围,但是依然没办法确认问题的具体原因。

5.3 如何监控耗电

前面说过,Android Vitals并不是那么好用,而且对于国内的应用来说其实也根本无法使用。那我们的耗电监控系统应该监控哪些内容,又应该如何做呢?首先,我们看一下耗电监控具体应该怎么做呢?

监控信息:简单来说系统关心什么,我们就监控什么,

而且应该以后台耗电监控为主。类似Alarm wakeup、WakeLock、WiFi scans、Network 都是必须的,其他的可以根据应用的实际情况。如果是地图应用,后台获取 GPS 是被允许的;如果是计步器应用,后台获取Sensor 也没有太大问题。

现场信息:监控系统希望可以获得完整的堆栈信息,比如哪一行代码发起了 WiFi scans、哪一行代码申请了WakeLock 等。还有当时手机是否在充电、手机的电量水平、应用前台和后台时间、CPU 状态等一些信息也可以帮助我们排查某些问题。

提炼规则:最后我们需要将监控的内容抽象成规则,当然不同应用监控的事项或者参数都不太一 样。 由于每个应用的具体情况都不太一样,可以用来参考的简单规则。

5.3.2 Hook方案

明确了我们需要监控什么以及具体的规则之后,接下来我们来看一下电量监控的技术方案。这里首先来 看一下Hook方案。Hook 方案的好处在于使用者接入非常简单,不需要去修改代码,接入的成本比较低。下面我以几个比较常用的规则为例,看看如何使用 Java Hook 达到监控的目的。

1,WakeLock

WakeLock 用来阻止 CPU、屏幕甚至是键盘的休眠。类似Alarm、JobService 也会申请 WakeLock 来完成后台CPU 操作。WakeLock 的核心控制代码都在PowerManagerService中,实现的方法非常简单,如下所示。

// 代 理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE),
"mService", this);
@Override
public void beforeInvoke(Method method, Object[] args) {
/ 申请 Wakelock
if (method.getName().equals("acquireWakeLock"))
if (isAppBackground()) {
/ 应用后台逻辑,获取应用堆栈等等
else {
// 应用前台逻辑,获取应用堆栈等等
{
    // 释放 Wakelock
}
else if (method.getName().equals("releaseWakeLock")) {
    // 释放的逻辑
    }
}

2,Alarm

Alarm 用来做一些定时的重复任务,它一共有四个类型,其中ELAPSED_REALTIME_WAKEUP和
RTC_WAKEUP类型都会唤醒设备。同样,Alarm 的核心控制逻辑都在AlarmManagerService中,实现如下。

// 代理 AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);
public void beforeInvoke(Method method, Object[] args) {
// 设置 Alarm
if (method.getName().equals("set")) {
// 不同版本参数类型的适配,获取应用堆栈等等
// 清除 Alarm
}
else if (method.getName().equals("remove")) {
// 清除的逻辑
}
}

除了WakeLock和Alarm外,对于后台 CPU,我们可以使用卡顿监控相关的方法;对于后台网络,同样我们可以通过网络监控相关的方法;对于 GPS 监控,我们可以通过Hook 代理LOCATION_SERVICE; 对于 Sensor,我们通 过Hook SENSOR_SERVICE 中 的 “mSensorListeners”,可以拿到部分信息。

最后,我们将申请资源到的堆栈信息保存起来。当我们触发某个规则上报问题的时候,可以将收集到的 堆栈信息、电池是否充电、CPU 信息、应用前后台时间等辅助信息上传到后台即可。

5.3.3 插桩法

使用 Hook 方式虽然简单,但是某些规则可能不太容易找到合适的 Hook 点,而且在 Android P 之后, 很多的 Hook 点都不支持了。出于兼容性考虑,我首先想到的是插桩法。以 WakeLock 为例:

public class WakelockMetrics {
/ Wakelock 申请
public void acquire(PowerManager.WakeLock wakelock)
/
{
wakeLock.acquire();
// 在这里增加 Wakelock 申请监控逻辑
}
/
/ Wakelock 释放
public void release(PowerManager.WakeLock wakelock, int flags)
{
    wakelock.release();
    // 在这里增加 Wakelock 释放监控逻辑
    }
}

如果你对电量消耗又研究,那么肯定知道Facebook的耗电监控的开源库Battery-Metrics,它监控的数据非常全,包括 Alarm、WakeLock、Camera、CPU、Network 等,而且也有收集电量充电状态、电量水平等信息。不过,遗憾的是Battery-Metrics只是提供了一系列的基础类,在实际使用时开发者仍然需要修改大量的源码。

6. 安装包优化

现在市面上的App,小则几十M,大则上百M。安装包越小,下载时省流量,用户好的体验,下载更 快,安装更快。那么对于安装包,我们可以从哪些方面着手进行优化呢?

6.1 常用的优化策略

1,清理无用资源

在android打包过程中,如果代码有涉及资源和代码的引用,那么就会打包到App中,为了防止将这些 废弃的代码和资源打包到App中,我们需要及时地清理这些无用的代码和资源来减小App的体积。清理 的方法是,依次点击android Studio 的 【 Refactor 】 -> 【 Remove unusedResource】,如下图所示。

Lint工具还是很有用的,它给我们需要优化的点:
检测没有用的
布局并且删除
把未使用到的
资源删除
建议String.xml有一些没有用到的字符也删除掉

2,开启shrinkResources去除无用资源

在build.gradle 里面配置shrinkResources true,在打包的时候会自动清除掉无用的资源,但经过实验发现打出的包并不会,而是会把部分无用资源用更小的东西代替掉。

注意,这里的“无用”是指调用图片 的所有父级函数最终是废弃代码,而shrinkResources true 只能去除没有任何父函数调用的情况。

android {
buildTypes {
    release {
    shrinkResources true
    }
   }
}

除此之外,大部分应用其实并不需要支持几十种语言的国际化支持,还可以删除语言支持文件。

6.2 资源压缩

在android开发中,内置的图片是很多的,这些图片占用了大量的体积,因此为了缩小包的体积,我们 可以对资源进行压缩。常用的方法有:

  1. 使用压缩过的图片:使用压缩过的图片,可以有效降低App的体积。

  2. 只用一套图片:对于绝大对数APP来说,只需要取一套设计图就足够了。

  3. 使用不带alpha值的jpg图片:对于非透明的大图,jpg将会比png的大小有显著的优势,虽然不是
    绝对的,但是通常会减小到一半都不止。

  4. 使用tinypng有损压缩:支持上传PNG图片到官网上压缩,然后下载保存,在保持alpha通道的情
    下对PNG的压缩可以达到1/3之内,而且用肉眼基本上分辨不出压缩的损失。

  5. 使用webp格式:webp支持透明度,压缩比比,占用的体积比JPG图片更小。从Android
    4.0+开始原生支持,但是不支持包含透明度,直到Android4.2.1+才支持显示含透明度的webp,使用时候要特别注意。

  6. 使用svg:矢量图是由点与线组成,和位图不一样,它再放大也能保持清晰度,而且使用矢量图比位 图
    设计方案能节约30~40%的空间。

  7. 对打包后的图片进行压缩:使用7zip压缩方式对图片进行压缩,可以直接使用微信开源的
    AndResGuard压缩方案

apply plugin: 'AndResGuard'
buildscript {
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.1.7'
}
}
andResGuard {
mappingFile = null
use7zip = true useSign
=
/
/
true keepRoot = false
/ add <your_application_id>.R.drawable.icon into whitelist.
/ because the launcher will get thgge icon with his name def
packageName = <your_application_id>
whiteList = [
/
/for your icon
packageName + ".R.drawable.icon",/for fabric
packageName + ".R.string.com.crashlytics.*",
/for umeng update
/
/
packageName + ".R.string.umeng*",
packageName + ".R.string.UM*",
packageName + ".R.string.tb_*",
packageName + ".R.layout.umeng*",
packageName + ".R.layout.tb_*",
packageName + ".R.drawable.umeng*",
packageName + ".R.drawable.tb_*",
packageName + ".R.anim.umeng*",
packageName + ".R.color.umeng*",
packageName + ".R.color.tb_*",
packageName + ".R.style.*UM*",
packageName + ".R.style.umeng*",
packageName + ".R.id.umeng*"
]
compressFilePattern = [ "*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"
resources.arsc"
]
sevenzip {
    artifact = 'com.tencent.mm:SevenZip:1.1.7'
    //path = "/usr/local/bin/7za"
    }
}

6.3资源动态加载

在前端开发中,动态加载资源可以有效减小apk的体积。除此之外,只提供对主流架构的支持,比如
arm,对于mips和x86架构可以考虑不支持,这样可以大大减小APK的体积。

当然,除了上面提到的场景的优化场景外,Android App的优化还包括存储优化、多线程优化以及奔溃处理等方面。

7,企业常见性能优化面试题集

7.1 谈谈你对Android性能优化方面的了解?

启动优化: application中不要做大量耗时操作,如果必须的话,建议异步做耗时操作
布局优化:使用合理的控件选择,少嵌套。(合理使用include,merge,viewStub等使用)
apk优化(资源文件优化,代码优化,lint检查,.9.png,合理使用shape替代图片,webp等)
性能优化,网络优化,电量优化 避免轮询,尽量使用推送。
应用处于后台时,禁用某些数据传输限制访问频率,失败后不要无限重连选用合适的定位服务(GPS定位,网络定位,被动定 位)
使用缓存
startActivityForResult替代发送广播内存优化
循环尽量不使用局部变量
避免在onDraw中创建对象,onDraw会被频繁调用,容易造成内存抖动。循环中创建大的对象,也是
如此。

不用的对象及时释放数据库的cursor及时关闭
adapter使用缓存注册广播后,在生命周期结束时反注册

及时关闭流操作
图片尽量使用软引用,较大的图片可以通过
bitmapFactory缩放后再使用,并及时recycler。另外加载巨图时不要 使用setImageBitmap或setImageResourse或BitmapFactory.decodeResource,这些方法拿到的都是
bitmap的对象,占用内存较大。可以用BitmapFactory.decodeStream方法配合BitmapFactory.Options进行缩放

避免static成员变量引用资源耗费过多实例避免静态内部类的引用

7.2 一般什么情况下会导致内存泄漏问题?

  1. 资源对象没关闭造成的内存泄漏(如 : Cursor、File 等)

  2. ListView 的 Adapter 中没有使用缓存的ConvertView

  3. Bitmap对象不在使用时调用recycle()释放内存

  4. 集合中对象没清理造成的内存泄漏(特 别 是 static 修饰的集合)

  5. 接收器、监听器注册没取消造成的内存泄漏

  6. Activity 的 Context 造成的泄漏,可以使用ApplicationContext

  7. 造成的内存泄漏问题(一 般 由 于 Handler生命周期比其外部类的生命周期长引起的)

7.3 自定义 Handler 时如何有效地避免内存泄漏问题?

  1. 自定义的静态handler
  2. 可以加一个弱引用
  3. 还有一个主意的就是当你activity被销毁的时候如果还有消息没有发出去 就remove掉吧
  4. removecallbacksandmessages去清除Message和Runnable 加null 写在生命周的ondestroy()就行

7.4 哪些情况下会导致oom问题?

1.例如handler 在界面销毁的时候消息还未发送
2.file没有关流
3.查询到结果还没有停止
4.内部类持有外部类引用得不到释放
5.不用的对象最好null 让GC回收一下
6.图片资源什么的最好加一个软引用

7.5 ANR 出现的场景以及解决方案?

在Android中,应用的响应性被活动管理器(ActivityManager)和窗口管理器(Window Manager)这两个系统服务所监视。当用户触发了输入事件(如键盘输入,点 击按钮等),如果应用5秒内没有响应用户的输入事件,那 么,Android会认为该应用无响应,便弹出ANR对话框。而弹出ANR异常,也主要是为了提升用户体验。

解决方案是对于耗时的操作,比如访问网络、访问数据库等操作,需要开辟子线程,在子线程处理耗时的操作,主线程主要实现UI的操作

7.6 谈谈Android中内存优化的方式?

关于内存泄漏,一般像单例模式的使用不当啊、集合的操 作不当啊、资源的缺乏有效的回收机制啊、Handler、线程 的使用不当等等都有可能引发内存泄漏。

  1. 单例模式引发的内存泄漏:
    原因:单例模式里的静态实例持有对象的引用,导致对象无法被回收,常见为持有Activity的引用
    优化:改为持有Application的引用,或者不持有使用的时候传递。

  2. 集合操作不当引发的内存泄漏:
    原因:集合只增不减
    优化:有对应的删除或卸载操作

  3. 线程的操作不当引发的内存泄漏:
    原因:线程持有对象的引用在后台执行,与对象的生命周期不一致
    优化:静态实例+弱引用(WeakReference)方式,使其生命周期一致

  4. 匿名内部类/非静态内部类操作不当引发的内存泄漏:
    原因:内部类持有对象引用,导致无法释放,比如各种回调
    优化:保持生命周期一致,改为静态实例+对象的弱引用方式(WeakReference)

  5. 常用的资源未关闭回收引发的内存泄漏:
    原因:BroadcastReceiver,File,Cursor,IO流,Bitmap等资源使用未关闭
    优化:使用后有对应的关闭和卸载机制

  6. Handler使用不当造成的内存泄漏:
    原因:Handler持有Activity的引用,其发送的Message 中持有Handler的引用,当队列处理Message的时间过长会导致Handler无法被回收
    优化:静态实例+弱引用(WeakReference)方式内存溢出:
    原因:

  7. 内存泄漏长时间的积累

  8. 业务操作使用超大内存
    优化:

  9. 调整图像大小后再放入内存、及时回收

  10. 不要过多的创建静态变量

7.7 谈谈布局优化的技巧?

1、降低Overdraw(过度绘制),减少不必要的背景绘制
2、减少嵌套层次及控件个数

3、使用Canvas的clipRect和clipPath方法限制View的绘制区域
4、通过imageDrawable方法进行设置避免ImageView的
background和imageDrawable重叠

5、借助ViewStub按需延迟加载
6、选择合适的布局类型
7、熟悉API尽量借助系统现有的属性来实现一些UI效果

7.8 Android 中的图片优化方案?

  1. 首先我们可以对图片进行二次采样,从本质上减少图片的内存占用。就是将大图片缩小之后放入到内存中,以实现减小内存的目的

  2. 其次就是采用三层缓存架构,提高图片的访问速度。三层缓存架构是内存-文件-网络。
    内存是访问速度最快的部分但是分配的空间有限,所以不可能占用太多。其中内存缓存可以采用LRU算
    法(最近最少使用算法),来确定要删除内存中的那些图片,保存那些图片。

文件就是将图片保存到本地,可以使SD卡中,也可以是手机内部存储中。
网络就是访问网络下载图片,进行图片的加载。

  1. 常见的png,JPG,webp等格式的图片在设置到UI上之前需要经过解码过程,而图片采用不同的码率,也会造成对 内存的占用不同。

  2. 最后一点,也是图片优化最重要的一点。重用Bitmap.

  3. 不使用bitmap要记得实时回收,减小内存的开销

7.9 Android Native Crash问题如何分析定位?

1.利用breakpad,dump Native崩溃时日志信息,
2.利用addr2line跟ndk-strace等工具 根据崩溃日志偏移量定位具体源码位置
3.根据信号提示进行具体处理。

此步骤的难点在于1:
Native Crash的时候整个app的状态是极其不稳定的,很可能由于保存日志(或者上报日志)等操作引起其他异常,所以此时最好fork一个子进程来保存当前进程的日志。

7.10 谈谈怎么给apk瘦身?

第1条:使用一套资源
这是最基本的一条规则,但非常重要。
对于绝大对数APP来说,只需要取一套设计图就足够了。鉴于现在分辨率的趋势,建议取720p的资源,放到xhdpi 目录。

相对于多套资源,只使用720P的一套资源,在视觉上差别不大,很多大公司的产品也是如此,但却能显著的减少资源占用大小,顺便也能减轻设计师的出图工作量了。
注意,这里不是说把不是xhdpi的目录都删除,而是强调保留一套设计资源就够了。

第2条:开启minifyEnabled混淆代码
在gradle使用minifyEnabled进行Proguard混淆的配置, 可大大减小APP大小:

android {
buildTypes {
release {
minifyEnabled true
}
}
}

在proguard中,是否保留符号表对APP的大小是有显著的影响的,可酌情不保留,但是建议尽量保留用于调试。 详细proguard的相关的配置和原理可自行查阅。

第3条:开启shrinkResources去除无用资源
在gradle使用shrinkResources去除无用资源,效果非常 好。

android {
buildTypes {
release {
shrinkResources true
}
}
}

第4条:删除无用的语言资源

大部分应用其实并不需要支持几十种语言的国际化支持。还好强大的gradle支持语言的配置,比如国内应用只支持 中文:

android {
defaultConfig
{
"
resConfigs
zh"
}

第5条:使用tinypng有损压缩
android打包本身会对png进行无损压缩,所以使用像tinypng这样的有损压缩是有必要的。

重点是Tinypng使用智能有损压缩技术,以尽量少的失真换 来图片大小的锐减,效果非常好,强烈推荐。
Tinypng的官方网站:http://tinypng.com/

第6条:使用jpg格式
如果对于非透明的大图,jpg将会比png的大小有显著的优势,虽然不是绝对的,但是通常会减小到一半都不止。

在启动页,活动页等之类的大图展示区采用jpg将是非常明智的选择。

第7条:使用webp格式
webp支持透明度,压缩比比jpg更高但显示效果却不输于jpg,官方评测quality参数等于75均衡最佳。
相对于jpg、png,webp作为一种新的图片格式,限于android的支持情况暂时还没用在手机端广泛应用起来。从Android4.0+开始原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。
官方介绍

第8条:缩小大图
如果经过上述步骤之后,你的工程里面还有一些大图,考虑是否有必要维持这样的大尺寸,是否能适当的缩小。 事实上,由于设计师出图的原因,我们拿到的很多图片完全可
以适当的缩小而对视觉影响是极小的。

第9条:覆盖第三库里的大图
有些第三库里引用了一些大图但是实际上并不会被我们用到,就可以考虑用1x1的透明图片覆盖。
你可能会有点不舒服,因为你的drawable下竟然包含了一 些莫名其妙的名称的1x1图片…

第10条:删除armable-v7包下的so
基本上armable的so也是兼容armable-v7的,armable-v7a的库会对图形渲染方面有很大的改进,如果没有这方面的要求,可以精简。
这里不排除有极少数设备会Crash,可能和不同的so有一定 的关系,请大家务必测试周全后再发布。

第11条:删除x86包下的so

与第十条不同的是,x86包下的so在x86型号的手机是需要的,如果产品没用这方面的要求也可以精简。
建议实际工作的配置是只保留armable、x86下的so文件, 算是一个折中的方案。

第12条:使用微信资源压缩打包工具
微信资源压缩打包工具通过短资源名称,采用7zip对APP 进行极致压缩实现减小APP的目标,效果非常的好,强烈推荐。
详情参考:Android资源混淆工具使用说明

原理介绍:安装包立减1M–微信Android资源混淆打包工具 建议开启7zip,注意白名单的配置,否则会导致有些资源找不到,官方已经发布AndResGuard到gradle中了,非常方便:

apply plugin: 'AndResGuard'
buildscript {
dependencies
{
h
classpat
'
com.tencent.mm:AndResGuard-gradle-
plugin:1.1.7'
}
andResGuard {
mappingFile = null
use7zip = true
useSign = true
keepRoot = false
/ add
your_application_id>.R.drawable.icon into whitelist.
/
<
/
/ because the launcher will get thgge icon with
his name
def packageName = <your_application_id>
whiteList = [
//for your icon
packageName + ".R.drawable.icon",
//for fabric
packageName +
".R.string.com.crashlytics.*",
//for umeng update
packageName + ".R.string.umeng*",
packageName + ".R.string.UM*",
packageName + ".R.string.tb_*",
packageName + ".R.layout.umeng*",
packageName + ".R.layout.tb_*",
packageName + ".R.drawable.umeng*",
packageName + ".R.drawable.tb_*",
packageName + ".R.anim.umeng*",
packageName + ".R.color.umeng*",
packageName + ".R.color.tb_*",
packageName + ".R.style.*UM*",
packageName + ".R.style.umeng*",
packageName + ".R.id.umeng*"
]
compressFilePattern = [ "*.png",
"*.jpg",
"*.jpeg",
"*.gif",
resources.arsc"
]
sevenzip {
artifact =
com.tencent.mm:SevenZip:1.1.7'
/path = "/usr/local/bin/7za"
'
}
/
}

会生成一个andresguard/resguard的Task,自动读取release签名进行重新混淆打包。

第13条:使用provided编译
对于一些库是按照需要动态的加载,可能在某些版本并不需要,但是代码又不方便去除否则会编译不过。
使用provided可以保证代码编译通过,但是实际打包中并 不引用此第三方库,实现了控制APP大小的目标。但是也同时就需要开发者自己判断不引用这个第三方库时就不要执行到相关的代码,避免APP崩溃。

第14条:使用shape背景
特别是在扁平化盛行的当下,很多纯色的渐变的圆角的图片都可以用shape实现,代码灵活可控,省去了大量的背景图片。

第15条:使用着色方案
相信你的工程里也有很多selector文件,也有很多相似的图片只是颜色不同,通过着色方案我们能大大减轻这样的工 作量,减少这样的文件。

借助于android support库可实现一个全版本兼容的着色方案,参考代码:DrawableLess.java

第16条:在线化素材库
如果你的APP支持素材库(比如聊天表情库)的话,考虑在线 加载模式,因为往往素材库都有不小的体积。这一步需要开发者实现在线加载,一方面增加代码的复杂度,一方面提高了APP的流量消耗,建议酌情选择。

第17条:避免重复库
避免重复库看上去是理所当然的,但是秘密总是藏的很深,一定要当心你引用的第三方库又引用了哪个第三方库,这就很容易出现功能重复的库了,比如使用了两个图片加载库:Glide和Picasso。
通过查看exploded-aar目录和External Libraries或者反编译生成的APK,尽量避免重复库的大小,减小APP大小。

第18条:使用更小的库
同样功能的库在大小上是不同的,甚至会悬殊很大。如果并无对某个库特别需求而又对APP大小有严格要求的话,比较这些相同功能第三方库的大小,选择更小的库会减小APP大小。

第19条:支持插件化

过去的一年,插件化技术雨后春笋一样的都冒了出来,这些技术支持动态的加载代码和动态的加载资源,把APP的一部分分离出来了,对于业务庞大的项目来说非常有用,极大的分解了APP大小。

因为插件化技术需要一定的技术保障和服务端系统支持,有一定的风险,如无必要(比如一些小型项目,也没什么扩展业务)就不需要了,建议酌情选择。

第20条:精简功能业务
这条完全取决于业务需求。从统计数据分析砍掉一些没用的功能是完全有可能的,甚至干脆去掉一些花哨的功能出个轻聊版、极速版也不是不可以的。

第21条:重复执行第1到20条
多次执行上述步骤,你总能发现一些蛛丝马迹,这是一个好消息,不是吗?

第22条:Facebook的redex优化字节码
redex是facebook发布的一款android字节码的优化工具, 需要按照说明文档自行配置一下。

redex input.apk -o output.apk --sign -s
<
KEYSTORE> -a <KEYALIAS> -p <KEYPASS>

7.11 谈谈你是如何优化App启动过程的?

  1. 把application oncreate 中要执行的方法 分为同步和异步,尽量去延迟执行 或者使用空闲线程去初始化一些方法
  2. 配置一个启动背景,避免白屏或者黑屏,然后做一个空的Activity这个Activity只做一件事,就是跳转到真的Activity,因为 启动速度 和applicationoncreate的耗时和第一个
    Activity的绘制有关,上面都是easy的做法
  3. 利用 redex 工具 优化 dex ,因为 class字节码 分布在不同的dex中,所以启动的时候必须逐个查找一些文件,他们 散列分布在不同的dex中,查找起来耗时又不方便,利用redex把相关的class放在同一个dex包下,避免 同一个dex包被多次查找
  4. 在attachedbaseContext中 新起一个进程 去加载mutildex 可以加速App启动页的打开(可能在启动页中会等待,但是加速了从launcher到启动页的速度)

7.12 谈谈代码混淆的步骤?

开启混淆

  1. 查看项目使用的第三方哪些需要设置混淆
  2. 保持反射native对应的类和方法不混淆
  3. 保持自定义类不混淆
  4. 保持实体类参与序列化的不混淆
  5. 系统组件等一些固定方法会被系统调用的不混淆

7.13 谈谈App的电量优化?

优化方案总结
(1)GPS
— 使用要谨慎,如精确度不高可用WiFi定位或者基站定
位,可用;非要用的话,注意定位数据的复用和定位频率的
阈值
(2)Process和Service
— 按需启动,用完就退出
(3)网络数据交互
— 减少网络网络请求次数和数据量;WiFi比手机网络省

(4)CPU
— 减少I/O操作(包括数据库操作),减少大量的计算
(5)减少手机硬件交互
— 使用频率优化和选择低功耗模式

7.14 谈谈如何对WebView进行优化?

  1. 单/多进程化:webView在独立的进程里面,那么WebView的进程崩溃不会影响到主进程运行;同时WebView的安 全漏洞也很难影响到主进程;如果是多进程的话,可以使用WebView的容器池,有二次秒开的作用; 不过缺点就是需要你做好和WebView的跨进程通讯了

  2. 网络优化:我们可以让WebView的host和客户端的host 保持一致,那么就达到复用DNS缓存的效果;如果客户端有针对网络请求进行了优化,那么可以让WebView的全部网络请求托管给客户端

  3. H5离线包:这个是手Q的H5方案之一,让客户端提前去 下载离线的H5数据包,WebView只需要加载本地H5数据包即可,这么做不仅可以避免一些http的劫持,而且跳过了WebView的建立TCP连接和H5、CCS等数据下载的过程,直接开始UI渲染,大大提高了WebView的效率

7.15 如何处理大图的加载?

1、首先确定大图的用途,精度需求:
a)完整显示,对精度要求不高,图片本身就很大b)
对精度需求比较高,不需要完整显示
2、解决方案
a)针对第一种的处理图片本身,按需加载(根据显示设备本
身大小进行缩放),降低精度加载(改变图片模式,如将ARGB8888改成RGB565,ARGB4444),修改图片格式(png改成webp,jpg)
b)第二种的一般采用局部加载,主要要用到的是
BitmapRegionDecoder这个类decodeRegion的方法,读取图片指定大小的数据,然后通过移动来动态改变显示区域的图片

7.16 谈谈如何对网络请求进行优化?

  1. 最开始的是DNS,当我们发起一个网络请求,首先要经过DNS服务,将域名转化为IP地址,为避免DNS解析异常问题,可以直接使用 IP 建立连接;

  2. 使用 Gzip 压缩 Response 减少数据传输量;使用Protocol Buffer 代替 JSON;

  3. 请求图片的 url 中可以添加 格式、质量、宽高等参数;使用缩略图、使用 WebP格式图片,大图分片传输;

  4. 使用网络缓存,使用图片加载框架;

  5. 监听设备网络状态,根据不同网络状态选择对应情况下 的网络请求策略:网络良好和弱网、离线等情况下分别设计不同的请求策略,比如 WIFI 下一个请求可以获取几十个数据,甚至可以一次性执行多个请求;而弱网下一个请求获取几个数据,且文本类型优先,富文本其次,除文本数据外其它类型的数据一开始只显示占位符;离线下事先保存请求数据到磁盘,在离线时从磁盘加载数据。

7.17 请谈谈如何加载Bitmap并防止内存溢出?

首先我们 要知道bitmap内存是怎么计算的例子:
手机屏幕大小 1080 x 1920(inTarget =420),加载xhdpi(=inDensity = 320)中的图片 1920 x 1080,scale420 /320,
最总我们可以得知 他的占用内存 1418 * 2520 * 4 很明显 被放大了。防止内存溢出:

  1. 对图片进行内存压缩;
  2. 高分辨率的图片放入对应文件夹;
  3. 内存复用
  4. 及时回收

Android 中的 IPC

5.1 请回答一下Android进程间的通信方式?

5.2 请谈谈你对Binder机制的理解?

Binder机制:

  1. 为了保证进程空间不被其他进程破坏或干扰,Linux中的 进程是相互独立或相互隔离的。
  2. 进程空间分为用户空间和内核空间。用户空间不可以进行数据交互;内核空间可以进行数据交互,所有进程共用一个内核空间。
  3. Binder机制相对于Linux内传统的进程间通信方式:
    1)性能更好;Binder机制只需要拷贝数据一次,管道、 消息队列、Socket等都需要拷贝数据两次;而共享内存虽然不需要拷贝,但实现复杂度高。
    2)安全性更高;
    Binder机制通过UID/PID在内核空间添加了身份标识,安全性更高。
  4. Binder跨进程通信机制:基于C/S架构,由Client、Server、ServerManager和Binder驱动组成。
  5. Binder驱动实现的原理:通过内存映射,即系统调用了mmap()函数。
  6. Server Manager的作用:管理Service的注册和查询。
  7. Binder驱动的作用:(1)传递进程间的数据,通过系统 调用mmap()函数;(2)实现线程的控制,通过Binder 驱动的线程池,并由Binder驱动自身进行管理。
  8. Server进程会创建很多线程处理Binder请求,这些线程采用Binder驱动的线程池,由Binder驱动自身进行管理。一个进程的Binder线程池默认最大是16个,超过的请求会 阻塞等待空闲的线程。
  9. Android中进行进程间通信主要通过Binder类(已经实现了IBinder接口),即具备了跨进程通信的能力。

5.3 谈谈 AIDL?

AIDL是一种辅助工具,不用AIDL ,一样可以实现跨进程通讯AIDL 的原理是binder,真正有跨进程通讯能力的也是Binder,所 以 AIDL 只是一个能帮你少写代码,少出错的辅助工具,由于设计的太好,使用太方便,所以非常常用就像 retrofit和okhttp关系一样,retrofit提供 更加友好的api,真正的网络请求还是由 okhttp发起的

Android 系统 SDK 相关

6.1 请简要谈谈Android系统的架构组成?

android系统分为四部分,从高到低分别是:
1、Android应用层
Android会同一系列核心应用程序包一起发布,该应用程序 包包括email客户端,SMS短消息程序,日历,地图,浏览 器,联系人管理程序等。所有的应用程序都是使用JAVA语 言编写的。
2、Android应用框架层
开发人员也可以完全访问核心应用程序所使用的API框架。该应用程序的架构设计简化了组件的重用;任何一个应用程序都可以发布它的功能块并且任何其它的应用程序都可以使用其所发布的功能块(不过得遵循框架的安全性限制)。同样,该应用程序重用机制也使用户可以方便的替换程序组件。
3、Android系统运行层
Android 包含一些C/C++库,这些库能被Android系统中不同的组件使用。它们通过 Android 应用程序框架为开发者提供服务。
4、Linux内核层
Android 的核心系统服务依赖于 Linux 2.6 内核,如安全性,内存管理,进程管理, 网络协议栈和驱动模型。Linux 内核也同时作为硬件和软件栈之间的抽象层。

6.2 SharedPreferences 是线程安全的吗?

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

6.3 Serializable和Parcelable的区别?

Android中序列化有两种方式:Serializable以及Parcelable。其中Serializable是Java自带的,而Parcelable是安卓专有的。
Seralizable相对Parcelable而言,好处就是非常简单,只 需对需要序列化的类class执行就可以,不需要手动去处理 序列化和反序列化的过程,所以常常用于网络请求数据处理,Activity之间传递值的使用。

Parcelable是android特有的序列化API,它的出现是为了解决Serializable在序列化的过程中消耗资源严重的问题, 但是因为本身使用需要手动处理序列化和反序列化过程, 会与具体的代码绑定,使用较为繁琐,一般只获取内存数据的时候使用。

6.4 请简述一下 Android 7.0 的新特性?

  1. 低电耗功能改进
  2. 引入画中画功能
  3. 引入“长按快捷方式”,即App Shortcuts
  4. 引入混合模式,同时存在解释执行/AOT/JIT,安装应用时默认不全量编译,使得安装应用时间大大缩短
  5. 引入了对私有平台库限制,然而用一个叫做Nougat_dlfunctions的库就行
  6. 不推荐使用file:// URI传递数据,转而推荐使用FileProvider
  7. 快速回复通知

6.5 谈谈ArrayMap和HashMap的区别?

1.查找效率
HashMap因为其根据hashcode的值直接算出index,所以其 查找效率是随着数组长度增大而增加的。
ArrayMap使用的是二分法查找,所以当数组长度每增加一 倍时,就需要多进行一次判断,效率下降

2.扩容数量
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。

ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申请4 个。
这样比较ArrayMap其实是申请了更少的内存空间,但 是扩容的频率会更高。因此,如果数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。

3.扩 容 效 率
HashMap每次扩容的时候重新计算每个数组成员的位置, 然后 放 到 新 的 位 置 。
ArrayMap则是直接使用System.arraycopy,所以效率上 肯定是ArrayMap更占优势。

4.内存消耗
以ArrayMap采用了一种独特的方式,能够重复的利用因为 数据扩容而遗留下来的数组空间,方便下一个ArrayMap的 使用。而HashMap没有这种设计。 由于ArrayMap之缓存了长度是
4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的是节省内 存的。

5.总结
综上所述,数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。 而数据量比较大的时候,则推荐使用HashMap。

6.6 简要说说 LruCache 的原理?

androidx.collection.LruCache 初始化的时候, 会限制缓存占据内存空间的总容量
maxSize;
底层维护的是 LinkedHashMap, 使用 LruCache 最好要重写 sizeOf 方法, 用于计算每个被缓存的对象, 在内存中存储时, 占用多少空间;
在 put 操作时, 首先计算新的缓存对象, 占多少空间, 再根据key, 移除老的对象,
占用内存大小 = 之前占用的内存大小 + 新对象的大小 -老对 象 的 大 小 ;
put 操作最后总会根据 maxSize, 在拿到LinkedHashMap.EntrySet中链表的头节点, 循环判断, 只要当前缓存对象占据内存超出 maxSize,就移除一个头节点, 一直到符合要求;
lruCache 和 LinkedHashMap 的 关 系 :

LinkedHashMap中维护一个双向链表, 并维护 head 和tail指针, lruCache 使用了 LinkedHashMap accessOrder 为 true的属性, 只要访问了某个 key,包括 get 和 put, 就把当前这个 entry 放在链表的位节点,所以链表的头节点, 是最老访问的节点, 尾节点是最新访问的节点,

所以, lruCache 就很巧妙的利用了这个特点, 完成了 LeastRecently Used 的需求;

6.7 为什么推荐用SparseArray代替

HashMap?
并不能替换所有的HashMap。只能替换以int类型为key的HashMap。
HashMap中如果以int为key,会强制使用Integer这个包装类型,当我们使用int类型作为key的时候系统会自用装箱成为Integer,这个过程会创建对象一想效率。SparseArray内部是一个int数组和一个object数组。可以直 接使用int减少了自动装箱操作。

6.8 PathClassLoader和DexClassLoader有何区别?

根据art源码来看,两者都继承自BaseDexClassLoader, 最终都会创建一个DexFile,不同点是一个关键的参数:

optimizedDirectory,PathClassLoader 为 null,
DexClassLoader则使用传递进来的
然后会根据optimizedDirectory判断对应的oat文件是否已经生成(null则使用/data/dalvik-cache/),如果有且该oat对应的dex正确则直接加载,否则触发dex2oat(就是这家伙耗了
我们宝贵的时间!!),成功则用生成的oat, 失 败 则 走解 释 执 行

ps:貌似Q版做了优化,不会再卡死在dex2oat里了根据加载外部dex的实验,DexClassLoader会触发
dex2oat,而PathClassLoader不会

6.9 说说HttpClient与HttpUrlConnection的区别?并谈谈为何前者会被替代?

1,android2.3之前,HttpUrlConnection具有一些bug, 例 如关 闭 输 入 流 时 可 能 导 致 连 接 池 关 闭 。
2,android2.3之后,HttpUrlConnection才相对成熟。特 点 是 ,轻 量 、 api 少
3,HttpClient一直很强大,支持get、post、delete等其他 协议。
具体可参考表格:
https://www.cnblogs.com/spec-dog/p/3792616.html
被替代原因:
太重了,api太多;针对Android系统,不便于向后维护

6.10 什么是Lifecycle?请分析其内部原理和使用场景?

Jetpack 的 Lifecycle 库:它可以有效的避免内存泄漏,解决Android 生命周期的常见难题。

内部原理:ComponentActivity 的 onCreate 方法中注入了ReportFragment,通过 Fragment 来实现生命周期监听。

使用场景:给 RecyclerView 的 ViewHolder 添加 Lifecycle 的能力。自己实现 LifecycleHandler,在 Activity 销毁的时候,自动移除 Handler 的消息避免 Handler导致的内存泄漏。

6.11 谈一谈Android的签名机制?
当面试题要考察你某个技术的理解时,除了他们两个在技术上的区别和特点之外。更希望听到的是在业务上的应 用,
技术是为业务服务的,展现你的业务能力远比展现你的技术能力要重要。
现在我将从技术和业务应用两个方面来剖析android v1和v2签名的区别。
关于v1和v2两种签名的技术上的区别上面 已经有很详细的回答了,那么我就说一说他们在业务应用方面的区别: 我们有时候做app推广,需要记录这个APP是通过谁的推广 链接安装的,那么我我们只需要在每次通过服务器下发apk 的时候对apk文件动一些手脚,在apk中标示这个apk是哪 个用户分享的,然后当apk安装好打开时就可以读到apk事 先存储好的标示,从而实现我们需要的业务需求。给apk文件动手脚,v1支持,v1+v2的签名方式就不支持了。

6.12 谈谈安卓apk构建的流程?

  1. 使用aapt处理资源文件,如编译AndroidManifest.xml,编译生成resources.arsc,生成R.java等
  2. 使用javac等工具编译java文件,生成class格式文件
  3. 使用dx等工具将.class和项目依赖的jar编译成.dex
  4. 将生成的这些文件压缩进一个zip中
  5. 签名
    这只是最简单的过程,实际还会涉及到multidex,使用如proguard的工具处理生成的字节码,需要依赖aar文件, 需要编译kotlin,使用jack,使用jni,使用d8/r8等情况~

6.13 简述一下Android 8.0、9.0 分别增加了哪些新特性?

10.0
ExternalStrorage文件沙盒后台限制启动activity

9.0
刘海模式,手机可以直接设计刘海模式 夜间模式
默认使用https 非 SDK 接口的限制
全面屏
后台应用:
您的应用不能访问麦克风或摄像头。
使用连续报告模式的传感器(例如加速度计和陀螺仪)不会接收事件。
使用变化或一次性报告模式的传感器不会接收事件。
如果您的应用需要在运行 Android 9 的设备上检测传感器事件,请使用前台服务。

电话信息现在依赖设备位置设置 如果用户在运行Android 9 的设备上停用设备定位,则以下函数不提供
结果:

TelephonyManager.getAllCellInfo()
TelephonyManager.listen()
TelephonyManager.getCellLocation()
TelephonyManager.getNeighboringCellInfo()
Build.SERIAL始终设置为 "UNKNOWN" 以保护用户的
隐私,如果您的应用需要访问设备的硬件序列号, 您应
改为请求 READ_PHONE_STATE权限,然后调用
getSerial()。
多进程 webview 信息访问限制
对使用非 SDK 接口的限
制:NoSuchMethodError/NoSuchFieldException
if (Build.VERSION.SDK_INT>=
Build.VERSION_CODES.P){
/
/ Android P or above
else {
}
//below Android P
}
检测是否使用了非SDK接口 工具veridex
Apache HTTP 客户端弃用,需要自定义classloader

针对 Android 9 或更高版本并使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限。 这是普通权限,因此,系统会自动为请求权限的应用授予此权限。
8.0
未知来源应用
通知渠道
应用无法使用其清单注册大部分隐式广播(即,并非
专门针对此应用的广播)

6.14 谈谈Android10更新了哪些内容?如何

进行适配?
Foldables
5G网络
通知中的智能回复
黑暗主题
设置面板
分享快捷方式
用户隐私

6.15 请简述Apk的安装过程?

复制APK安装包到/data/app目录下,解压缩并扫描安装包,向资源管理器注入APK资源,解析AndroidManifest文件,并在/data/data目录下创建对应的应用数据目录,然后针对Dalvik/ART环境优化dex文件,保存到dalvik-cache 目录,将AndroidManifest文件解析出的组件、权限注册 到PackageManagerService并发送广播。

6.16 Java与JS代码如何互调?有做过相关优化吗?

WebView?
java调js:
可以用loadUrl指定 javascript: 协议,然后带上js代码, 如
webView.loadUrl(“javascript:alert(‘hello!’)”
)
4.4以后还提供了一个execJavascript方法,更方便调用
js调java:
可以先写好一个互调接口类,用addJavascriptInterface 绑定好,然后js代码里调用
注意:api17以后希望被js调用的需要添加
@
JavascriptInterface注解,因为api17以前存在漏洞, 通过互调接口的getClass()方法可以拿到Class对象,之后通过Class.forName()等一系列反射api可以调用任何方法也可以在java层通过shouldOverrideUrlLoading拦截跳转请求,js代码里通过跳转页面传递给java

6.17 什么是JNI?具体说说如何实现Java与

C的互调?
在Java类中声明native方法
使用javac命令将Java类生成class文件使
用javah文件生成头文件
编写cpp文件实现jni方法
生成so库
到此为止,java就可以通过调用native方法调用c
函数了,
对于在c++中调用java方法,
通过完整类名获取jclass
根据方法签名和名称获取构造方法id
创建对象(如果要调用的是静态方法则不需要创建对象)
获取对象某方法id
通过JNIEnv根据返回值类型、是否是静态方法调用对应函数即可

6.18 请简述从点击图标开始app的启动流程?

  1. 点击app图标,Launcher进程使用BinderIPC向systemserver进程发起startActivity请求;
  2. systemserver进程收到1中的请求后,向zygote进程发送创建新进程的请求;
  3. zygote进程fork出新的App进程
  4. App进程通过BinderIPC向systemserver进程发起attachApplication请求;
  5. systemserver进程收到4中的请求后,通过Binder IPC向App进程发送scheduleLauncherActivity请求;
  6. App进程的ApplicationThread线程收到5的请求后,通 过handler向主线程发送LAUNCHER_ACTIVITY消息;
  7. 主线程收到6中发送来的Message后,反射创建目标Activity,回调oncreate等方法,开始执行生命周期方法, 我们就可以看到应用页面了。

第三方框架分析

7.1 谈一谈LeakCanray的工作原理?

LeakCanary 主要利用了弱引用的对象, 当 GC 回收了这个对象后, 会被放进 ReferenceQueue 中;
在页面消失, 也就是 activity.onDestroy的时候,判断利用idleHandler 发送一条延时消息, 5秒之后,
分析 ReferenceQueue 中存在的引用, 如果当前 activity 仍在引用队列中, 则认为可能存在泄漏, 再利用系统类VMDebug 提供的方法, 获取内存快照,找出 GC roots 的最短强引用路径, 并确定是否是泄露, 如果泄漏, 建立导致泄露的引用链;

System.runFinalization(); // 强制调用已经失去引用的对象的
finalize 方法
Retryable.Result ensureGone(final
KeyedWeakReference reference, final long
watchStartNanoTime){
//1.. 从 retainedKeys 移除掉已经被会回收的弱引
用
removeWeaklyReachableReferences();
3.. 若当前引用不在 retainedKeys, 说明不存在内
//存泄漏
if (gone(reference)) { return
DONE;
}
//
4.. 触发一次gc
gcTrigger.runGc();
5.. 再次从 retainedKeys 移除掉已经被会回收的
//弱引用
removeWeaklyReachableReferences(); if
(!gone(reference)) {
//存在内存泄漏
long startDumpHeap =
System.nanoTime();
long gcDurationMs =
NANOSECONDS.toMillis(startDumpHeap -
gcStartNanoTime);
//获得内存快照
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs =
NANOSECONDS.toMillis(System.nanoTime() -
startDumpHeap);
HeapDump heapDump =
heapDumpBuilder.heapDumpFile(heapDumpFile).re
ferenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
heapdumpListener.analyze(heapDump);
}
return DONE;
}

7.2 谈一谈EventBus的原理?

EventBus
1、 register方法将对象实例用软引用包裹,保存到一个map缓存集合中
2、post方法 传入一个对象进去,然后遍历map里面多有的对象,找到所有的带有 @subscribe注解的并且方法参数与post的对象是同一类型的Method。 并通过反射执行Method。
3、Subscribe线程调度 执行method方法的时候会去获取注解上标记得线程,然后切换到指定线程。
4、unregister取消订阅 从第一步中的缓存map中移除对应注册的对象实例

7.3 谈谈网络请求中的拦截器(Interceptor)?

系统自带的拦截器:
1,重试和重定向
2,请求头+响应头处理
3,缓存
4,dns + 三次握手
5,CallServer,读写数据流
常用的自定义拦截器:
1,日志拦截器
2,自定义缓存规则拦截器
3,重试机制等等

7.4 谈一谈Glide的缓存机制?

Glide的缓存机制,主要分为2种缓存,一种是内存缓存, 一种是磁盘缓存。
使用内存缓存的原因是:防止应用重复将图片读入到内存,造成内存资源浪费。
使用磁盘缓存的原因是:防止应用重复的从网络或者其他地方下载和读取数据。
具体来讲,缓存分为加载和存储:

① 当加载一张图片的时候,获取顺序:Lru算法缓存-》弱引用缓存-》磁盘缓存(如果设置了的话)。
当想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,并将该图片放入WeakReference中,如果LruCache中没有,则去WeakReference中寻找,如果WeakReference中有,则从WeakReference中取出图片使用,如果WeakReference中也没有图片,则从磁盘缓存/网络中加载图片。
②将缓存图片的时候,写入顺序:弱引用缓存-》Lru算法缓存-》磁盘缓存中。
当图片不存在的时候,先从网络下载图片,然后将图片存 入弱引用中,glide会采用一个acquired(int)变量用来记录图片被引用的次数, 当acquired变量大于0的时候,说明图片正
在使用中,也就是将图片放到弱引用缓存当中; 如果acquired变量等于0了,说明图片已经不再被使用了, 那么此时会调用方法来释放资源,首先会将缓存图片从弱 引用中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。

另:从Glide4.x开始,读取图片的顺序有所改变:弱引用缓 存-》Lru算法缓存-》磁盘缓存

7.5 ViewModel的出现是为了解决什么问题?

并简要说说它的内部原理?
viewModel出现为了解决什么问题?
看下viewModel的优点就知道了:

1.对于activity/fragment的销毁重建,它们内部的数据也会销毁,通常可以用onSaveInstanceState()防法保存,通过onCreate的bundle中重新获取,但是大量的数据不合适, 而vm会再页面销毁时自动保存并在页面加载时恢复。

2.对于异步获取数据,大多时候会在页面destroyed时回收资源,随着数据和资源的复杂,会造成页面中的回收操作越来越多,页面处理ui的同时还要处理资源和数据的管理。而引入vm后可以把资源和数据的处理统一放在vm 里,页面回收时系统也会回收vm。加上databinding的支持后,会大幅度分担ui层的负担。

内部原理:
vm内部很简单,只有一个onClean方法。
vm的创建一般是这样

ViewModelProviders.of(getActivity()).get(UserModel.cla ss);
1.ViewModelProviders.of(getActivity())

在of方法中通过传入的activity获取构造一个HolderFragment,HolderFragment内有个
ViewModelStore,而ViewModelStore内部的一个hashMap保存着系统构造的vm对象,HolderFragment可 以感知到传入页面的生命周期(跟glide的做法差不多),HolderFragment构造方法中设置了setRetainInstance(true),所以页面销毁后vm可以正常保存。

2.get(UserModel.class);
获取ViewModelStore.hashMap中的vm,第一次为空会走 创建逻辑,如果我们没有提供vm创建的Factory,使用我们传入的activity获取application创建AndroidViewModelFactory,内部使用反射创建我们需要 的vm对象。

7.6 请说说依赖注入框架ButterKnife的实现原理?

  1. 通过注解器在编译期间生成一个XX_ViewBinding.java文 件(XX可以是activity,fragment,adapter,dialog),这个文件 这么生成的?

注解器里会添加需要类型的注解;
查找XX类中的特定类型注解,如果有,拼接成字符串,创建 并写到XX_ViewBinding.java文件中

  1. X_ViewBinding.java会持有XX的引用,如果是初始化控件,通过xx.findViewById实现
    如果是设置监听,类似xx.setOnClickListener实现

  2. X类中初始化XX_ViewBinding对象,这样打通了整个流 程

7.7 谈一谈RxJava背压原理?

总 共 分 为 4 种 策 略

  1. BackpressureStrategy.ERROR:若上游发送事件速度超出下游处理事件能力,且事件缓存池已满,则抛出异常 // 阻 塞 时 队 列
  2. BackpressureStrategy.BUFFER:若上游发送事件速度超出下
    游处理能力,则把事件存储起来等待下游处理
  3. BackpressureStrategy.DROP:若上游发送事件速度超 出下游处理能力,事件缓存池满了后将之后发送的事件丢 弃
  4. BackpressureStrategy.LATEST:若上有发送时间速度超出下游处理能力,则只存储最新的128个事件

8.综合技术

8.1 请谈谈你对 MVC 和 MVP 的理解?

MVC:
Model:主要用于网络请求、数据库、业务逻辑处理等操作。
View:用于展示UI,一般采用XML文件进行界面的描述。
Controller:控制层的重任落在了众多Activity上,Activity需要交割业务逻辑至Model层处理。
事件从View层流向Controller层,经过Model层处理,然后通知View层更新。
缺点:
a.Controller层,也就是activity承担了部分View层的职责,导致该层过于臃肿。
b.在MVC中,Model层处理完数据后,直接通知View层更新,因此MV耦合性强。
MVP:
Model: 数 据 处 理 层 。
View:视图显示层。Presenter:业务逻辑处理层。
通过接口,V和P间接相互持有引用,P和M间接相互持有引 用,但是V和M完全解耦,而且把业务逻辑从activity中移到P中,减轻了activity的压力。
缺点:
当View层足够复杂的时候,View接口会变得很庞大。
MVVM:
Model:同上。
View:同上。
ViewModel:视图模型,通过框架实现View层和Model层的双向绑定。

优点:
a.View和Model双向绑定,一方的改变都会影响另一方, 开发者不用再去手动修改UI的数据。
b.不需要findViewById也不需要butterknife,不需要拿到 具体的View去设置数据绑定监听器等等,这些都可以用DataBinding完成。
c.View和Model的双向绑定是支持生命周期检测的,不会担心页面销毁了还有回调发生,这个由lifeCycle完成。
d.不会像MVC一样导致Activity中代码量巨大,也不会像MVP一样出现大量的View和Presenter接口,项目结构更加低耦合。
缺点:
由于数据和视图的双向绑定,导致出现问题时不好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致。
选择:
1、如果项目简单,没什么复杂性,未来改动也不大的话, 那就不要用设计模式或者架构方法,只需要将每个模块封装好,方便调用即可,不要为了使用设计模式或架构方法而使用。

2、对于偏向展示型的app,绝大多数业务逻辑都在后端,app主要功能就是展示数据,交互等,建议使用mvvm。
3、对于工具类或者需要写很多业务逻辑app,使用mvp或 者mvvm都可。

8.2 分别介绍下你所知道Android的几种存储方式?

网络存储 :一般就是http get或http post 从服务器获取数据,业务数据获取的常用办法。 sqllite:将数据缓存到本地数据库,可用于存储大量不经常改变的数据,可配合
contentProvider使用。 文件存储:将一些不太敏感的数据保存到本地, SharePreference:用XML格式文件存储数据,在data/data/<pa'ka'geName>/shared_prefs下, 不支持数据频繁读写,频繁读写会造成数据错乱。
ContentProvider:四大组件之一,一般配合sqlite、
SharePreference、文件存储使用,支持数据的并发读取。

8.3 简述下热修复的原理?

热修复分为三个部分,分别是Java代码部分热修复,Native代码部分热修复,还有资源热修复。
资源部分热更新直接反射更改所有保存的AssetManager和Resources对象就行(可能需要重启应用)
Native代码部分也很简单,系统找到一个so文件的路径是 根据ClassLoader找的,修改ClassLoader里保存的路径就行(可能需要重启应用)

Java部分的话目前主流有两种方式,一种是Java派,一种是Native派。
java派:通过修改ClassLoader来让系统优先加载补丁包里的类
代表作有腾讯的tinker,谷歌官方的Instant Run,包括multidex也是采用的这种方案
优点是稳定性较好,缺点是可能需要重启应用 native派:通过内存操作实现,比如方法替换等
代表作是阿里的SopHix,如果算上hook框架的话,还有dexposed,epic等等
优点是即时生效无需重启,缺点是稳定性不好:如果采用方法替换方式实现,假如这个方法被内
联/Sharpening优化了,那么就失效了;inline hook则无法修改超短方法。
热修复后使用反射调用对应方法时可能发生IllegalArgumentException。

8.4 谈谈如何适配更多机型的?

  1. 一般来说主要针对屏幕适配,最小宽度适配和今日头条density适配
  2. 权限适配,安卓6.0的运行时权限,这里有坑,6.0以前,
    Vivo有i管家进行权限管理,魅族自带有权限管理,还有其 他第三方软件进行权限限制,导致权限不可用
  3. 异形屏幕适配,一般来说都是刘海,水滴,挖孔部分不进行使用或者就直接不管不显示缺失部分,可以满足大部分需求,小部分需求需要使用异形部分的需要按手机型号进行特定适配,官网都有适配方法
  4. 安卓系统适配,及时关注新系统新特性,使情况修改targetSdk
  5. 语言,Left Right和Start End,这些适配基本不需要太大关注
  6. “和ios一样”,口才或者脑细胞适配,能说服就下班,不 能就加班

8.5 请谈谈你是如何进行多渠道打包的?

配置gradle实现多渠道打包
每当应用发布一个新的版本的时候,我们会分发到每一个 应用市场中去,比如,360手机助手,小米应用市场,华为应用市场等。为了能够统计每个应用市场的下载量,活跃 量我们必须用一个标记来区分这些不同市场分发下去的应 用,渠道号也就应运而生。随着渠道的不断增加,需要生 成的渠道包也就越来越多。 在打包的过程中,我们一般都是使用gradle来进行的。gradle为我们的打包提高了很多 的便利,
多渠道打包也可以轻松实现。

1.首先在AndroidManifest.xml文件中定义一个

<meta-data
android:name="CHANNEL"
android:value="${CHANNEL_VALUE}" />

2.然后在gradle文件中设置一下productFlavors

android {
productFlavors {
xiaomi {
manifestPlaceholders =
[CHANNEL_VALUE: "xiaomi"]}
_
360 {
manifestPlaceholders =
CHANNEL_VALUE: "_360"]
}
baidu {
manifestPlaceholders =
CHANNEL_VALUE: "baidu"]
}
wandoujia {
manifestPlaceholders =
[
}
CHANNEL_VALUE: "wandoujia"]
}
}

productFlavors是为了在同一个项目中创建应用的不同版本。具体的配置信息可以看官方说明。

3.执行gradle aS就可以将所有的渠道包输出了。gradle实现多渠道打包的缺点
虽然gradle配置多渠道打包很简单,也很方便,但是这种 方式存在一个致命的缺陷,那就是费时间。因为AndroidManifest.xml文件被修改过了,所以所有的包都必须重新编译签名。一般来说100个渠道包就要至少一个小时的时间,这一个小时5杯咖啡都不够等的。更要命的是万一哪里需要微调一下代码或者文案,那么不好意思,一切又得重头来。这就很麻烦了,所以有没有什么方法可以快速完成打包呢?我们继续往下看。

8.6 多渠道快速打包

快速打包方案Version_1.0
如上所说,我们去到信息只是修改了一下manifest文件里 面的一个meta-data的值而已,有没有什么办法可以不需 要重新构建代码呢?答案是肯定的。我们可以使用apktool,反编译我们的APK文件。

apktool d yourApkName build

经过解码后,我们会得到如下文件:

所以我们只要像META_INF文件夹里面写入空白的文件来标识
渠道号就可以了。
通过Python脚本像APK文件中写入渠道

import zipfile
zipped = zipfile.ZipFile(your_apk, 'a',
zipfile.ZIP_DEFLATED)
empty_channel_file = "META-
INF/mtchannel_{channel}".format(channel=your_
channel)
zipped.write(your_empty_file,
empty_channel_file)

执行后会在META-INF文件夹下面生成一个空白文件: 然后我们在项目中去读取这个空白文件:

public static String getChannel(Context context) {
ApplicationInfo appinfo =context.getApplicationInfo();
String sourceDir = appinfo.sourceDir; String ret="";
ZipFile zipfile = null; 
try {
    zipfile = new ZipFile(sourceDir);
    Enumeration<?> entries = zipfile.entries();
    while (entries.hasMoreElements())
    {
        ZipEntry entry = ((ZipEntry)
        entries.nextElement());
        String entryName = entry.getName();
        if(entryName.startsWith("mtchannel")) {
            ret = entryName;
            break;
    }
  }
 }
}
catch (IOException e)
{
    e.printStackTrace();
    finally {
        if (zipfile != null) { try {
           zipfile.close();
}catch (IOException e)
{
    e.printStackTrace()
   }
}
String[] split = ret.split("_");
if (split != null && split.length >=2) {
    return
    ret.substring(split[0].length() + 1);
  }
}else {
return "";
}

这样每生成一个渠道包只需要复制一下APK,然后添加一个空态的文件到META-INF下面就可以了,这样100个渠道包一分钟之内就可以搞定了.
具体的原因可以看下这里。快速打包方案Version_2.0
上面的方案基本上已经比较完美的解决我们打包的问题了,然而好景不长,Google在Android 7.0中更新了应用的签名算法-APK Signature Scheme v2,它是一个对全文件进行签名的方案,能提供更快的应用安装时间、对未授权

APK文件的更改提供更多保护,在默认情况下,Android Gradle 2.2.0插件会使用APK Signature Scheme v2和传统签名 方案来签署你的应用。因为是对全文件进行签名的, 所以之前的添加空白文件的方案就没有作用了。不过目前这个方案还不是强制性的,我们可以选择在
gradle配置文件中将其关闭:

android {
defaultConfig { ... }
signingConfigs {
release
{
storeFi
le
file("myreleasekey.keystore")
storePassword "password"
keyAlias "MyReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}

那么新的签名方案对已有的渠道生成方案有什么影响呢?下图是新的应用签名方案和旧的签名方案的一个对比:

新的签名方案会在ZIP文件格式的 Central Directory 区块所在文件位置的前面添加一个APK Signing Block区块,下面按照ZIP文件的格式来分析新应用签名方案签名后的APK 包。 整个
APK(ZIP文件格式)会被分为以下四个区块:

  1. Contents of ZIP entries(from offset 0 until the start of APK
    SigningBlock)
  2. APK Signing Block
  3. ZIP Central Directory
  4. ZIP End of Central Directory
    新应用签名方案的签名信息会被保存在区块2(APKSigning Block) 中 , 而 区 块 1(Contents of ZIPentries)、区块3(ZIP Central Directory)、区块4(ZIP End ofCentral Directory)是受保护的,在签名后任何对区块1、3、4的修改都逃不过新的应用签名方案的检查。之前的渠道包生成方案是通过在META-INF目录下添加空文件,用空文件的名称来作为渠道的唯一标识,之前在META-INF下添加文件是不需要重新签名应用的,这样会节省不少打包的时间,从而提高打渠道包的速度。但在新的应用签名方案下META-INF已经被列入了保护区了,向META-INF添加空文件的方案会对区块1、3、4都会有影 响,新应用签名方案签署的应用经过我们旧的生成渠道包方案处理后,在安装时会报以下错误
Failure
[
INSTALL_PARSE_FAILED_NO_CERTIFICATES:
Failed to collect certificates from base.apk:
META-INF/CERT.SF indicates base.apk is signed
using APK Signature Scheme v2,
but no such signature was found. Signature
stripped?]

区块1,3,4是受保护的,任何的修改都会引起签名的不一致,但是区块2是不受保护的,所以能不能在区块2上面找到解决办法呢?首先看一下区块2的文件结构:

区块2中APK Signing Block是由这几部分组成:2个用来标示这个区块长度的8字节 + 这个区块的魔数(APK Sig Block 42)+这个区块所承载的数据(ID-value)。
我们重点来看一下这个ID-value,它由一个8字节的长度标 示+4字节的ID+它的负载组成。V2的签名信息是以ID(0x7109871a)的ID-value来保存在这个区块中,不知大家有没有注意这是一组ID-value,也就是说它是可以有若干个这样的ID-value来组成,那我们是不是可以在这里做一些文章呢?
对于签名的认证过程是这样的:

  1. 寻找APK Signing Block,如果能够找到,则进行验证, 验证成功则继续进行安装,如果失败了则终止安装
  2. 如果未找到APK Signing Block,则执行原来的签名验证机制,也是验证成功则继续进行安装,如果失败了则终止安装

在校验的时候,检验代码如下:

public static ByteBuffer
findApkSignatureSchemeV2Block(
ByteBuffer apkSigningBlock,
Result result) throws
SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
// FORMAT:
// OFFSET
//DATA TYPE
//DESCRIPTION
/ * @+0
bytes (excluding this field)
/
bytes uint64:
size in
// * @+8
bytes pairs
// * @-24 bytes uint64:
size in magic bytes (same as the one above)
// * @-16 bytes uint128:

ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) { entryCount++; if (pairs.remaining() < 8) { throw new SignatureNotFoundException(
"Insufficient data to read size of APK Signing Block entry #" + entryCount); }
long lenLong = pairs.getLong(); 
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {throw new SignatureNotFoundException(
"APK Signing Block
entry #" + entryCount+" size out of range: " + lenLong); }
int len = (int) lenLong; int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) { throw new SignatureNotFoundException( "APK Signing Block entry #" + entryCount + " size out of range: " + len + ",
available: " + pairs.remaining());
}
int id = pairs.getInt(); if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
    return getByteBuffer(pairs,len - 4);
}
    result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
    pairs.position(nextEntryPos);
}
    throw new SignatureNotFoundException("No APK Signature Scheme v2 block in APK Signing Block");
}

我们可以发现,述代码中关键的一个位置是 if (id ==APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {return getByteBuffer(pairs,len-4);},通过源代码可以看出 Android是通过查找ID为
APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a 的ID-value,来获取APK Signature Scheme v2 Block,对这个区 块中其他的ID-value选择了忽略。也就是说,在APK Signature
Scheme v2中没有看到对无法识别的ID,有相关处理的介绍。
所以我们可以通过写入自定义的ID-Value来自定义渠道。

8.6 MVP中你是如何处理Presenter层以防止内存泄漏的?

首先 MVP 会出现内存泄漏是因为 Presenter 层持有 View对象,一般我们会把 Activity 做为 View 传递到Presenter,Presenter 持有 View对象,Activity 退出了但是没有回收出现内存泄漏。

解决办法:

  1. Activity onDestroy() 方法中调用 Presenter 中的方法, 把View置 为null
  2. 使用 Lifecycle
  3. 使用 MVVM

8.7 如何计算一张图片所占的内存空间大小?

通常情况下图片占用内存的大小:图片分辨率X像素点大小 。
api获取方法为bitmap.getByteCount()。如果放入res/drawable下,通过BitmapFactory.decodeResource()方法加载不同dpi文件下的同一张图片到内存的大小是不一样(存在分辨率的转换),同时也与设备的dpi大小有关。而放在sd卡、网络或者assert里则是一样的(即使是不通dpi的设备)。图片占 用内存大小跟控件大小无关(glide是先拿到控件大小,再去对图片加载做
的处理)。

8.8 有没有遇到64k问题,应该如何解决?

单个dex文件方法超过64k,基本上都是引用过多的依赖才 导
致的。
解决方案:

  1. 导入依赖
'com.android.support:multidex:1.0.1'
  1. defaultConfig增加这个设置
multiDexEnabled true
  1. android下面增加这个设置
dexOptions {
incremental true
javaMaxHeapSize "4g"
}

以上都是在app的buildl.gradle中设置的,编译。
4. 打开自定义的Application,继承MultiDexApplication, 并重
写attachBaseContext方法

8.9 如何优化 Gradle 的构建速度?

1:物理设备,16g+内存,硬盘ssd,高配u。最好是超频 u。5.0g那种
2:配置.使用高版本as

android.injected.testOnly=false
android.buildCacheDir=buildCacheDir
org.gradle.caching=true
android.enableBuildCache=true
android.enableAapt2=true
org.gradle.jvmargs=-Xmx2048m -
XX:MaxPermSize=512m -
XX:+HeapDumpOnOutOfMemoryError -
Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.daemon=true
android.enableSeparateAnnotationProcessing = true
buildTypes{ 
debug{
        crunchPngs false
        aaptOptions.cruncherEnabled = false
    }
}
dexOptions
preDexLibraries
{
    true
}

如果使用了multiDexEnabled,一定要依赖最新版本的 1.0.3版本

依赖的库不带+号,每次都会自动检查最新版
:as设置,如果已下载好依赖,可以设置离线模式以上配置可以加快构建和编译速度。

8.10 如何获取Android设备唯一ID?

常用方法:
IMEI
权限(读手机权限)
可以手动更改(模拟器上) /关闭权限报错
Mac地址
访问WIFI权限
有些设备没有wifi(没有上网功能)/6.0以上版本返回的都是固定的02:00:00 之类地址
ANDROID_ID
设备首次运行,系统会随机生成一个64位的数字,转换成16进 制保存 手机恢复出厂设置后值有变化/ 有些国产设备不会返回值
Serial Number (设备序列号)
有些国产手机(红米)会出现垃圾数据
实现思路:

  1. 获取设备的IMEI -->设备的Mac地址–>随机生成的UUID,
  2. 拼接在一起,然后用MD5加密
  3. 存储到本地的文件中,隐藏. 并且存储在App中的sp中
  4. 使用的时候先从sp中读取, 没有的再生成,然后把生成的唯一id保存到sp中

8.11 谈一谈Android P禁用http对我们开发有什么影响?

APP改用https请求
targetSdkVersion 降到27以下
res --> add : network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
network-security-config>
<
base-config
cleartextTrafficPermitted="true" />
/network-security-config>
<
AndroidManifest.xml--> application :
android:networkSecurityConfig="@xml/network_s
ecurity_config"

8.12 什么是AOP?在Android中它有哪些应用场景?

aop的实现有静态代理和动态代理,静态代理有静态代理模式,基于Ajc编译器的AspectJ,

动态代理有JDK动态代理,在android的实现是InvicationHandler,需要实现接口,该有CGlib 实习方式是创建子类来继承源类。

应用场景有各种状态监测比如登录、网络、权限等。日志埋点,性能分析。

8.13 什么是MVVM?你是如何将其应用于具体项目中的?

MVVM架构其实由来已久,在Android中MVVM指代的是Model-View-ViewModel。Model就是数据模型,View一般认为是Activity/Fragment。那么这个ViewModel其实是Google为Android应用程序搭建MVVM架构而设计的一个组件(类)。顾名思义,ViewModel可以认为是View与Model之间的一个容器,在没有MVVM之前,我们通常会将View和Model之间的业务处理直接放在Activity/Fragment中进行处理,现在有了ViewModel,可以将相关逻辑放在其中处理,View(Activity/Fragment) 只进行数据的展示,而不进行数据的加工处理。那么数据在处理或者变化后,如何通知到View呢?这就需要通过
Jetpack的另外一个组件LiveData。LiveData可以使得ViewModel中的数据变化后,通知到View,它起到一个桥梁的作用。本质上这是一个观察者模式。

在Android项目中应用MVVM架构,Google官方建议的做法是使用Jetpack相关组件,这些组件设计的目的就是为了方便开发者搭建MVVM规范的应用程序。答案中的图片来自《Android Jetpack 应用指南》一书。我就是这本书的作者,这本书详细介绍了Jetpack的各个组件,并介绍了如 何使
用这些组件搭建符合MVVM规范的应用程序。如果想 系统学习Jetpack以及MVVM,不妨购买一本阅读一下,书本不贵,相信可以节省你大量宝贵的时间。

8.14 请谈谈你是如何实现数据埋点的?

一、埋点技术
代码埋点:

所谓的代码埋点就是在你需要统计数据的地方植入N行代码,统计用户的关键行为。比如你想统计首页某个banner 的点击量,上报的数据可以采用KEY-VALUE形式,我们定 义KEY为「CLICK_ADD_BTN」,VALUE的值为点击的次数。当用户点击banner 时,banner 详情的代码会通过按钮的「回调」来触
发执行,程序猿在业务代码执行完后,又 加上了统计代码,把「CLICK_ADD_BTN」对应的VALUE加
1,banner被统计到了一次使用。
代码埋点的优点:
使用者控制精准,可以非常精确地选择什么时候发送数据使用者可以比较方便地设置自定义属性、自定义事件,传递比较丰富的数据到服务端。

代码埋点的缺点:
埋点代价比较大,每一个控件的埋点都需要添加相应的代码,不仅工作量大,而且限定了必须是技术人员才能完成;

更新代价比较大,每一次更新,都需要更新埋点方案,然后通过各个应用市场进行分发,而且有的用户还不一定更新,这样你就获取不到这批用户数据。

可视化埋点:
既然代码埋点代价比较大,每一个埋点都需要写代码,那 就使用可视化交互手段代替写代码;既然每次代码埋点都 需要更新,那就参照现在的很多手游做法,把核心代码和 配置、资源分开,每次用户启动app的时候通过网络更新配置和资源。

可视化埋点优势:
可视化买点解决了代码埋点埋点代价大和更新代价大两个问题。

可视化埋点劣势:
可视化埋点能够覆盖的功能有限,目前并不是所有的控件操作都可以通过这种方案进行定制;

无埋点:
可视化埋点先通过界面配置哪些控件的操作数据需要收集;“无埋点”则是先尽可能收集所有控件的操作数据,然后再通过界面配置哪些数据需要在系统里面进行分析,“无埋点”也就是“全埋点”的意思。

无埋点的优点:
可视化埋点只能收集到你埋点以后的数据,如果你想对某个按钮进行点击分析,则只能分析增加可视化埋点以后的数据,之前的数据你收集不到,而无埋点在你部署SDK的时候数据就一直在收集。

因为无埋点对页面所有元素进行埋点,那么这个页面每个元素被点击的概率你也就知道,对点击概率比较大的元素可以进行深入分析。

无埋点的缺点:
由于无埋点方案所有的元素数据都收集,会给数据传输和服务器带来较大的压力。

二、数据埋点方式
1、公司研发在自己的产品当中注入统计代码,搭建相应的后台查询,这种代价比较大。

2、集成第三方统计的SDK,比如友盟、百度移动统计、SensorsData、GrowingIO、TalkingData等。

三、如何进行数据埋点
1、明确目标
经常有人问我说我要获取那些数据来进行数据分析,其实这个问题不应该问别人,应该问问你自己,你是想用这个数据干什么,如果你想绘制基础的人群画像你就需要获取用户机型、网络类型、操作系统,IP地域等数据;如果你想分析每一个注册转化率,你就需要获取每一个步骤的点击次数,然后制作成漏斗,看那一步转化率出现了问题; 目的不一样,获取的数据也不一样,使用的埋点技术也不一样,我们无论做什么事情都不能忘了我们的目的!

2、获取相应数据
业务不同,目的不同获取的数据也不同,这里我只说一些比较共性的数据。

2.1、产品各个渠道下载量

这个可以用第三方数据统计工具来进行,这样我们可以知道我们产品着重在那个渠道进行推广。

2.2、产品活跃状态分析

产品活跃状态监控,留存分析、流失分析、新增变化等, 次日留存率、七日留存率、月留存率,尤其对于处于成长期的产品而已,这个指标很重要,如果留存率比较低,说明你的
产品有问题,这个时候你就需要进行用户调研,找到流失的问题,否则大面积拉新,只能拉多少死多少,至于留存率、新增的变化这些数据,我们也可以借助第三方统计工具来进行。

2.3、事件分析

比如你想统计某个页面的Uv、PV、元素的点击量、用户停 留时长、页面跳出率等数据指标,可以选择代码埋点和可视化埋点等前端埋点解决方案。当某个页面的UV很高,但是跳出率也很高,说明页面有问题,你就要好好想想页面的问题出在什么地方。

2.4、基本信息获取

基本信息获取,例如机型、网络类型、操作系统,IP地域等,绘制基础用户人群画像,这种分析出来的用户画像颗粒度比较大,如果想更精准的进行用户画像可以结合推荐系统,来获取用户的兴趣指标,以及用户操作行为等数据来进行更精准的用户画像,从而为产品运营和产品设计提供参考,可以借助第三方统计工具和自定义埋点的方式进行数据的收集。

2.5、漏斗模型

对于产品的关键路径一定要进行漏斗模型分析,比如注册 路径,从用户输入注册手机号到注册成功,中间可能会有 几个步骤,如果100个人注册,最后只有一个人注册成功, 那么求运营同学心里的阴影面积。还有电商的购买下单路 径,从浏览商品到最后下单购买成功,每一个步骤的转化 率是多少,对于漏的比较多的那个步骤我们肯定要着重关 注,分析原因。这个可以技术研发进行埋点,获取更精确 的数据,比
如下图的埋点表。

如何给app客户端进行埋点?
领导说,APP需要加一下统计,你负责搞定
研发说,APP需要统计哪些地方,你列一下埋点需求
研发说,APP的数据统计SDK用哪家的?你选好了注册一下、
运营说,咱们的APP都能看哪些数据?平台在哪?怎么查首页的UV?

数据分析是一个很复杂的工作,很多人在谈如何挖掘数据,做用户画像,设计数据漏斗,如何负责用户生命周期管理,但发现很多人却卡在了数据分析的第一步,那就是如何做数据埋点。

首页陈峰老师先明确下完成一个APP数据埋点的几个步骤:

注册一家统计网站
新建应用
获取KEY和SDK代码包
将埋点需求和SDK包发给研发
自定义埋点需求完善
研发开发并完成APP上线
在后台查看数据

1、注册账号

建议用公司邮箱或者公用邮箱注册,别用自己的私人邮箱和手机号码,后续一旦有交接和工作变动时会比较麻烦。

2、新建应用

登录后一般都有“新建应用”,可以选标准统计,大部分APP 都选这个。游戏的app另说。名称写自己app的名称,分类自己选1个。选错了也不影响。

平台根据情况自己选。后期我们看数据和埋点都是ios和安卓分开的,所以你如果2个端都做,就一起都选上。描述可选,不用填。

3、获取KEY和SDK代码包

完成后可以得到2个APPKEY。分别是ios和安卓的。
这里的appkey很重要,你可以下载了给研发,也可以稍后 让研发自己登录进来自己下载。
ios和安卓是分开2个独立的,后续埋点和看数据都是分开的。这个切记。

这时候,重点来了。
此时,如果我们只想看 APP的活跃用户,留存用户,下载量。用户地域分布,渠道分布,那么其实就够了。

4、将埋点需求和SDK包发给研发

你这时候,就把刚才获得的appkey和sdk包的下载地址, 发给研发。或者直接把账号和密码发给研发。然后告诉研发,集成下百度移动统计的SDK包。这样发版后,就可以看到大部分数据了。

如下的数据都可以看到:


5、自定义事件完善

比如我们想看页面里面 注册 搜索按钮,顶部banner,底部 首页和 我的 2个导航条的点击量。
一个埋点事件对应1个按钮或者一个页面或者一个弹层。你来定义。
如果埋点比较多,你也可以批量添加。批量添加的时候, 您需要下载excel模板,按照要求填写好,上传进来即可。 具体一看便知。
添加完成后就可以把这个列表导出或者人肉复制出来一个表格。发给研发。并附上你的原型图。做好对应关系标 注。

6、研发开发并完成APP上线

完成上面几步后,研发哥哥就可以看懂进入第7步研发阶段了。

7、在后台查看数据

上线后就可以看到数据了。大部分数据一般隔天更新。

三、埋点后能看到什么数据
上面提到,按照步骤完成数据分析sdk集成和自定义事件后,就可以看到数据了。
不添加自定义事件,可以看到基础数据,添加后,可以看到更细节的按钮,页面等点击数据。

查看自定义事件埋点数据,还是进入刚才的“事件分析”页面,点击对应埋点即可看到数据。可以筛选时间段。
除了这些外,如果你还想看 几个页面之间的转化路径和数据漏斗。那还需要添加“转化分析”。
添加转化分析后,可以看到例如: 进入首页-点击注册按钮-进入注册成功页 这几步的转化率和流失率。会自动生成一个转化分析图。当然你也可以分别看这几个页面的数据, 自己去分析汇总。
进阶的方法还有把事件埋点配合转化分析、访问路径、转化漏斗等工具使用,从点到面地了解用户的使用行为、APP存在的问题。
产品核心指标一般包含:
1.1. 产品规模
用户数据。如新增用户、用户类型分布、活跃用户、沉默用户、启动次数、版本分析等。
1.2 业务数据。这个与具体业务有关,如问答社区的问题数,回答数,全网热度,浏览量;如对含交易平台的交易量,交易额,客单价,转化率,利润等。
2. 产品运营
2.1 流量数据。pv,uv,dau,mau,留存分析(次日留 存,7日留存, 用户新鲜度), 流失分析(日周月、自然流失、回归流失),
2.2 渠道数据。渠道流量,渠道转换率,渠道评价,渠道时段详情,渠道质量(渠道活跃用户/渠道流量)等。

8.15 假如让你实现断点上传功能,你认为应该怎样去做?

分2种情况:
分块上传:多线程读取本地文件指定区域流上传,header 带有上传位置标记,服务器接收到多个上传请求会生成多个上传临时文件接收,最后合并成完整文件。(续传如下)
正常续传:本地请求上传,服务器响应是否未完成的临时文件和文件byte,本地收到就接着指定位置读流上传。其中会涉及块的数据校验等

8.16 webp和svg格式的图片各自有什么特点?

应该如何在Android中使用?
一、svg格式
svg是矢量图,这意味着svg图片由直线和曲线以及绘制它 们的方法组成。当你放大一个svg图片的时候,你看到的还是线和曲线,而不会出现像素点。svg图片在放大时,不会失
真,所以它非常适合用来绘制企业Logo、IconSVG格式特点:
1、SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
2、SVG 用来定义用于网络的基于矢量的图形
3、SVG 使用 XML 格式定义图形
4、SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
5、SVG 是万维网联盟的标准
6、SVG 与诸如 DOM和 XSL 之类的W3C标准是一个整体
二、webp格式
webp是谷歌开发的一种新图片格式,是同时支持有损和无损压缩的、使用直接色的、点阵图。

8.17 说说你是如何进行单元测试的?以及如何应用在MVP和MVVM中?

单元测试库 junit mockito Rebolectric
说下mvp工程中的测试方法
测试主要有 三大部分
1.普通工具类 使用junit 直接测试
2.mvp的p 使用 @mock标注view的接口, 初始化真正的p,直接调用p的 方 法 看看 verify view的某些方法是否按照预期被调用

3.mvp的v 用rebolectric 去setup 一个Activity, 然后 用这个库找到 界面上的按钮,或者触发生命周期(onstart),判断一下当前界面的某些view是否被显示 或者 textview的值或者dialog 是否显示 toast是否弹出错误

4.还有网络部分的测试,可以直接使用junit进行测试 判断下返回值是否符合预期

8.18 对于GIF 图片加载有什么思路和建议?

gif图实际上就是多帧合并的图
参 考 Fresco 内 部 实 现 :
1,View层使用一个Drawable,包含bitmap,并依据gif的 信息不断的更新并绘制bitmap
2,C层提供api功能,例如:输入gif数据流,提供解析gif信息、更新bitmap等功能

8.19 为什么要将项目迁移到AndroidX?如何进行迁移?

现在Android官方支持的最低系统版本已经是4.0.1,对应 的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。
那么很明显,Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级,升级内容主要在于以下两个方面。

第一,包名。之前Android Support Library中的API,它们的包名都是在android.support.下面的,而AndroidX库中所有API的包名都变成了在androidx.下面。这是一个很大的变化,意味着以后凡是android.包下面的API都是随着Android操作系统发布的,而androidx.包下面的API都是随
着扩展库发布的,这些API基本不会依赖于操作系统的具体版本。

第二,命名规则。吸取了之前命名规则的弊端,AndroidX 所有库的命名规则里都不会再包含具体操作系统API的版本号了。比如,像appcompat-v7库,在AndroidX中就变成了appcompat库。

一个AndroidX完整的依赖库格式如下所示:

implementation 'androidx.appcompat:appcompat:1.0.2'

了解了AndroidX是什么之后,现在你应该放轻松了吧?它 其实并不是什么全新的东西,而是对Android Support Library 的一次升级。因此,AndroidX上手起来也没有任何困难的地方,

比如之前你经常使用的RecyclerView、ViewPager等等库,在AndroidX中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化。但是有一点需要注意,AndroidX和AndroidSupport Library中的库是非常不建议混合在一起使用的,因为它们 可能会产生很多不兼容的问题。最好的做法是,要么全部使用AndroidX中的库,要么全部使用Android Support Library中
的库。

而现在Android团队官方的态度也很明确,未来都会为AndroidX为主,Android Support Library已经不再建议使用,并会慢慢停止维护。另外,从Android Studio 3.4.2开始,我发现新建的项目已经强制勾选使用AndroidX架构了。

10.第数据结构方面

9.1 什么是冒泡排序?如何优化?

冒泡排序算法原理:(从小到大排序)
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,交换一趟后,最后的元素会是最大的数
3.针对所有的元素重复以上的步骤,除了最后一个
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较

优化方案1(定义一个变量l来保存一趟交换中两两交换的次数,如果l==0,则说明排序已经完成,退出for循环)
优化方案2(假如有一个长度为50的数组,在一趟交换后, 最后发生交换的位置是10,那么这个位置之后的40个数必定已经有序了,记录下这位置,下一趟交换只要从数组头 部到这个位置就可以了)

定义一个变量n来保存一趟交换中最后一次发生交换的位置,并把它传递给下一趟交换

9.2请用 Java 实现一个简单的单链表?

定义链表:

public class ListNode
int val;
{
ListNode next;
public ListNode(int
val){ this.val = val;
}
}

基本操作:

public class SingleLinkedList {
/** 链 表 的 头 结 点 */
ListNode head = null;
/**
*
*链表添加结点:
*找到链表的末尾结点,把新添加的数据作为末尾结点的后续结点
*@param data/
public void addNode(int data){ ListNode newNode = new ListNode(data);
    if(head == null){ head = newNode; return;
}
ListNode temp = head;
while(temp.next != null){
    temp = temp.next;
} 
    temp.next = newNode;
}
/**
*
*链表删除结点:
*把要删除结点的前结点指向要删除结点的后结点,即直接跳过待删除结点
*@param index
*@return
*
/
public boolean deleteNode(int index){ if(index<1 || index>length()){//待删除
结点不存在
    return false;
}
if(index == 1){//删除头结点head  
     ListNode= head.next;
     return true;
}
ListNode preNode = head;
curNode = preNode.next; int i = 1;
while(curNode != null){
if(i==index){//寻找到待删除结点
preNode.next =
curNode.next;//待删除结点的前结点指向待删除结点的后结点
    return true;
}
//当先结点和前结点同时向后移
preNode = preNode.next;
curNode = curNode.next;
i++;
}
return true;
}
/**
*求链表的长度
*@return
*/
public int length(){ int length = 0;
    ListNode curNode = head;
    while(curNode != null){
        length++;
        curNode = curNode.next;
    }
    return length;
}
");
public void
/**
*printLink(){ ListNode curNode
打印结点 = head;
while(curNode !=null){
    System.out.print(curNode.val+"* pcurNode = curNode.next);
}
    System.out.println();
}
/**
*查找正数第k个元素
*
/
public ListNode findNode(int k){ if(k<1 ||
k>length()){//不合法的k
    return null;
}

ListNode temp = head; for(int i =0; i<k-1; i++){
    temp = temp.next;
}
return temp;
}
public static void main(String[]args){ 
    SingleLinke dList myLinkedList = new SingleLinkedList();
//添加链表结点
myLinkedList.addNode(9);
myLinkedList.addNode(8);
myLinkedList.addNode(6);
myLinkedList.addNode(3);
myLinkedList.addNode(5);
//打印链表
myLinkedList.printLink();
System.out.println("链表结点个数为:" +
myLinkedList.length());
myLinkedList.deleteNode(3);
 myLinkedList.printLink();
}

9.3 如何反转一个单链表?

思路:定义3个节点分别是preNode,curNode,nextNode. 先将curNode的next赋值给nextNode, 再curNodel的next 指向preNode. 至此 curNode 的指针 调整完毕。 然后往后移动curNode, 将curNode赋值给preNode,将nextNode赋值给curNode,如此循环到curNode==null为止 代
码:

public static Node reverseListNode(Node head)
{
//单链表为空或只有一个节点,直接返回原单链表
if (head == null || head.getNext() == null){ 
return head;
}
//前一个节点指针
Node preNode = null;
//当前节点指针
Node curNode = head;
//下一个节点指针
Node nextNode = null;
while (curNode != null){ 
nextNode = curNode.getNext();//nextNode 指向下一个节点
curNode.setNext(preNode);//将当前节点 next域指向前一个节点
preNode = curNode;//preNode 指针向后移动
curNode = nextNode;//curNode指针向后移动
    }
return preNode;
}

9.4 谈谈你对时间复杂度和空间复杂度的理解?

在进行算法分析时,语句总的执行次数 T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随 n 变化情况并确定 T(n) 的数量级。算法的时间复杂度,也就是算法的时间度量,记作:
T(n) =O(f(n))。它表示随问题规模 n的增大,算法执行时间的增长率和 f(n) 的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中 f(n) 是问题规模 n 的某个函数。

这样用大写 O()来体现算法时间复杂度的记法,我们称之为大O记法。随着 n 的增大,T(n) 增长最慢的算法为最优算法

算法的空间复杂度是通过计算算法所需的存储空间实现,
记作:S(n) = O(f(n)),其中,n 为问题的规模,f(n) 为语句关于 n 所占存储空间的函数。
若算法执行时间所需要的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为 O(1)。

9.5 谈一谈如何判断一个链表成环?

Two ways tosolve:
/////////////Two Pointers://///////////////////////
public boolean hasCycle(ListNode head)
{
if(head==null || head.next==null){
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while(slow != fast){
if(fast==null || fast.next==null)
return false;
fast=fast.next.next;
slow=slow.next;
}
return true;
}
/
////////////HashSet:///////////////////////////
public boolean hasCycle(ListNode head)
{
Set<ListNode> dict = new HashSet<>();
while(head !=
null){ if(dict.contains(
head)){
return true;
else {
dict.add(head);
}
}
head = head.next;
}
return false;

9.6 什么是红黑树?为什么要用红黑树?

jva 的 HashMap, TreeMap, TreeSet, Linux 的虚拟内存管理;
了解红黑树之前, 先了解二叉搜索树:

  1. 左子树上所有结点的值均小于或等于它的根结点的值;
  2. 右子树上所有结点的值均大于或等于它的根结点的值;
    但是这样的二叉树, 很有可能变成线性结构, 为了避免出现这样的情况, 产生了红黑树;

每个节点, 都包含5个属性:color, key, left, right 和parent;
如果一个节点没有子节点, 或者没有父节点, 则其对应属性值为 NULL;
我们可以把这些 NULL 视为指向二叉搜索树的叶子节点指针, 外部指针;把带关键字的节点视为树的内部节点;
一般红黑树必须是满足以下属性的二叉搜索树:
1… 每个节点是红色, 或者是黑色;
2… 根节点是黑色;
3… NULL 节点是黑色的;
4… 如果一个节点是红色的, 那么他的两个子节点都是黑色的;
5… 每个节点到所有叶子节点, 所经过的黑色节点数量相同;
6… 最长路径不会超过最短路径的 2 倍;

从某个节点 N 出发, 达到一个叶子节点, 经过任意一个简单路径, 其黑色节点的个数称之为黑高, 记为bh;
红黑树必须满足, 任意一个节点, 到其任意一个叶子节点,经过任意的简单路径, 其黑高相等;

左旋传会带上左父亲, 右旋转会带上右父亲;
插入操作
基本描述
要将红黑树当作一棵二叉查找树, 找到插入的位置;
将节点着色为红色(默认就是红色);
通过旋转和重新着色, 对其修正, 使之成为一棵新的红黑树;
因为新插入的节点, 肯定是叶子节点咯, 所以要满足[每个节点到所有叶子节点, 所经过的黑色节点数量相同];
接下来, 还要依次满足剩余所有的基本属性;什么情况下需要旋转, 什么情况下不需要旋转?
如果新插入的当前节点的父节点, 是黑色的, 就不需要做任何操作;
如果新插入的当前节点的父节点, 是红色的, 就需要重新着色, 旋转;
红黑树调整的整体思想是, 将红色的节点移到根节点, 然后,将根节点设为黑色;

调整红黑树
(1)当前节点是根节点, 直接设为黑色的;
(2)当前节点的父节点是黑色的, 无序任何操作;
(3)当前节点的父节点是红色的, 叔叔节点是红色的;
将父亲节点, 设为黑色;
将叔叔节点, 设为黑色;
将祖父节点, 设为红色;
将祖父节点, 设为新的当前节点, 继续判断;
(4)当前节点的父节点, 是红色的, 叔叔节点是 T.nil 节点或者是黑色的, 当前节点是父节点的右孩子;将祖父节点设为红色;将父节点设为黑色;
当前节点的父节点, 设为新的当前节点;
以当前节点, 为支点进行左旋, 继续判断;
(5)当前节点的父节点, 是红色的, 叔叔节点是 T.nil 节点或者是黑色的, 当前节点是父节点的左孩子;
将祖父节点设为红色;
将父节点设为黑色;
当前节点的父节点, 设为新的当前节点;
以当前节点, 为支点进行右旋, 继续判断;
删除操作
待删除的节点情况可以分为 3 种:
1… 是叶子节点;
2… 只有左子树或只有右子树;
3… 有左子树和右子树;
调整红黑树
后继节点, 就是右子树上最小的节点;
前驱节点, 就是左子树上最大的节点;
先看待删除的节点的颜色, 再看兄弟节点的颜色, 先看远侄子再看近侄子, 最后看父亲节点的颜色;

case… 待删除的节点, 是红色的, 没有子节点, 直接删除既可;
case…待删除的节点, 是红色的, 只有一个子节点, 子节点一定是黑色的, 父节点也一定是黑色的;

直接删除当前节点, 子节点替换当前节点, 设为黑色即可;
case…待删除的节点, 是红色的, 它有两个子树, 当然左右子树都是黑色的, 父节点是黑色的;
从待删除的右子树, 找到最小的节点;待删除节点与右子树最小节点, 数值互换;
右子树最小节点作为新的待删除节点, 继续判断;
case…待删除的节点, 是黑色的, 只有一个子节点, 子节点是红色的;
直接删除当前节点, 子节点替换当前节点, 设为黑色即可;
case…待删除的节点, 是黑色的, 是父节点的左子树, 并且兄弟节点是红色的;
父节点和兄弟节点颜色互换, 以兄弟节点为支点, 左旋传,
继续判断;
case…待删除的节点, 是黑色的, 是父节点的左子树, 它的兄
弟节点是黑色的, 远侄子是红色的;
父节点和兄弟节点颜色互换, 远侄子设为黑色, 删除当前
节点, 以兄弟节点为支点, 左旋传即可;
case…待删除的节点, 是黑色的, 是父节点的左子树, 它的兄
弟节点是黑色的, 近侄子是红色的;
兄弟节点和近侄子颜色互换, 以兄弟节点为支点, 右旋传,
继续判断;
case…待删除的节点, 是黑色的, 是父节点的左子树, 它的兄
弟节点是黑色的, 两个侄子都是黑色的;
从待删除的右子树, 找到最小的节点;
待删除节点与右子树最小节点, 数值互换;
右子树最小节点作为新的待删除节点, 继续判断;
case…待删除的节点, 是黑色的, 是父节点的右子树, 并且兄弟节点是红色的;
父节点和兄弟节点颜色互换, 以兄弟节点为支点, 右旋传,继续判断;
case…待删除的节点, 是黑色的, 是父节点的右子树, 它的兄弟节点是黑色的, 远侄子是红色的;
父节点和兄弟节点颜色互换, 远侄子设为黑色, 删除当前节点, 以兄弟节点为支点, 右旋转即可;
case…待删除的节点, 是黑色的, 是父节点的右子树, 它的兄弟节点是黑色的, 近侄子是红色的;
兄弟节点和近侄子颜色互换, 以兄弟节点为支点, 左旋传,继续判断;
case…待删除的节点, 是黑色的, 是父节点的右子树, 它的兄弟节点是黑色的, 两个侄子都是黑色的;
从待删除的右子树, 找到最小的节点;
待删除节点与右子树最小节点, 数值互换;
右子树最小节点作为新的待删除节点, 继续判断;
case…待删除的节点, 是黑色的, 它的兄弟节点是黑色的, 它和兄弟节点都没有子节点;
父节点设为黑色, 兄弟节点设为红色, 删除当前节点即可;

9.7 什么是快速排序?如何优化?

快速排序是从冒泡排序演变而来的算法,但是其比冒泡排序要高效,所以叫做快速排序,简单理解如下。
我举个简单例子来理解吧:
比如我们即将排序的数组如下:
1
8 9 5 6 3 0
我们一般将首位 1 或者最后一个数字 0 认为是基准元素,然后左右对比,大致规律如下:
第一次 : 将 1 移出,从最右边 数字0 开始,如果 <= 基准数1,则将其移到左边第一个位置,此时 最右边的数字相当于被挖空。

如下,其中 — 代表被挖空的数字
0
8 9 5 6 3 —
接下来从左边开始,如果大于等于基准数1,则将移到右边刚才挖空的位置上,如下:
2
— 9 5 6 3 8
接下来继续从右边开始,刚才右边我们进行到3了,继续左移,如果遇到 <= 基准数 0,那么将其移到刚才 挖的 坑上,如果没有遇到,并且左右操作的数相同时,此时 将 基准数 移动到这个空着的坑位。
如下:
0
1 9 5 6 3 8
我们可以发现,基准数1左边的都小于其,右边的都大于 其,所以两边各自继续按照刚才上面的逻辑继续递归。(虽然这里最左边只是0,可以忽略)

接下来的过程如下:
8
(基准数9)
0
0
1 — 5 6 3
1 8 5 6 3 —
0
0
1 8 5 6 3 9
9
(基准数8)
1 — 5 6 3
0
1 3 5 6 9 _
1 3 5 6 _ 9
0
0
1 3 5 6 8 9 (基准数6)
最后,再盗一张图,详细的看一下详细参考逻辑,请参考 程序员小灰,后知,后觉优化:
快速排序在序列中元素很少时,效率将比较低,不如插入排序,按需使用。基准数采用随机。
尾递归优化。
快速排序和分治排序算法一样,都有两次递归调用,而且快排的递归在尾部,所以我们可以对快排代码实施尾递归优化。减少堆栈深度。
将每次分割结束后,将于本次基数相等的元素聚集在一起,再次分割时,避免对聚集过的元素进行分割。
多线程优化,基于分治法的思想,将一个规模为 n 的问题分解为 k个规模较小的问题。这些子问题互相独立且与原问题相同。求解这些子问题,然后将子问题的解合并,从而得到原问题的解。

9.8 说说循环队列?

引用的前面设计的数组类

Array.java
/**
*Array
*@author : lao
@date : 2019/11/2 13:17
*/
public class Array<E> {
private E[] data;
private int size;
/**
*
*构造函数
*
@param capacity 传入数值初始值大小
*/
public Array(int capacity) {
//Java不支持直接构造函数中new 泛型数据类型,使用强制装换
    data = new E[capacity]; data =(E[])new Object[capacity]; size = 0;
}
/**
*无参构造函数,默认容量capacity=10
*/
public Array()
{   
    this(10);
}
/***
@return 返回数组元素的个数
*/
public int getSize() { 
return size;
}

/**
*@return 返回数组的容量
*/
public int getCapacity() { 
return data.length;
}
public boolean isEmpty() { 
return size == 0;
}
/****
*向数组中添加一个元素
@param e
*/
public void addLast(E e)
{
    add(size, e);
}
/**
*向数组添加一个 元素
*
*@param e
*/
public void addFirst(E e) { add(0,e);
}
/**
*在index位置插入一个元素
@param index 位置
@param e
参数值
*/
public void add(int index, E e) {
    if (index < 0 || index > size) { throw new IllegalArgumentException("AddList failed.Require index>=0 and index <= size");
}
if (size == data.length) { resize(2 * data.length);
}
for (int i = size - 1; i >= index; i--)
{
    data[i + 1] = data[i];
}
data[index] = e;
size++;
}

/***
获取数组中某一元素
@param index
@return
*/
public E get(int index) {
if (index < 0 || index >= size) { throw new
IllegalArgumentException("Get failed.Index is illegal.");
}
return data[index];
}
public E getLast() { return
get(size - 1);
}
public E getFirst() { return
get(0);
}

/**
设置某一位置的元素值
@param index
@param e
*/
public void set(int index, E e) { if (index < 0 || index >= size) {
    throw new IllegalArgumentException("Get failed.Index is illegal.");
}
    data[index] = e;
}
/**
*
*查找数组中是否包含某一元素e
@return
*/
public boolean contains(E e) { for (int i = 0; i < size; i++) {
if (data[i].equals(e)) { 
    return    true;
    }
}
return false;
}
/**
*查找数组中元素e的位置
@param e 查找元素
@return 成功返回元素下标,不成功返回-1
*/
public int find(E e) {
for (int i = 0; i < size; i++) {
//比较是否为同一对象
if (data[i].equals(e)) {
    return i;
    }
}
    return -1;
}
/**
*删除index位置的元素,返回index中的元素
@param index
@return
*/
public E remove(int index) {
    if (index < 0 || index >= size) { throw new
    IllegalArgumentException("Get failed.Index is illegal.");
}
E res = data[index];
for (int i = index + 1; i < size; i++)
{
    data[i - 1] = data[i];
}
size--;
data[size] = null;
//loitering object! = memory leak
//减少容量,不能创建数组大小为0
if (size == data.length / 4 && data.length / 2 != 0) {
    resize(data.length / 2);
}
return res;
}
/**
*删除数组首元素
@return
*/
public E removeFirst() { 
    return remove(0);
}
/**
*删除数组末尾元素
*/
public E removeLast() { 
    return remove(size - 1);
}

/**
*删除数组中指定元素
@param e
@return
*/
public boolean removeElement(E e) { 
    int index = find(e);
if(index remove(index) != -1)
{
    return true;
}
return false;
}
/**
*@return 返回数组大小、容量
*/
@Override
public String toString()
{
StringBuilder res = new
StringBuilder();
res.append(String.format("Array: size =
d , capacity = %d\n", size, data.length)); res.append('[');
%
for (int i = 0; i < size; i++) { res.append(data[i]);
if (i != size - 1) {
res.append(", ");
}
}
res.append(']'); return
res.toString();
}
/**
*数组扩容
@param newCapacity
*
/
private void resize(int newCapacity) {
E[] newData = (E[])new Object[newCapacity];
for (int i = 0; i < size; i++) { 
    newData[i] =data[i];
}
data = newData;
    }
}

定义队列的接口Queue.java

/**
*
*Queue
*
@author : lao
@date : 2019/11/3
12:45
*/
public interface Queue<E> { 
int getSize();
boolean isEmpty();
void enqueue(E e); 
Edequeue(); E getFront();
}

普通队列 ArrayQueue.java

/**
*
ArrayQueue
*
*
@author : lao
*
@date : 2019/11/3
12:46
*
/
public class ArrayQueue<E> implements
Queue<E> {
private Array<E> array;
public ArrayQueue(int capacity) { array =
new Array<>(capacity);
}

public ArrayQueue() { array =
new Array<>();
}
@Override
public int getSize() { return
array.getSize();
}
@Override
public boolean isEmpty() { return
array.isEmpty();
}

public int getCapacity() { return
array.getCapacity();
}
/**
*添加元素
*
/
@Override
public void enqueue(E e)
{
    array.addLast(e);
}

/**
*
*取出元素
@return
/
@Override
public E dequeue() {
    return array.removeFirst();
}
/**
*
*查看队首元素
@return
*/
@Override
public E getFront() {
    return array.getFirst();
}
@Override
public String toString()
{
    StringBuilder res = new StringBuilder();
    res.append("Queue: ");
    res.append("front [");
    for (int i = 0; i < array.getSize();i++) {
        res.append(array.get(i));
     if (i != array.getSize() - 1) {
        res.append(", ");
    }
}
res.append("] tail"); 
return res.toString();
   }
}

循环队列
LoopQueue.java

/**
*LoopQueue
@author : lao
@date : 2019/11/3
13:00
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data;
//数组
//队头
private front;
private int;
private int;
private int;
private tail;
//队尾
//容量
private size;
public LoopQueue(int capacity) {
data = (E[])new Object[capacity + 1]; front = 0;
tail = 0;
size = 0;
}
public LoopQueue() { this(10);
}
/**
*
容量
@return
*
/
public int getCapacity() { return
data.length - 1;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() { return
front == tail;
}
/**
*
入队
@param e
*
/
@Override
public void enqueue(E e) {
if ((tail + 1) % data.length == front)
{
resize(getCapacity() * 2);
}
data[tail] = e;
tail = (tail + 1) % data.length; size++;
}
/**
*
*
出队
@return
*
/
@Override
public E dequeue() {

    if (isEmpty()) { throw new IllegalArgumentException("Cannot dequeue froman queue.");
}
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length; size--;
if (size == getCapacity() / 4 &&
    getCapacity() / 2 != 0) {
resize(getCapacity() / 2);
}
return ret;
}
/**
*
获取队头元素
@return
*
/
@Override
public E getFront() { if
(isEmpty()) {
throw new IllegalArgumentException("Queue
is empty.");
}
return data[front];
}
/***
*
队列扩容或缩减
@param newCapacity 扩容或缩减大小
*
/
private void resize(int newCapacity) { E[]
newData = (E[])new
Object[newCapacity + 1];
for (int i = 0; i < size; i++) { newData[i] =
data[(i + front %
data.length)];
}
data = newData;
front = 0;
tail = size;
}
@Override
public String toString()
{
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size
%d, capacity = %d\n", size,= getCapacity()));
res.append("front [");
for (int i = front ; i != tail; i = (i
1) % data.length) { + res.append(data[i]);
if ((i + 1) % data.length != tail)
{
    res.append(", ");
    }
}
res.append("] tail"); return
res.toString();
}
public static void main(String[] args)
{
    LoopQueue<Integer> queue = new LoopQueue<>();
    for (int i = 0; i < 10; i++)
    {
        queue.enqueue(i);
        System.out.println(queue); if ( i % 3 == 2) {
        queue.dequeue();
        System.out.println(queue);
    }
    System.out.println("---");
   }
  }
}

队列的运行比较
Main.java

import java.util.Random;
public class Main {
private static double
testQueue(Queue<Integer> q, int opCount) {
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < opCount; i++) {
    q.enqueue(random.nextInt(Integer.MAX_VALUE));
}
for (int i = 0; i < opCount; i++) { q.dequeue();
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1}
000000000.0;
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue,
opCount);
System.out.println("ArrayQueue, time: " + time1 + "s");
LoopQueue<Integer> loopQueue = new
LoopQueue<>();
double time2 = testQueue(loopQueue,
opCount);
System.out.println("LoopQueue, time: " + time2 + "s");
    }
}
执行比较的结果:

9.9 如何判断单链表交叉

这个题应该是问假如两个单键表相交,请找出相交的位置的节点。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
    return null;
}
ListNode currA = headA;
ListNode currB = headB;
int lengthA = 0;
int lengthB = 0;
// 让长的先走到剩余长度和短的一样
while (currA != null)
{
    currA = currA.next;
    lengthA++;
}
while (currB != null)
   
{
 currB = currB.next;
 currA = headA; 
 currB = headB;
while (lengthA > lengthB) { 
currA = currA.next; 
lengthA--;
}
while (lengthB > lengthA) { 
currB= currB.next; 
lengthB--;
}
// 然后同时走到第一个相同的地方
while (currA != currB) { currA = currA.next; currB = currB.next;}
// 返回交叉开始的节点
return currA;

10.设计模式

10.1请简要谈一谈单例模式?

借助类加载机制,可以在不使用synchronized等内容的情况下,最高效的实现单例。

public class Singleton {
public static Singleton getInstance(){
// 1. 调用该方法时,才会访问
LazyHolder.INSTANCE这个静态类的静态变量
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
// 2. 访问 LazyHolder.INSTANCE才会触发Singleton的初始化
static final Singleton INSTANCE = newSingleton();
}
    private Singleton(){}
}
  1. 参考自类加载规范的: 访问静态字段时,才会会初始化静态字段所在的类
  2. 因为类初始化是线程安全的,并且只会执行一次。因此在多线程环境下,依然能保证只有一个Singleton实例。
  3. 解释:getInstance()调用了LazyHolder的静态字段INSTANCE,所以会触发LazyHolder的加载和初始化。
    LazyHolder的初始化阶段会对静态字段INSTANCE进行赋值,也就是new Singleton(),此外初始化阶段是线程安全的且只执行一次,因此就算是多线程,也只会创建一个Singleton对 象。从而实现单例。

10.2 对于面向对象的六大基本原则了解多少?

单一职责原则(Single Responsibility Principle):

对于一个类来说,它里面包含的应该是相关性很高的函数或者变量。比如,两个不相关的业务功能不应该放在一个类中处理,我们应该尽可能的针对于具体的业务功能对类进行拆分,以达到“每个类只负责自己需要关心的内容,其他的与我无关”的目的。

开闭原则(Open Close Principle):

开闭原则是我们程序设计过程中最基本的原则之一。在我们的程序里,我们所熟知的对象,对于扩展应该是开放的,而对于修改应该是封闭的。什么意思呢?其实很好理解。我们平常在开发的时候可能会经常碰到一个问题:今天写了一个类,里面封装了很多功能方法,但是过了没多久,业务功能改动了,我们又不得不修改这个类里面已存在的代码,以满足当前业务需求。然而,修改已存在的代码会带来很大问题,倘若这个类在很多其他类中用到,而且耦合度较高,那么即使我们对该类内部代码做很小的改动,可能都会导致与之相关(引用到该类)的部分的改动,如果我们不注意这个问题,可能就会一直陷入无止
境修改和烦躁当中,这就违背了开闭原则。推荐的做法是:为了防止与之相关类(引用到该类)的改动,我们对于该类的修改应该是封闭的,我们可以提供对于该类功能的扩展途径。那么该如何扩展 呢?可以通过接口或者抽象类来实现,我们只需要暴露公共的方法(接口方法),然后由具体业务决定的类来实现这方法,并在里面处理具体的功能代码,至于对外具体怎么用,用户无需关心。其实,开闭原则旨在指导
用户,当我们业务功能需要变化时,应该尽量通过扩展的方式来实现,而不是通过修改已有代码来达到目的。
只有这样,我们才能避免代码的冗余和腐化,才能使系统更加的稳定和灵活。

里氏替换原则(Liskov Substitution Principle):

对于一个系统来说,所有引用基类的地方必须同样能够透明地替换为其子类的对象。看下面这张图应该就能理解什么意思了:

这里简单模拟了一下 Android 系统中的 Window 与View 的关系,Window显示视图的时候只需要传一个View 对象,但在具体的绘制过程中,传过来的可以是View 的子类 TextView或者是 ImageView等等。

依赖倒置原则(Dependence Inversion Principle):

在Java中可以这样理解:模块之间应该通过接口或者抽 象来产生依赖关系,而实现类之间不应该发生直接的依赖关系。这也就是我们常说的面向接口/抽象编程。举个列子:

interface Hobby { fun
playGame()
}
class Boy : Hobby {
override fun playGame() { System.out.print("男孩子喜欢枪战游戏.")
    }
}
class Girl : Hobby {
override fun playGame() { System.out.print("女孩子喜欢看书和打扮.")
    }
}
class Survey {
fun studyChildrenHobby(hobby: Hobby)
    hobby.playGame()
   }
}
fun test(){
    val survey = Survey()
    survey.studyChildrenHobby(Girl())
}

从上面简单的小例子可以看出,Survey 类依赖的是接口Hobby,并没有依赖于具体的实现类。

接口隔离原则(Interface Segregation Principle):
接口隔离原则提倡类之间的依赖关系应该建立在最小接口上,提倡将复杂、臃肿的接口拆分为更加具体和细小的接口,以达到解耦的目的。这里的"最小接口"指的也是抽象的概念,将具体实现细节隔离,降低类的耦合 性。
迪米特原则(Law of Demeter):

一个对象应该尽可能的对其他对象有最少的了解。即类与类之间应该尽可能的减小耦合度,否则一个类变化,它对另一个类的影响越大。
个人感觉,其实这几大原则来来回回阐述的都是"抽 象"的概念,通过抽象来让我们的系统更加稳定、灵活和易维护。

10.3 请列出几种常见的工厂模式并说明

//简单工厂模式:
public interface
    MyAbInterca{ public void a();
}
public class MyInterceImplement implement
MyAbInterca{
    .......
}
//工厂方法模式:
public abstract class Product { public abstract void method();
}
public class ConcreteProductA extends Product
{
    @Override
    public void method() {
        System.out.println("产品A");
    }
}
public class ConcreteProductB extends Product
{
    @Override
    public void method()
    {
        System.out.println("产品B");
    }
}
//抽象工厂模式:
    public abstract void method();
}
/**
具体产品类A
*/
public class ConcreteProductA extends Product
{
@Override
public void method() {
    System.out.println(“我是具体的产品A”);
  }
}
/*具体产品类B
*/
public class ConcreteProductB extends Product
{
@Override
public void method() {
    System.out.println(“我是具体的产品B”);
    }
}

10.4 说说项目中用到的设计模式和使用场景?

单例模式
常见应用场景:网络请求的工具类、sp存储的工具类、弹窗的工具类等

工厂模式
常见应用场景:activity的基类等

责任链模式
常见应用场景:OKhttp的拦截器封装

观察者模式
常见应用场景:Rxjava的运用

代理模式
常见应用场景:AIDL的使用

建造者模式
常见应用场景:Dialog的创建

10.5 什么是代理模式?如何使用?Android源码中的代理模式?

1.代理模式

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.

这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法(那这个思想就能让我们做很多事情了)——代理模式是面向对象编程中比较常见的设计模式。
举个例子来说明代理的作用:我们在生活中见到过的代理,大概最常见的就是商场里的商品。商品从工厂生产之后进入大型超市,之后消费者再来购买。

用图表示如下:

代理模式的关键点是:代理对象与目标对象.代理对象是对目标对象的扩展,并会调用目标对象

1.1.静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类.
下面举个案例来解释:
模拟保存动作,定义一个保存动作的接口:IUserDao.java,然后目标对象实现这个接口的方法UserDao.java,此时如果使用静态代理方式, 就 需 要 在 代 理 对 象 (UserDaoProxy.java) 中 也实现IUserDao接口.调用的时候通过调用代理对象的方法 来调用目标对象.

需要注意的是,代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法代码示例:

public interface IUserDao {
    void addRecord();
}
public class UserDao implements IUserDao
{
@Override
public void addRecord() {
        System.out.println("UserDao--为您添加一条新记录");
    }
}
public class UserDaoProxy implements IUserDao
{
    private IUserDao target;
    public UserDaoProxy(IUserDao target){
    this.target = target;
}
@Override
public void addRecord() {

System.out.println("开始执行任务。。。。");
target.addRecord();
System.out.println("提交执行任务。。。。");
    }
}
public static void staticProxy()
{
    UserDaoProxy userDaoProxy = new UserDaoProxy(new UserDao());
    userDaoProxy.addRecord();

}

开始执行任务。。。。UserDao–为您添加一条新记录提交执行任
务。。。。 Process finished with exit code 0
静态代理总结:
优点:可以做到在不修改目标对象的功能前提下,对目标功能扩展.
缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.

如何解决静态代理中的缺点呢?答案是可以使用动态代理方式

1.2.动态代理

动态代理有以下特点:
1.代理对象,不需要实现接口
2.代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
3.动态代理也叫做:JDK代理,接口代理JDK中生成代理对象的API代理类所在包:java.lang.reflect.Proxy
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

static Object newProxyInstance(ClassLoader loader, Class<?>[]
interfaces,InvocationHandler h )

注意该方法是在Proxy类中是静态方法,且接收的三个参数 依次为:

ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
Class<?>[] interfaces

Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入 上一节代码中 UserDaoProxy类是代理,我们需要手动编写代码让 UserDaoProxy实现 IUserDao接口,而在动态代理中,我们可以让程序在运行的时候自动在内存中创建一个实现IUserDao接口的代理,而不需要去定义UserDaoProxy这个类。这就是它被称为动态的原因。

也许概念比较抽象。现在实例说明一下情况。
->柳胥街有很多饭店,口味独特,分量十足,有一个饭店卖红烧爆鱼,我们用Java代码模拟。

public interface Restaurant
void eatFan();
{}
public class SauerkrautRest implements Restaurant {
@Override
public void eatFan() {
    System.out.println("来我家吃酸菜鱼啦");
  }
}
public class PlaceForSell implements InvocationHandler {
private Object target;
public PlaceForSell(Object service) { this.target = service; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("this is a place for restaurant ,please coming");
    method.invoke(target, args);
    System.out.println("please seat down and have fun");
    return null;
   }
}
public static void dynamicProxy()
{
    SauerkrautRest sauerkrautRest = new SauerkrautRest();
    InvocationHandler invocationHandler = new PlaceForSell(sauerkrautRest);
    Restaurant sauerkRest = (Restaurant)Proxy.newProxyInstance(SauerkrautRest.class.getClassLoader(),
SauerkrautRest.class.getInterfaces(),invocationHandler);
//this is a place for restaurant ,please coming来我家吃酸菜鱼啦please seat down and have fun
Proxy.newProxyInstance(target.getClass.getClassLoader(),
target.getClass.getInterfaces(),invocationHandler);
// 第一二个参数皆为target的sauerkRest.eatFan();
}

InvocationHandler
InvocationHandler 是一个接口,官方文档解释说,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的InvocationHandler实现类,由它决定处理。

/**
*
{
@code InvocationHandler} is the interface implemented by
*the <i>invocation handler</i> of a proxy
instance.
*
*
<p>Each proxy instance has an associated
invocation handler.
When a method is invoked on a proxy
instance, the method
invocation is encoded and dispatched to the
@code invoke}
*
{
*method of its invocation handler.
@author
@see
Peter Jones
Proxy
@since
1.3
*/
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)throws  Throwable;
}
InvocationHandler 内部只是一个 invoke() 方法,正是这个方法决定了怎么样处理代理传递过来的方法调用。
proxy 代理对象
method 代理对象调用的方法
args 调用的方法中的参数
因为,Proxy 动态产生的代理会调用 InvocationHandler实现类,所以 InvocationHandler 是实际执行者。
所以我们可以进一步将其封装为一个ProxyFactory,节省时间与复用代码,follow me AV8D

public class ProxyFactory
{
private Object target; 
public ProxyFactory(Object target)
{
    this.target = target;
}
public Object getProxyInstance() { return
Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy,Method method, Object[] args) throws Throwable {
    System.out.println("开始事务--"+target.getClass().getSimpleName() + "-----");
//执行目标对象方法Object
returnValue = method.invoke(target, args);
System.out.println("提交事务--"target.getClass().getSimpleName() + "-----+");
    return returnValue;
        }
    });
 }
}

动态代理原理探究
一定有同学对于为什么 Proxy 能够动态产生不同接口类型的代理感兴趣,我的猜测是肯定通过传入进去的接口然后通过反射动态生成了一个接口实例。
比如 Restaurant是一个接口,那么Proxy.newProxyInstance() 内部肯定会有,那么我们进入源码查看。

public static Object
newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
    Objects.requireNonNull(h);
    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
*Look up or generate the designated proxy
class.
/
Class<?> cl = getProxyClass0(loader, intfs);
/*
*
Invoke its constructor with the
designated invocation handler.
/ try
*
{
if (sm != null) {
    checkNewProxyPermission(Reflection.getCallerC lass(), cl);
}
final Constructor<?> cons =cl.getConstructor(constructorParams);
final InvocationHandler ih = h; 
if (!Modifier.isPublic(cl.getModifiers()))
{
AccessController.doPrivileged(newPrivilegedAction<Void>() {
public Void run()
{
    cons.setAccessible(true);
    return null;
    }
 });
}
return cons.newInstance(new Object[]{h});
}
catch (IllegalAccessException|InstantiationExceptione) {e);
    throw new InternalError(e.toString(),
}
catch (InvocationTargetException e) { Throwable t = e.getCause();
if (t instanceof RuntimeException) { throw (RuntimeException) t;
}else {
    throw new InternalError(t.toString(), t);
    }
}catch (NoSuchMethodException e) {
    throw new InternalError(e.toString(),e);
   }
}
newProxyInstance 的确创建了一个实例,它是通过 cl 这个Class 文件的构造方法反射生成。cl 由getProxyClass0() 方法获取。
/**
*Generate a proxy class.
Must call the
checkProxyAccess method
to perform permission checks before calling this.
*/
private static Class<?>getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
    if (interfaces.length > 65535) { throw new  IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given
loader implementing
// the given interfaces exists, this will simply return the
cached copy;
// otherwise, it will create the proxy class via the
ProxyClassFactory
/*如果给定的加载程序定义的代理类给定的接口存在,这将简单地返回缓存的副本;否则,它将通过代理类工厂创建代理类。*/
    return proxyClassCache.get(loader, interfaces);
}
/**
*
A factory function that generates, defines and returns
the proxy class given the ClassLoader and array of interfaces.
*/
private static final class ProxyClassFactory implements BiFunction<ClassLoader,Class<?>[], Class<?>>
{
// prefix for all proxy class names private
static final String proxyClassNamePrefix = "$Proxy";
// next number to use for generation of unique
proxy class names 
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[]
interfaces) {
    Map<Class<?>, Boolean> interfaceSet = new   IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
    Class<?> interfaceClass = null; 
    try {
        interfaceClass = Class.forName(intf.getName(), false, loader);
        } catch (ClassNotFoundExceptione) {
}
    if (interfaceClass != intf) { throw new IllegalArgumentException(intf + " is not visible from class loader");
    represents an
}
/**
Verify that the Class object actually
interface.
*/
if
(!interfaceClass.isInterface()) {
throw new IllegalArgumentException(interfaceClass.getName() +" is not an interface");
}
/*
*Verify that this interface is not a duplicate.
*/
if(interfaceSet.put(interfaceClass,
Boolean.TRUE) != null) {
throw new IllegalArgumentException( "repeated interface: " + interfaceClass.getName());
        }
}
String proxyPkg = null;
package to define proxy class in int accessFlags = Modifier.PUBLIC |Modifier.FINAL;
/*
*Record the package of a non-public proxy interface so that the
*proxy class will be defined in the samepackage. Verify that
all non-public proxy interfaces are in
the same package.
*/
for (Class<?> intf : interfaces) {
     int flags = intf.getModifiers(); 
     if(!Modifier.isPublic(flags)) {
        accessFlags = Modifier.FINAL; 
        String name = intf.getName();
        int n = name.lastIndexOf('.');
        String pkg = ((n == -1) ? ""
        name.substring(0, n + 1));
if (proxyPkg == null)
{
       proxyPkg = pkg;
}else if (!pkg.equals(proxyPkg)) {
    throw new IllegalArgumentException("non-public interfaces from different packages");
    }
}
if (proxyPkg == null) {
// if no non-public proxy
interfaces, use com.sun.proxy package
    proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*生成代理类从这里开始
Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
*Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces,accessFlags);
try {
return defineClass0(loader,proxyName,proxyClassFile, 0, proxyClassFile.length);
 catch (ClassFormatError e) {
}
/*
A ClassFormatError here means that
proxy class generation code) there
invalid aspect of the(barring bugs in thewas some other arguments supplied to the proxy class creation (such as virtual
machine limitations
exceeded).
throw new IllegalArgumentException(e.toString());
        }
    }
}

通过指定的 ClassLoader 和 接口数组 用工厂方法生成 proxy class。 然后这个 proxy class 动态生成的代理类名称是包名+$Proxy+id序号。

long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
Cglib代理
上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib代理Cglib代理,也叫作子类代理,它是在内存中构建一个子类对 象从而实现对目标对象功能的扩展.
JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类, 就可以使用Cglib实现.

Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如SpringAOP和synaop,为他们提供方法的interception(拦截)

Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM, 因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.
Cglib子类代理实现方法:

1.需要引入cglib的jar文件即可(皆已上传github,文末有地
址).
2.引入功能包后,就可以在内存中动态构建子类
3.代理的类不能为final,否则报错
4.目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法。

public class CgLibProxyFactory implements MethodInterceptor {
    private Object target;
    public CgLibProxyFactory(Object target) { this.target = target;
}
//给目标对象创建一个代理对象
public Object getProxyInstance() {
//1.工具类
Enhancer en = new Enhancer();
//2.设置父类
en.setSuperclass(target.getClass());
//3.设置回调函数
en.setCallback(this);
//4.创建子类(代理对象)
    return en.create();
}
@Override
public Object intercept(Object o, Method method,Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("开始事务--" +
o.getClass().getSimpleName() + "-------------------");
//执行目标对象方法Object
returnValue = method.invoke(target, objects);
System.out.println(" 提 交 事 务 --" +
target.getClass().getSimpleName() + "--------------------- ");
return returnValue;
    } 
}
public class Animal { 
public void ying() {
    System.out.println("this is a animal");
   }
}
public static void dynamicCglibProxy() {
Animal animal = new Animal();
Animal proxy = (Animal) new CgLibProxyFactory(animal).getProxyInstance();
proxy.ying();
}

开始事务–Animal$$EnhancerByCGLIB$$cc94e0a5---- -this is a animal
提交事务--Animal-----

这种代理模式非JDK提供,我后面再去深入研究,或者老铁们需要直接给我提issue,我会抽时间写。
代理的作用主要作用,还是在不修改被代理对象的源码上,进行功能的增强。这在 AOP 面向切面编程领域经常见。
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。

上面的引用是百度百科对于 AOP 的解释,至于,如何通过代理来进行日志记录功能、性能统计等等,这个大家可以参考 AOP 的相关源码,然后仔细琢磨。

同注解一样,很多同学可能会有疑惑,我什么时候用代理呢?

总结
代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理(JDK),文中Cglib代理就不需要实现 任何接口

动态代理,代理类通过 Proxy.newInstance() 方法生成。静态代理和动态代理的区别是在于要不要开发者自己定义Proxy 类。

动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。

代理模式目的是为了增强增加现有代码的功能。

10.6 谈一谈单例模式,建造者模式,工厂模式的使用场景?如何合理选择?

单例模式,一般是指将消耗内存、属性和对象支持全局公用的对象,应该设置为单例模式,如持久化处理(网络、文件等)
建造者模式,一般见于开发的框架或者属性时可以直接链式设置属性,比如我们看到的AlertDialog,一般用在某些辅助类(如 BRVAH的 BaseViewHolder)或者开发的框架的时候方便连续设置多个属性和调用多个方 法。
工厂模式,一般用于业务的实体创建,在创建的过程中考虑到后期的扩展。在Android源码中比较常见的有BitmapFactory``LayoutInflater.Factory 在实体编码的过程中,比如BRVAH 的多布局,如果数据类型比较多或者后期需要扩展,则可以通过工厂布局的方式,将实现MultiItemEntity接口的实体通过工厂模式创建:

object MultiItemEntityFactory{ 
val TEXT = 1
val IMG = 2
val TEXT_IMG = 3
fun createBean(type:Int): MultiItemEntity
{
    when(type){
    TEXT -> return BeanA() IMG -> return BeanB()
    TEXT_IMG -> return BeanC() else -> return BeanA()
    }
    }
}
class MultipleItemQuickAdapter(data: List<*>):
BaseMultiItemQuickAdapter<MultiItemEntity,BaseViewHolder>(data) {
init {addItemType(MultipleItem.TEXT,R.layout.text_view)
addItemType(MultipleItem.IMG,R.layout.image_view)
}
override fun convert(helper:
BaseViewHolder, item: MultipleItem) {
    }
}

10.7 谈谈你对原型模式的理解?

1.定义
用原型对象的实例指定创建对象的种类,并通过拷贝这些原型创建新的对象.
2.使用场景
(1)类初始化需要消耗比较多的资源,通过原型拷贝可以避免这些消耗
(2)当new一个对象需要非常繁琐的数据准备等,这时可以使用原型模式
(3)当一个对象需要提供给其他调用者使用,并且各个调用者都可能修改其值时, 通过原型模式拷贝多个对象供调用者使用,保护性拷贝Android 源码中例子:
Intent ,Intent的查找与匹配原型模式实质上就是对象拷贝,要注意深拷贝和浅拷贝问题.还有就是保护性拷贝,就是某个对象对外是只读的,为了防止外部对这个只读对象修改,通常可以通过返回一个对象的拷贝来实现只读的限制.
优点:
原型模式是在内存中的二进制流的拷贝,性能要比new一个对象好的多.减少了约束.
缺点:
直接在内存中拷贝,构造函数是不会执行的

10.8 请谈谈策略模式原理及其应用场景?

策略模式很像简单工厂模式,后者是根据业务条件创建不同的对象,前者是根据业务条件去使用不同的策略对象。区别看下图
简单工厂模式

策略模式

主要区别在工厂类和CashContext这个两个类工厂 类

public class ShapeFactory
{
public static Shape
createShape(String shape){
switch(shape){ case
"circle":
    return new Circle();
case "rectangle":
    return new Ractangle();
}
return null;

CashContext

class
CashContext{
Strategy strategy;
public CashContext(Strategy strategy ){ 
    this.strategy = strategy;
}
//操作业务
public void method(){ strategy.met}

区别:

  1. 工厂没有持有具体类的引用,策略构造器(CashContext)持有了具体类的引用
  2. 工厂根据条件创建不同的对象,此时的策略构造器(CashContext)要在activity中做条件判断(很明显这里 可以和简单工厂模式结合)可知简单工厂一旦新增了具体类,工厂类就要做修改,但是策略模式不用修改CashContext类,他直接替换具体 类。但是当我们需要根据条件创建具体类的时候,策略模式会使得要在activity中做条件判断来确定创建什么具体类。这里又想到了使用工厂来优化。结合简单工厂模式后的策略构造器(CashContext)
class CashContext{
Strategy strategy;
public CashContext(string condition){
switch (condition){
case "circle":
strategy = new CicleStrategy();
break;
case "rectangle":
strategy = new RectangleStrategy();
break;
    }
}
//操作业务
public void
method(){ strategy.method;}

10.9 静态代理和动态代理的区别,什么场景使用?

静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已 经存在了。
动态代理类:在程序运行时,运用反射机制动态创建而成。
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

动 态 代 理是 实 现 JDK 里 的InvocationHandler 接 口 的 invoke 方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理 对象。
还有一种动态代理CGLIB,代理的是类,不需要业务类继 承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。

10.10 谈一谈责任链模式的使用场景?

责任连模式定义: 将多个对象连成一条链,并沿着这条链传递该请求,只到有对象处理该请求为止。使多个对象都有 机会处理请求,从而避免了请求的发送者和接受者之间的 耦合
关系。Android中责任链场景:
1) Android 源码中对于事件分发是基于该模式,Android 会将事件包装成一个事件对
象从ViewTree的顶部至上而下的分发传递,只到 有View处理该事件为止。
2)OkHttp 的拦截器也是基于责任链模式,用户请求和服务器返回数据,会经过内置拦截 器链逐级的处
理 。

计算机网络方面

11.1 请简述 Http 与 Https 的区别?

HTTP协议传输的数据都是未加密的,也就是明文的,因此 使用HTTP协议传输隐私信息非常不安全,为了保证这些隐 私数据能加密传输,于是网景公司设计了SSL(Secure SocketsLayer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。

1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
最后一点在Android 9.0 如果用http进行传输,需要在application节点下设置

android:usesCleartextTraffic="true"

11.2 说一说https,udp,socket区别?

https协议需要到CA申请证书。
http是超文本传输协议,信息是明文传输;https 则是具有安全性的ssl加密传输协议。
http和https使用的是完全不同的连接方式,用的端口也不 一样,前者是80,后者是443。
http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。http默认使用80端口,https默认使用443端口TCP:
传送控制协议(Transmission Control Protocol)
UDP:用户数据报协议 (UDP:User Datagram Protocol)
socket:
这是为了实现以上的通信过程而建立成来的通信管道,其真实的代表是客户端和服务器端的一个通信进程,双方进程通过socket进行通信,而通信的规则采用指定的协议。
socket只是一种连接模式,不是协议,socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),
通过Socket,我们才能使用TCP/IP协议。tcp、udp,简单 的说虽然不准确)是两个最基本的协议,
(很多其它协议都是基于这两个协议如,http就是基于tcp 的,.用socket可以创建tcp连接,也可以创建udp连接,这意味着,用socket可以创建任何协议的连接,因为其它 协议都是基于此的。

11.3 请简述一次http网络请求的过程?

这个看OKHTTP的EventListerner就知道了。这里总结一张okhttp的回调表格。详细的需要自己阅读源码注释哦

请求步骤 含义
dnsStart DNS解析开始
dnsEnd DNS解析结束
connectStart TCP连接开始
secureConnectStart 建立TLS安全信道开始
secureConnectEnd 信道建立结束
requestHeadersStart 发送首部字段开始
requestHeadersEnd 发送首部字段结束
requestBodyStart 发送请求体开始
requestBodyEnd 发送请求体结束
responseHeadersStart 接受首部开始
responseHeadersEnd 接受首部结束
responseBodyStart 接受响应体开始
responseBodyEnd 接受响应ti结束
connectEnd TCP连接断开

另外截一张postman上的图

11.4 谈一谈TCP/IP三次握手,四次挥手?

常见的 TCP 中的头部数据表示
ACK:该位为 1 时,「确认应答」的字段变为有效,
TCP 规定除了最初建立连接时的 SYN包之外该位必须设置为 1
SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定
RST:该位为 1时,表示 TCP 连接中出现异常必须强制断开连接
FIN:该 位 为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN位为 1 的 TCP 段

TCP 三次握手

一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态

  • 第一个报文—— SYN

      报文客户端会随机初始化序号( client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN标志位置为 1,表示 SYN报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
    
  • 第二个报文 —— SYN + ACK

        报文服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号( server_isn),将此序号填入TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD 状态。
    
  • 第三个报文 —— ACK 报文

      客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK标志位置为 1 ,其次「确认应答号」字段填入 server_isn+1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于ESTABLISHED 状态。服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态,此时 TCP 建立结束,双方可以收发数据。
    

为什么是三次握手?不是两次、四次?
三次握手才能保证双方具有接收和发送的能力
三次握手才可以阻止重复历史连接的初始化
三次握手才可以同步双方的初始序列号
三次握手才可以避免资源浪费

TCP 四次挥手过程

客户端主动关闭连接 —— TCP 四次挥手

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。

  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入状态。CLOSED_WAIT

  • 客户端收到服务端的 ACK 应答报文后,之后进入FIN_WAIT_2 状态。

  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入LAST_ACK 状态。

  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态

  • 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。

  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

客户端和服务端都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT状态。

为什么挥手需要四次?

回顾上方四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

11.5 为什么说Http是可靠的数据传输协议?

HTTP是属于应用层的协议,TCP(传输控制协议)和UDP(用户数据报协议)是属于传输层的协议。
我们都知道TCP协议是面向连接的,每次进行连接都要进行三次握手和四次挥手,所以它的连接是可靠的。而HTTP 是在TCP上层的协议,所以它也是可靠的。

那为什么TCP可靠?
首先来讲一下网络的分层,因特网协议可以分为五层,分别是:

应用层->传输层->网络互联层->网络访问层->物理层
或许你觉得很抽象,但是通过栗子你就会发现并没有那么复杂。如访问一个Http请求: http://45.124.252.66:9090/main/
怎么访问到这个网站呢?首先我们需要通过网络,可能是移动网或者宽带网等(这就是物理层,它是一个传输介 质),
然后找到对应那一台被我们访问的服务器的mac地址(网络访问层)进行连接,再匹配它的IP(网络互联层)是否对应,确定了主机后,再通过端口号9090(传输层)访问对应的进程,由于一个进程里面有很多业务模块,而我们需要访问main模块(应用层),最终通过不同层来实现网站的访问。

每个层都是相互独立,并且向下依赖,而传输层是能确定唯一主机的,因为我们可以通过mac地址、host和端口来 确定唯一的一台访问主机上面的进程。或许有的人会问, 那如果网络中断呢?那不就不可靠了吗,我们常说的网络中断是属于物理层,由于是向下依赖,传输层的建立是依赖于下面的三层(网络互联层、网络访问层、物理层)已经连接成功,
如果下面的层都没有连接成功,也就没有传输层这一说了,所以传输层协议是一个“靠谱”的协议。我们通过分层了解了传输层是“靠谱”的协议,那么怎么保证它是可靠的呢?

那就要讲到三次握手和四次挥手的作用了。
三次握手就是在建立连接之前需要客户端需要先给服务端发出SYN©报文,当服务器收到后需要返回客户端ACK=SYN©+1,并且传输自己生成的SYN(s)给客户端,客户端收到后进入已连接状态,需要再回一个ACK=SYN(s)+1 给服务器,服务器收到ACK后也进入了连接状态,这就是一个三次握手的过程,通过双方进行三次通信保证此时双方都已经进入准备状态。

四次挥手就是在结束连接的时候,客户端会发送FIN©给服 务器,服务器收到后回复客户端ACK=FIN©+1告知客户端收到客户端的结束请求了,这时客户端就会进入CLOSING(半关闭状态),等待服务器的结束请求。 在一段小延迟时间后,服务器也会发送一个FIN(s)请求给客户端, 客户端收到后发送ACK=FIN(s)+1给服务器,服务器收到ACK后就进入技术状态。客户端在等待2个MSL(避免服务器收不到ACK)后也进入结束状态。

在每次进行连接和断开连接都需要经过复杂的三次握手和四次握手,从而保证了每个连接都是可靠的,所以TCP协议是可靠的,而HTTP就是TCP上层的协议,所有连接都是基于TCP协议的。

在我们能够确定每个请求对应的唯一主机和端口号,并且通过Http协议添加响应的请求数据信息(如模块名字等)确定请求的代码位置,并且在每次请求都通过三次握手和四次挥手保证连接的可靠性,所以一个Http请求是可靠的。

11.6 TCP/IP协议分为哪几层?TCP和HTTP分别属于哪一层?

四层

应用层 传输层 网络层 数据链路层

http是 应用层 tcp 是传输层 ip是网络层
http 每次请求 需要 三次握手四次挥手

三次握手
第一次 客户端发送seq 确定了 客户端的 发送能力 和服务端的接收能力
第二次 服务端返回 seq 和 ack 客户端确认了自己的发送能力和接收能力
第三次 客户端发送 ack 服务端确定了自己的发送能力由此进行数据传输

tpc断开时 需要四次挥手
第一次 客户端发送 fin 给服务端
第二次服务端收到 返回 ack 等于 甲乙通话中,甲告诉乙我已经说完了,乙说我知道了
然后中间可能还有传输内容 乙还有话对甲说
第三次 服务端发送fin给客户端
第四次 客户端发送ack给服务端 等于 乙告诉甲 我要说的话说完了,甲说知道了, 由此
双方挂断电话

tcp是基于连接的 所以相对可靠, udp是直接发送 速度快但是不可靠
tcp可靠基于三次握手和四次挥手,和ack(回执机制) 如果客户端给服务端发送数据后没收到回执,会在一定条件下重复发送, 并且他们在连接过程中中断 又会重新三次握手

http1.1 引入了 keepalive机制 长连接 不必每次请求 都是三次握手四次挥手, 而是在超时时间内利用同一个 连接
http2.0 把基于文本传输改为基于二进制传输 多路复用https是在 http的基础上加上ssl安全套接字 加入了认证加密增加了一定的安全性,但也不是完全安全.在app中需要将https证书改为严格模式,并且要提前将证书放在客户端,如果放在服务端下证书有可能被人抓走. https 如果不是严格模式 也是可以进行抓包的

Kotlin方面

12.1 请简述一下什么是 Kotlin?它有哪些特性?

kotlin和java一样也是一门jvm语言最后的编译结果都是.class文件,并且可以通过kotlin的.class文件反编译回去java代码,并且封装了许多语法糖,其中我在项目中常用的特 性有

1.扩展,(使用非集成的方式 扩张一个类的方法和变量):

比方说 px和dp之间的转换 之前可能需要写个Util现在,通过扩展Float的变量 最后调用的时候仅仅是 123.dp这样px转成dp了

2.lamdba表达式,函数式编程.

lamdba表达式并不是kotlin 的专利,java中也有,但是有限制, 像setOnClickListener一样, 接口方法只有一个的情况才能调用, 而在kotlin中对接口的lambda也是如此,有这样的限制,但是他更推荐你使用闭包的方式而不是实现匿名接口的方式去实现这样的功能,闭包对lambda没有接口这么多的限制,另外就是函数式编程 在java8中提供了streamApi 对集合进行map sort reduce等等操作,但是对androidapi有限制,为了兼容低版本,几乎不可能使用streamApi

3.判空语法 省略了许多if xxx==null 的写法 也避免了空指针异常

aaa?.toString ?: “空空如也” 当aaa为空的时候 它的值被"空空如也"替代aaa?.let{ it.bbb}

当aaa不为空时 执行括号内的方法

4.省略了findViewById

使用kotlin 就可以直接用xml中定义的id 作为变量获取到这个控件,有 了 这 个 butterknife就可以淘汰了,使用databinding也能做到,但是,非常遗憾,databinding的支持非常不好,每次修改视图,都不能及时生成,经常要rebulid才能生成.

5. 默认参数 减少方法重载

fun funName(a :Int ,b:Int =123)
通过如上写法 实际在java中要定义两个写法 funName(a)和funName(a,b)

6. kotlin无疑是android将来语言的趋势,我已经使用kotlin

一年了,不仅App工程中使用,而且封装的组件库也是用kotlin,另外说明,kotlin会是apk大小在混淆后增加几百k.

12.2 Kotlin 中注解 @JvmOverloads的作用

@JvmOverloads注解的作用就是:在有默认参数值的方法加上
@JvmOverloads注解,则Kotlin就会暴露多个重载方法。可以减少写构造方法。
例如:没有加注解,默认参数没有起到任何作用。

fun f(a: String, b: Int = 0, c: String="abc")
{
.
..
}

那相当于在java中:void f(String a, int b, String c){}
如果加上注解@JvmOverloads ,默认参数起到作用

@JvmOverloads
fun f(a: String, b: Int = 0, c: String="abc")
{
.
..
}

相当于Java中:三个构造方法,

void f(String a)
void f(String a, int b)
void f(String a, int b, String c)

12.3 Kotlin中List与MutableList的区别?

List返回的是EmptyList,MutableList返回的是一个ArrayList,查看EmptyList的源码就知道了,根本就没有提 供add 方法。

internal object EmptyList : List, Serializable,
RandomAccess {
private const val serialVersionUID: Long  = -7390468764508069838L
override fun equals(other: Any?): Boolean = other is
List<*> && other.isEmpty() override fun hashCode():
Int = 1
override fun toString(): String = "[]"
override val size: Int get() = 0 override fun
isEmpty(): Boolean = true override fun contains(element: Nothing):
Boolean = false override fun containsAll(elements:
Collection<Nothing>): Boolean = elements.isEmpty()
override fun get(index: Int): Nothing = throw
IndexOutOfBoundsException("Empty list doesn't contain
element at index $index.")
override fun indexOf(element: Nothing): Int = -1
override fun lastIndexOf(element: Nothing):
Int = -1
override fun iterator(): Iterator<Nothing> =
EmptyIterator
override fun listIterator(): ListIterator<Nothing>
= EmptyIterator override fun listIterator(index:Int): ListIterator<Nothing> {
if (index != 0) throw IndexOutOfBoundsException("Index: $index")
    return EmptyIterator
}
override fun subList(fromIndex: Int, toIndex: Int): List<Nothing> {
if (fromIndex == 0 && toIndex == 0) return this throw IndexOutOfBoundsException("fromIndex:$}fromIndex, toIndex: $toIndex")
private fun readResolve(): Any = EmptyList

12.4 Kotlin中实现单例的几种常见方式?

饿汉式:

object StateManagementHelper {
fun init() {
//do some initialization works
    }
}

懒汉式:

class StateManagementHelper private constructor(){
companion object { 
private var instance:
  @StateManagementHelper? = null
{
Synchronized get()
if (field == null) field = StateManagementHelper()
    return field
    }
}
fun init() {
    //do some initialization works
    }
}

双重检测:

class StateManagementHelper private
constructor(){
companion object {
val instance: StateManagementHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED)
{
    StateManagementHelper() }
}
fun init() {
//do some initialization works
    }
}

静态内部类:

class StateManagementHelper private constructor(){
companion object
{
    val INSTANCE = StateHelperHolder.holder
}
private object StateHelperHolder {
val holder = StateManagementHelper()
}
fun init() {
//do some initialization works
    }
}

12.5 谈谈你对Kotlin中的 data 关键字的理解?相比于普通类有哪些特点?

Data Classes
We frequently create classes whose main purpose is to hold
data.
In such a class some standard functionality and utility
functions are often mechanically
derivable from the data. In Kotlin, this is called a data class
and is marked as data:
data class User(val name: String, val age:
Int)
The compiler automatically derives the following
members from all properties declared in the primary
constructor:
equals()/hashCode()pair;
toString()of the form "User(name=John, age=42)";
componentN() functions corresponding to the
properties in their order of declaration; copy()
function (see below).
To ensure consistency and meaningful behavior of the
generated code, data classes have to fulfill the following
requirements:
The primary constructor needs to have at least one
parameter;
All primary constructor parameters need to be
marked as valor var;
Data classes cannot be abstract, open, sealed or inner;
(before 1.1) Data classes may only implement
interfaces.
Additionally,themembersgenerationfollowsthese rules
with regard to the members inheritance:
If there are explicit implementations of equals() ,
hashCode()or toString()in the data class body or
final{: .keyword } implementations in a superclass,
then these functions are not generated, and the
existing implementations are used;
If a supertype has the componentN() functions that are
open{:.keyword}andreturncompatibletypes, the
corresponding functions are generated for the data class
and override those of the supertype. If the functions of
the
supertype cannot be overridden due to
incompatible signatures or being final, an error is
reported;
Deriving a data class from a type that already has a
copy(...)
function with a matching signature is
deprecated in
Kotlin 1.2 and is prohibited in Kotlin 1.3.
Providing explicit implementations for the
componentN()and copy()functions is not allowed.
Since 1.1, data classes may extend other classes (see Sealed
classes for examples).
On the JVM, if the generated class needs to have a
parameterless constructor, default values for all
properties have to be specified
(see Constructors).
data class User(val name: String = "", val
age: Int = 0)
Properties Declared in the Class Body
Note that the compiler only uses the properties defined
inside the primary constructor for the automatically
generated functions. To exclude a property from the
generated implementations, declare it inside the class body:
data class Person(val name: String)
var age: Int = 0
{
}
Only the property
will be used inside the
name
toString() , equals() , hashCode() , and implemecnotpayti(o)ns,
and there will only be one component function
Person
component1() . While two objects can have different ages,
they will be treated as equal.
data class Person(val name: String)
{
var age: Int = 0
}
fun main() {
//sampleStart
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
//sampleEnd
println("person1 == person2: ${person1 ==
person2}")
println("person1 with age ${person1.age}:
${person1}")
println("person2 with age ${person2.age}:
${person2}")
}
Copying
It'softenthecasethatweneedtocopyanobject altering some of its properties, but keeping the rest unchanged.This is what copy()function is generated for. For the User class above, its implementation would be as follows:
fun copy(name: String = this.name, age: Int =
this.age) = User(name, age)
This allows us to write:
val jack = User(name = "Jack", age = 1) val
olderJack = jack.copy(age = 2)
Data Classes and Destructuring Declarations
Component functions generated for data classes enable
their use indestructuringdeclarations:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // prints
"Jane, 35 years of age"

12.6 什么是委托属性?请简要说说其使用场景和原理?

属性委托
有些常见的属性操作,我们可以通过委托方式,让它实现,例如:
lazy 延迟属性: 值只在第一次访问的时候计算
observable 可观察属性:属性发生改变时通知
map 集合: 将属性存在一个map集合里面类委托
可以通过类委托来减少 extend 类委托的时,编译器回优使用自身重新函数,而不是委托对象的函数

interface Base{ 
    fun print()
}
case BaseImpl(var x: Int):Base{
    override fun print(){ print(x)
    }
}
// Derived 的 print 实现会通过构造函数的b对象来完成

12.7 请举例说明Kotlin中with与apply函数的应用场景和区别?

with不怎么使用,因为它确实不防空;经常使用的是runapply

  1. run 闭包返回结果是闭包的执行结果; apply 返回的是调用者本身。
  2. 使用上的差别: run 更倾向于做一些其他复杂逻辑操作,而 apply 更多的是对调用者自身配置。
  3. 大部分情况下,如果不是对调用者本身进行设置,我会使用 run。

12.8 Kotlin中 Unit 类型的作用以及与Java中Void 的区别?

Unit : Kotlin 中Any的子类, 方法的返回类型为Unit时,可以省略;
Void:Java中的方法无法回类型时使用,但是不能省略;Nothing:任何类型的子类,编译器对其有优化,有一定的 推导能力,另外其常常和抛出异常一起使用;

12.9 Kotlin 中 infix 关键字的原理和使用场景?

使用场景是用来修饰函数,使用了 infix 关键字的函数称为中缀函数,使用时可以省略 点表达式和括号。让代码看起来更加优雅,更加语义化。

原理不过是编译器在语法层面给与了支持,编译为 Java 代码后可以看到就是普通的函数调用。
kotlin 的很多特性都是在语法和编译器上的优化。

12.10 Kotlin中的可见性修饰符有哪些?相比于Java有什么区别?

kotlin存在四种可见性修饰符,默认是public。 private、protected、internal、public

  1. private、protected、public是和java中的一样的。
  2. 不同的是java中默认是default修饰符(包可见),而kotlin存在internal修饰符(模块内部可见)。
  3. kotlin可以直接在文件顶级声明方法、变量等。其中protected不能用来修饰在文件顶级声明的类、方法、变量等。
  4. 构造方法默认是public修饰,可以使用可见性修饰符修饰constructor关键字来改变构造方法的可见性。

12.11 你觉得Kotlin与Java混合开发时需要注意哪些问题?

kotlin调用java的时候 如果java返回值可能为null 那就必须加上@nullable 否则kotlin无法识别,也就不会强制你做非空处理,一旦java返回了null 那么必定会出现null指针异常,加上
@nullable注解之后kotlin就能识别到java方法可能会返回null,编译器就能会知道,并且强制你做非null处 理,这也就是kotlin的空安全

12.12 在Kotlin中,何为解构?该如何使用?

给一个包含N个组件函数(component)的对象分解为替 换等于N个变量的功能,而实现这样功能只需要一个表达式就可以了。

例如
有时把一个对象 解构 成很多变量会很方便,例如:

val (name, age) = person

这种语法称为 解构声明 。一个解构声明同时创建多个变量。 我们已经声明了两个新变量: name 和 age,并且可以独立使用它们:
println(name)
println(age)
一个解构声明会被编译成以下代码:

val name = person.component1() val
age = person.component2()

12.13 在Kotlin中,什么是内联函数?有什么作用?

Kotlin里 使 用 关 键 inline 来表示内联函数,那么到底什么是内联函数呢,内联函数有什么好处呢?

  1. 什么是内联inline?
    在 Java里是没有内联这个概念的,所有的函数调用都是普通方法调用,如果了解 Java 虚拟机原理的,可以知道Java方法执行的内存模型是基于 Java虚拟机栈的:每个方法被执行的时候都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧入栈、出栈的过程。
    也就是说每调用一个方法,都会对应一个栈帧的入栈出栈过程,如果你有一个工具类方法,在某个循环里调用很多次,那就会对应很多次的栈帧入栈、出栈过程。这里首先要记住的一点是,栈帧的创建及入栈、出栈都是有性能损耗的。下面以一个例子来说明,看段代码片段:
fun test() {
//多次调用 sum() 方法进行求和运算
println(sum(1, 2, 3))
println(sum(100, 200, 300))
println(sum(12, 34))
}
//......... 可能还有若干次
/**
求和计算
*/
fun sum(vararg ints: Int): Int {
    var sum = 0
    for (i in ints) {
        sum += i
    }
    return sum
}

在测试方法 test()里,我们多次调用了 sum()方法。为了避免多次调用 sum() 方法带来的性能损耗,我们期望的代码类似这样子的:

fun test()
var sum = 0
for (i in arrayOf(1, 2, 3)) { 
{   
    sum+=i
}
println(sum)
sum = 0
for (i in arrayOf(100, 200, 300)) { sum += i
}
println(sum)
sum = 0
for (i in arrayOf(12, 34))
{
sum += i
}
println(sum)
}

3次数据求和操作,都是在 test()方法里执行的,没有之前的 sum() 方法调用,最后的结果依然是一样的,但是由于减少了方法调用,虽然代码量增加了,但是性能确提升了。
那么怎么实现这种情况呢,一般工具类有很多公共方法,我总不能在需要调用这些公共方法的地方,把代码复制一遍吧,内联就是为了解决这一问题。

定义内联函数:

inline fun sum(vararg ints: Int): Int { 
var sum =0
for (i in ints)
{
    sum +=i
    return sum
   }
}

如上所示,用关键字 inline 标记函数,该函数就是一个内联函数。还是原来的 test() 方法,编译器在编译的时候, 会自动把内联函数 sum() 方法体内的代码,替换到调用该方法的地方。查看编译后的字节码,会发现 test() 方法里已经没了对 sum() 方法的调用,凡是原来代码里出现sum()方法调用的地方,出现的都是 sum()方法体内的字节码了。

  1. noinline
    如果一个内联函数的参数里包含 lambda表达式,也就是函数参数,那么该形参也是 inline 的,举个例子:
inline fun test(inlined: () -> Unit) {...}这里有个问题需要注意,如果在内联函数的内部,函数参数被其他非内联函数调用,就会报错,如下所示:
//内联函数
inline fun test(inlined: () -> Unit) {
//这里会报错
otherNoinlineMethod(inlined)
}//非内联函数
fun otherNoinlineMethod(oninline: () -> Unit) {}

要解决这个问题,必须为内联函数的参数加上 noinline 修饰,表示禁止内联,保留原有函数的特性,所以 test() 方法正确的写法应该是:

inline fun test(noinline inlined: () -> Unit)
{
otherNoinlineMethod(inlined)
}
  1. crossinline
    首先来理解一个概念:非局部返回。我们来举个栗子:
fun test()
{
innerFun {
//return 这样写会报错,非局部返回,直接退出 test() 函数。
return@innerFun //局部返回,退出 innerFun() 函数,这里必须明确退出哪个函数,写成 return@test 则会退出test() 函数
}
//以下代码依旧会执行
println("test...")
}
fun innerFun(a: () -> Unit) { a()
}

非局部返回我的理解就是返回到顶层函数,如上面代码中所示,默认情况下是不能直接 return 的,但是内联函数确是可以的。所以改成下面这个样子:

fun test() innerFun {
{
    return //非局部返回,直接退出 test() 函数。
}
//以下代码不会执行
    println("test...")
}
inline fun innerFun(a: () -> Unit) { a()
}

也就是说内联函数的函数参数在调用时,可以非局部返回,如上所示。那么 crossinline 修饰的 lambda 参数,可以禁止内联函数调用时非局部返回。

fun test()
{
    innerFun {
        return //这里这样会报错,只能 return@innerFun
}
//以下代码不会执行
    println("test...")
}
inline fun innerFun(crossinline a: () -> Unit) { a()}
  1. 具体化的类型参数 reified

这个特性我觉得特别牛逼,有了它可以少些好多代码。在Java 中是不能直接使用泛型的类型的,但是在 Kotlin 中确可以。举个栗子:

inline fun startActivity()
{
    startActivity(Intent(this, T::class.java))
}

使用时直接传入泛型即可,代码简洁明了:
startActivity()

12.14 谈谈kotlin中的构造方法?有哪些注意事项?

一、概要简述

  1. kotlin中构造函数分为主构造和 次级构造两类
  2. 使用关键词constructor标记构造函数,部分情况可省略
  3. init关键词用于初始化代码块,注意与构造函数的执行顺序,类成员的初始化顺序
  4. 继承,扩展时候的构造函数调用逻辑
  5. 特殊的类如data class、 object/componainobject、 sealed class等构造函数情况与继承问题
  6. 构造函数中的形参声明情况

二、详细说明

主/次 构造函数

  1. kotlin中任何class(包括object/data class/sealed class)都有一个默认的无参构造函数
  2. 如果显式的声明了构造函数,默认的无参构造函数就失效了。
  3. 主构造函数写在class声明处,可以有访问权限修饰符private,public等,且可以省略constructor关键字。
  4. 若显式的在class内声明了次级构造函数,就需要委托调用主构造函数。
  5. 若在class内显式的声明处所有构造函数(也就是没有了所谓的默认主构造),这时候可以不用依次调用主构造函数。例如继承View实现自定义控件时,三四个构造函数同时显示声明。

init初始化代码块
kotlin中若存在主构造函数,其不能有代码块执 行init起到类似作用,在类初始化时侯执行相关的代码
块。
1.init代码块优先于次级构造函数中的代码块执行。
2. 即使在类的继承体系中,各自的init也是优先于构造函数执行。
3. 在主构造函数中,形参加有var/val,那么就变成了成员属性的声明。这些属性声明是早于init 代码块的。

特殊类
1.object/companion object是对象示例,作为单例类或者伴生对象,没有构造函数。
2. data class要求必须有一个含有至少一个成员属性的主构造函数,其余方面和普通类相同。
3.sealed class只是声明类似抽象类一般,可以有主构造函数,含参无参以及次级构造等。

12.15 谈谈Kotlin中的Sequence,为什么它处理集合操作更加高效?

处理集合时性能损耗的最大原因是循环。集合元素迭代的次数越少性能越好。
我们写个例子:

list map { it ++ }
filter { it % 2 == 0 }
count { it < 3 }

反编译一下,你会发现:Kotlin编译器会创建三个while循 环 。
Sequences 减少了循环次数
Sequences提高性能的秘密在于这三个操作可以共享同一个迭代器(iterator),只需要一次循环即可完成。Sequences允许 map 转换一个元素后,立马将这个元素传递给 filter操作 ,而不是像集合(lists) 那样,等待所有的元素都循环完成了map操作后,用一个新的集合存储起来, 然后又遍历循环从新的集合取出元素完成filter操作。

Sequences 是懒惰的
上面的代码示例, map、 filter、 count 都是属于中间操作,只有等待到一个终端操作,如打印、sum()、average()、first()时才会开始工作,不信?你跑下下面的代码?

val list = listOf(1, 2, 3, 4, 5, 6) val
result = list.asSequence()
map{ println("--map"); it * 2 }
filter { println("--filter");it % 3 == 0 }
println("go~")
println(result.average())

扩展:Java8 的 Stream(流) 怎么样呢?

list.asSequence()
filter { it < 0}
map { it++ }
average()
list.stream()
filter { it < 0}
map { it++ }
average()

stream的处理效率几乎和Sequences一样高。它们也都是基于惰性求值的原理并且在最后(终端)处理集合。

12.16 请谈谈Kotlin中的Coroutines,它与线程有什么区别?有哪些优点?

说一下个人理解吧。先列出协程几个特点:

1,在单个进程内,多个协程串行执行,只挂起不阻塞,协程最终的执行还是在各个线程之中

2,由于不阻塞线程,异步任务是编译器主动交到线程池中执行。因此,在异步任务执行上,切换和消耗的资源都较少 。
3,由于协程是跨多个线程,并且能够保持串行执行;因 此,在处理多并发的情况上,能够比锁更轻量级。通过状态量实现

12.17 Kotlin中该如何安全地处理可空类型?

对于方法传入的参数直接通过if判断,例如:

fun a(tag: String?, type: String) { if
(tag != null && type != null){
// do something
    }
}

还有就是

a?.let{}
a?.also{}
a?.run{}
a?.apply{}

然后接着有一个疑问,假如同时判断两个变量,写成:

a?.let{
b?.let{
//do something
}
}

12.18 说说Kotlin中的Any与Java中的Object 有何异同?

同:
都是顶级父类

异:
成员方法不同
Any只声明了toString()、hashCode()和equals()作为成员方法。

我们思考下,为什么 Kotlin 设计了一个 Any ?
当我们需要和 Java 互操作的时候,Kotlin 把 Java 方法参数和返回类型中用到的 Object 类型看作 Any,这个 Any 的设计是 Kotlin 兼容 Java 时的一种权衡设计。
所有 Java 引用类型在 Kotlin 中都表现为平台类型。当在Kotlin中处理平台类型的值的时候,它既可以被当做可空类型来处理,也可以被当做非空类型来操作。

试想下,如果所有来自 Java的值都被看成非空,那么就容易写出比较危险的代码。反之,如果 Java值都强制当做可空,则会导致大量的 null 检查。综合考量,平台类型是一种折中的设计方案。

12.19 Kotlin中的数据类型有隐式转换吗?为什么?

不可隐式转换
在Java中从小到大,可以隐式转换,数据类型将自动提升。
下面以int为例
这么写是ok的

int a = 2312;
long b = a;
//那么在Kotlin中
//隐式转换,编译器会报错
val anInt: Int = 5
val ccLong: Long = anInt
//需要去显式的转换,下面这个才是正确的
val ddLong: Long = anInt.toLong()

12.20 Kotlin中集合遍历有哪几种方式?
对于如下集合
val list = mutableListOf(“a”,“b”,“c”,“d”,“e”,“f”,“g”)
kotlin中集合的遍历方式有下面几种
1.1、 通过解构的方式,可以方便的获取index和value

for ((index,value) in
list.withIndex()){ println("index = $ index , value = $ value")}
for (i in 0 .. list.size - 1){ println("index =i , value =$ ${list[i]}")
}

1、 until 左闭右开 [,)

for (i in 0 until list.size){ println("index =$i , value =${list[i]}")
}

2、 downTo 递减的循环方式 左闭右闭 [,]

for (i in list.size - 1 downTo0){ println("index = $i , value =${list[i]}")}

3、带步长的循环step可以和其他循环搭配使用

for (i in 0 until list.size step 2){ println("index=$i , value =${list[i]}")}

4、指定循环次数 it就代表了当前循环的计数,从0开始下面的语句循环了10次 每次的计数分别是 0,1…9

repeat(10){
println(it)
}

5、不需要数据的下标,直接for循环list中的每个item

for (item in list){
    println(item)
}