Android系统架构
Android 系统架构由5部分组成,分别是:Linux Kernel、Android Runtime、Libraries、Application Framework、Applications。
Activity 生命周期
在不同生命周期调用finish()
1、在Activity的onCreate()中调用finish()方法,则执行的生命周期方法顺序为:
onCreate() -> onDestroy()
2、在Activity的onStart()中调用finish()方法,则执行的生命周期方法顺序为:
onCreate() -> onStart() -> onStop() -> onDestroy()
3、在Activity的onResume()或onPostResume()中调用finish()方法,则执行的生命周期方法顺序为:
onCreate() -> onStart() -> onResume() -> onPostResume() -> onPause() -> onStop() -> onDestroy()
4.页面回收,重建
onPause -> onSaveInstanceState -> onStop -> onDestroy -> onCreate -> onStart -> onRestoreInstanceState -> onResume
activity三种状态
active:当Activity运行在屏幕前台(处于当前任务活动栈的最上面),此时它获取了焦点能响应用户的操作,属于活动状态,同一个时刻只会有一个Activity处于活动(Active)。
paused:当Activity失去焦点但仍对用户可见(如在它之上有另一个透明的Activity或Toast、AlertDialog等弹出窗口时)它处于暂停状态。暂停的Activity仍然是存活状态(它保留着所有的状态和成员信息并保持和窗口管理器的连接),但是当系统内存极小时可以被系统杀掉。
stoped:完全被另一个Activity遮挡时处于停止状态,它仍然在内存中保留着所有的状态和成员信息。只是对用户不可见,当其他地方需要内存时它往往被系统杀掉。
第一个Activity的启动顺序:onCreate()——>onStart()——>onResume()
当另一个Activity启动时:第一个Activity onPause()——>第二个Activity onCreate()——>onStart()——>onResume()——>第一个Activity onStop()
当返回到第一个Activity时:第二个Activity onPause()——> 第一个Activity onRestart()——>onStart()——>onResume()——>第二个Activity onStop()——>onDestroy()
从第二点可以看出,view是在onResume之后才绘制的,因此导致第一个view完全被遮挡而响应第一个activity的onStop
App进程的创建流程
App的进程是由zygote fork出来的,运行在Android Runtime,进程创建图如下:
当点击桌面应用图标时,会触发startActivity,startActivity发起进程便是Launcher所在进程。发起进程通过binder发送消息到system_server进程。
system_server进程检测到目标应用所在进程没有启动的话,就会调用Process.start来启动目标进程,该函数通过socket向zygote进程发送创建新进程的请求。
Zygote进程启动的时候运行了ZygoteInit.main函数,并进入runSelectLoop循环,该循环用来检测外部的socket请求,当运行Process.start的时候,该循环检测到请求,就会调用ZygoteConnection.runOnce函数,最后创建出新的目标进程。
View 事件一般要经过三个流程:分发(dispatchTouchEvent)、拦截(onInterceptTouchEvent)、消费(onTouchEvent)
dispatchTouchEvent:View 会先执行 OnTouchListener 中 onTouch 方法,如果这个监听器的方法返回 true 则不会将事件交由自身的 onTouchEvent 进行消费。
而 ViewGroup 跟 View 不同的是,它是直接将事件交由触摸位置上的子 View 的 dispatchTouchEvent 方法处理。
onInterceptTouchEvent:这个是 ViewGroup 独有的方法,在 dispatchTouchEvent 方法中调用,一般情况下返回 false(也就是不拦截),ViewGroup 如果想拦截触摸事件可以重写此方法返回 true,将事件交由自身的 onTouchEvent 方法处理。
onTouchEvent:一次触摸事件会产生 down(按下)、move(移动)、up(抬起)三个事件,这个方法将决定事件产生的效果,比如状态选择器的 Drawable 变换就是在这个方法中触发的,还有最常用的点击事件 onClick 也是在这里回调的。
总结:三个事件方法各有各的作用,分发是想把事件传递下去,拦截是不希望事件传递下去,而消费是对事件的一系列处理。通过这三个方法,我们可以控制触摸事件交由谁去处理,整个事件机制采用了责任链设计模式,事件会一层一层往下传递。
1. dispatchTouchEvent
return true; 表示该view内部消化掉了所有事件
return false: 表示事件在本层不再继续分发,并交由上层控件的onTouchEvent方法进行消费
return super.dispatchTouchEvent(ev) 默认事件将分发给本层事件拦截onInterceptTouchEvent
2. onInterceptTouchEvent
return true: 表示将事件进行拦截,并将拦截到的事件交由本层的 onTouchEvent进行处理
return false: 表示不进行事件拦截处理,事件得以分发到子View
return super.onInterceptTouchEvent(ev) 默认表示不拦截该事件,并将事件传递给下一层的view的dispatchTouchEvent;
3. onTouchEvent
return true: 表示onTouchEvent处理完事件后消费了此事件
return false : 表示不响应事件,那么该事件将不断的向上层view的onTouchEvent方法传递;
return super.onTouchEvent(ev) 表示不响应事件,与return false一样;
IPC
IPC方式有多少种:
传统的IPC方式有Socket、共享内存、管道、信号量等
安卓特有的是Binder,其他系统也有自己定义的比如window的wfc(Windows Communication Foundation)
Bundle:Intent中传递Bundle数据,Bundle实现了Parcelable接口,可以在不同进程间传输。
文件共享:进程通过读写同一个文件交换数据
AIDL
ContentProvider
Messenger:Messenger是一种轻量级的IPC方案,他的底层实现是AIDL
binder是一种进程间通讯(IPC,Interprocess Communication)的机制
binder的定义
每个进程拥有自己的独立虚拟机,系统为他们分配的地址空间都是互相隔离的。
如两个进程需要进行通讯,则需要使用到内核空间做载体,内核空间是所有进程共享的一块内存区域。 而用户空间切到内核空间需要使用到系统api ioctl进行通讯。内核获取用户的数据需要使用copy_from_user,内核将数据发送给其他进程需要使用copy_to_user,这两个方法是有性能开销的,对于socket就是使用的这种模式
为了减少这部分的开销,内核提供了binder,binder只需要一次拷贝就可以实现进程通讯。
binder的是实现原理:
在内核空间和用户空间都开辟一块虚拟内存区域同时指向一块物理地址,这样内核需要传递数据给用户空间时,只需要将数据拷贝到对应的虚拟内存地址中,用户可以通过虚拟内存映射关系,获取到内核中的数据,实现了一次拷贝通讯。
传统的数据拷贝方式如socket:
用户空间---->内核空间:`copy_from_user `
内核空间---->用户空间:`copy_to_user`
第一遍是进程A复制到Socket通道,第二遍是Socket通道复制到进程B,两次拷贝。
而binder使用mmap机制
在内核空间和用户空间中间使用物理地址开辟了一个映射关系
内核空间调用copy_from_user会直接将数据拷贝到内核空间并反馈到映射后的物理地址上,
由于用户空间和物理地址也有个映射关系,用户空间可以直接通过映射的虚拟地址指针访问到写入物理地址的数据。
这就是binder一次拷贝的原理
有四个角色,其实很好理解,两个进程、一个Binder内核空间、一个ServiceManager服务。两个进程时采用的C/S结构一个客户端一个服务端。
Client、Server和Service Manager实现在用户空间中,Binder驱动程序实现在内核空间中
1、首先,Server进程要向SM注册;告诉自己是谁,自己有什么能力;在这个场景就是Server告诉SM,它叫A,它有一个object对象,可以执行add 操作;于是SM建立了一张表:A这个名字对应进程Server
2、然后Client向SM查询:我需要联系一个名字叫做A的进程里面的object对象;这时候关键来了:进程之间通信的数据都会经过运行在内核空间里面的驱动,驱动在数据流过的时候做了一点手脚,它并不会给Client进程返回一个真正的object对象,而是返回一个看起来跟object一模一样的代理对象objectProxy,这个objectProxy也有一个add方法,但是这个add方法没有Server进程里面object对象的add方法那个能力;objectProxy的add只是一个傀儡,它唯一做的事情就是把参数包装然后交给驱动。
3、驱动收到消息,发现是这个objectProxy;一查表就明白了:我之前用objectProxy替换了object发送给Client了,它真正应该要访问的是object对象的add方法;于是Binder驱动通知Server进程,调用你的object对象的add方法,然后把结果发给我,Sever进程收到这个消息,照做之后将结果返回驱动,驱动然后把结果返回给Client进程;于是整个过程就完成了。
4、由于驱动返回的objectProxy与Server进程里面原始的object是如此相似,给人感觉好像是直接把Server进程里面的对象object传递到了Client进程;因此,我们可以说Binder对象是可以进行跨进程传递的对象
5、这里隐藏了SM这一部分驱动进行的操作;实际上,由于SM与Server通常不在一个进程,Server进程向SM注册的过程也是跨进程通信,驱动也会对这个过程进行暗箱操作:SM中存在的Server端的对象实际上也是代理对象,后面Client向SM查询的时候,驱动会给Client返回另外一个代理对象。Sever进程的本地对象仅有一个,其他进程所拥有的全部都是它的代理。
binder架构上面使用的是C/S架构:
三要素:
客户端,服务端和ServiceManager
整体过程:
1.注册服务 2.获取服务 3.使用服务
binder的大小限制
对于内核可以传输的是4M,但是应用层限制在1M-8K范围内,这就是在进程间传输过大的数据会导致崩溃的原因
为什么zygote进程跟其他进程通讯使用socket而不是binder
Binder虽然在内核,但是提供服务的是ServiceManager,这个时候如果要给AMS提供Binder IPC就需要等ServiceManager先初始化好,这个是没办法保证的,如果要保证这个先后顺序又要搞多一套进程通讯就更麻烦了。
另外,由于Zygote进程只与少数几个进程进行通讯,使用Socket通讯的开销相对较小,因此选择Socket通讯更加合适。而且这里面是优化过的LocalSocket效率会更高。
SystemServer->run()->createSystemContext():创建了系统的ActivityThread对象,运行环境mSystemContext、systemUiContext。
SystemServer->run()->startBootstrapServices()->ActivityManagerService.Lifecycle.startService():AMS在引导服务启动方法中,通过构造函数new ActivityManagerService()进行了一些对象创建和初始化(除activity外3大组件的管理和调度对象创建;内存、电池、权限、性能、cpu等的监控等相关对象创建),start()启动服务(移除进程组、启动cpu线程、注册权限、电池等服务)。
SystemServer->run()->startBootstrapServices()->setSystemServiceManager()、setInstaller()、initPowerManagement()、setSystemProcess():AMS创建后进行了一系列相关的初始化和设置。
setSystemProcess():将framework-res.apk的信息加入到SystemServer进程的LoadedApk中,并创建了SystemServer进程的ProcessRecord,加入到mPidsSelfLocked,由AMS统一管理。
SystemServer->run()->startOtherServices():AMS启动后的后续工作,主要调用systemReady()和运行调用时传入的goingCallback。
systemReady()/goingCallback:各种服务或进程等AMS启动完成后需进一步完成的工作及系统相关初始化。 桌面应用在systemReady()方法中启动,systemui在goingCallback中完成。当桌面应用启动完成后,发送开机广播ACTION_BOOT_COMPLETED,到此为止。
使用步骤
服务端
1 创建一个服务端 也就是说创建一个Service
2 继承Service,重写onBind函数
3 创建内部类继承Aidl接口重写Aidl接口方法
客户端
1. 把服务端的aidl 接口复制过来.他的包名必须跟服务端的包名一致,
2 通过bindService绑定服务端的服务
BroadcastReceiver广播接收器生命周期
生命周期只有十秒左右,如果在onReceive()内做超过十秒内的事情,就会报ANR(Application No Response)程序无响应的错误信息。
它的生命周期为从回调onReceive()方法开始到该方法返回结果后结束。
动态注册过的BroadcastReceiver需要解注册,否则可能造成内存泄漏。因为动态注册的BroadcastReceiver持有注册他的Activity的引用。
动态注册广播。
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
},new IntentFilter("Action.xxxxx"));
发送广播:
context.sendBroadcast(intent);
静态注册广播:
在AndroidManifest里面注册广播。
本地广播和全局广播的区别
本地广播通过LocalBroadcastManager来实现广播的注册和发送,只在本应用范围内传播,不必担心隐私数据的泄漏。
// 注册 接收
LocalBroadcastManager.getInstance(context).registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
}, new IntentFilter("Action.xxx"));
// 发送
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
全局广播会在整个系统内发布。
Service服务生命周期
Service完整的生命周期从调用onCreate()开始直到调用onDestroy()结束。
Service有两种使用方法:
(1)以调用Context.startService()启动,而以调用Context.stopService()结束。
(2)以调用Context.bindService()方法建立,以调用Context.unbindService()关闭
startService和bindService的区别
生命周期的区别:
生命周期:
onCreate → startCommand → onDestroy
onCreate → onBind→onUnBind→ onDestroy
静态绑定对应着startService;动态绑定对应着bindService,静态有自己独立的生命周期,动态会依附activity等组件的生命周期。
1.如果先startService,再bindService:
在bind的Activity退出的时候,Service会执行unBind方法而不执行其onDestory方法,因为有startService方法调用过,
所以Activity与Service解除绑定后会有一个与调用者没有关连的Service存在。
2.如果先bindService,再startService,再调用Context.stopService
Service的onDestory方法不会立刻执行,因为有一个与Service绑定的Activity,但是在Activity退出的时候,会执行其(Service的)onDestory方法,
如果要立刻执行stopService,就得先解除绑定。
当一个服务没被onDestory()销毁之前,只有第一个启动它的客户端能调用它的onBind()和onUnbind()。
结论:无论是先start还是先bind,再结束时执行onUnbind和stopService;只要还有一种启动方法存在,Service就会继续存活。
Activity的四种启动模式:
standard
模式启动模式,每次激活Activity时都会创建Activity,并放入任务栈中。
singleTop
如果在任务的栈顶正好存在该Activity的实例, 就重用该实例,否者就会创建新的实例并放入栈顶(即使栈中已经存在该Activity实例,只要不在栈顶,都会创建实例)。
singleTask
如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的onNewIntent())。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移除栈。如果栈中不存在该实例,将会创建新的实例放入栈中。
singleInstance
在一个新栈中创建该Activity实例,并让多个应用共享改栈中的该Activity实例。一旦改模式的Activity的实例存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例,其效果相当于多个应用程序共享一个应用,不管谁激活该Activity都会进入同一个应用中。
OkHttp最核心的工作是在 getResponseWithInterceptorChain() 中进行,在进入这个方法分析之前,我们先来了 解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。责任链顾名思义就是由一系列的负责者构成的一个链条,类似于工厂流水线。
责任链设计模式
它为请求创建了一个接收者对象的链。为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链,当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。(责任链模式也叫职责链模式)
在责任链模式中,每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象 决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的 情况下动态的重 新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象 所接受。
默认的5大拦截器有哪些?
RetryAndFollowUpInterceptor(重试和重定向拦截器)
第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求。主要就是完成两件事情:重试与重定向。
请求阶段发生了 RouteException 或者 IOException会根据进行recover 判断是否重新发起请求。
BridgeInterceptor(桥接拦截器)
补全请求,并对响应进行额外处理。比如设置请求内容长度,编码,gzip压缩,cookie等补全请求头,获取响应后保存Cookie等操作
CacheInterceptor(缓存拦截器)
请求前查询缓存,获得响应并判断是否需要缓存。
在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存 在Get请求的缓存)
ConnectInterceptor(链接拦截器)
与服务器完成TCP连接(Socket)
这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。
CallServerInterceptor(请求服务拦截器)
与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)
这个请求头代表了在发送请求体之前需要和服务器确定是 否愿意接受客户端发送的请求体。
OkHttp和HttpClient
OkHttp和HttpClient都是Java中常用的HTTP客户端库,它们都有各自的优缺点。
OkHttp的优点:
1. 性能高:OkHttp使用了连接池技术,可以复用TCP连接,减少了连接建立的时间,提高了性能。
2. 简单易用:OkHttp的API设计简单易用,支持同步和异步请求,支持链式调用,使用起来非常方便。
3. 支持SPDY和HTTP/2:OkHttp支持SPDY和HTTP/2协议,可以提高网络传输效率。
4. 支持拦截器:OkHttp支持拦截器,可以在请求和响应的过程中进行一些自定义的操作,比如添加请求头、记录日志等。
5. 支持缓存:OkHttp支持缓存,可以减少网络请求的次数,提高性能。
HttpClient的优点:
1. 稳定可靠:HttpClient是Apache开源组织的产品,经过了长时间的使用和测试,稳定性和可靠性都比较高。
2. 功能丰富:HttpClient提供了很多高级功能,比如连接池、Cookie管理、代理设置、SSL/TLS支持等。
3. 支持HTTP/1.1:HttpClient支持HTTP/1.1协议,可以在网络传输效率和兼容性之间做出权衡。
4. 支持多种认证方式:HttpClient支持多种认证方式,比如基本认证、摘要认证、NTLM认证等。
5. 可扩展性强:HttpClient提供了很多扩展点,可以方便地进行二次开发和定制。 总的来说,OkHttp更适合移动端开发,因为它轻量级、性能高、简单易用;HttpClient更适合服务器端开发,因为它功能丰富、稳定可靠、可扩展性强。
retrofit的好处就是降低的网络接口请求的代码
Retrofit 使用起来非常方便,共包含以上四个步骤
书写网络请求的接口类
构建 Retrofit 对象
使用retrofit 对象 获得实现接口的实体类对象
调用接口方法,实现网络请求
android存储的5种方式
SharedPreferences存储数据(如app的配置信息)
SharedPreferences是一个轻量级的存储类,基于XML文件存储的key-value键值对数据,通常用来存储一些简单的配置信息。
文件存储数据(I/O流 如保存网络图片)
ContentProvider
ContentProvider不仅是Android四大组件之一,还是Android五大存储方式之一,当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同。
使用ContentProvider共享数据的好处是统一了数据访问方式。 Android内置的许多数据都是使用ContentProvider形式,供开发者调用的(如视频,音频,图片,通讯录等)。
网络存储数据
网络存储方式,需要与Android 网络数据包打交道
SQLite数据库存储数据(如保存网络数据)
如果事务操作成功,则该事务进行的所有数据均会提交,更改成为数据库中的永久组成部分;如果事务操作失败,则所有操作均取消,所有数据的更改均清除
特性:
原子性:事务是数据库的逻辑工作单元
一致性:事务执行结果必须是使数据库从一个一致性状态变到另一个一致性状态
隔离性:一个事务的执行不能被其他事务干扰
持续性:一个事务一但提交,它对数据库中数据的改变就应该是永久性的
事务的语句
开始事物:BEGIN TRANSACTION
提交事物:COMMIT TRANSACTION
回滚事务:ROLLBACK TRANSACTION
隔离级别
四个级别(低到高;性能由高到低):
Read uncommitted:未提交读。事务A读取数据时,其他事务可读取该数据,导致事务A提交数据后,其他事务读取的数据失效
Read committed:提交读。事务A提交数据时,其他事务在其之前也提交数据。导致事务A提交数据时,读取到的数据失效
Repeatable read:重复读。事务A读取数据后,其他事务不能读取该数据
Serializable:序列化
脏读:
指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
不可重复读:
指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。
幻读:
并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。
更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
性能分析
抓trace的方法
在需要测试的函数前添加
Debug.startMethodTracing("/sdcard/xxx.trace");
然后在测试结束后添加
Debug.stopMethodTracing();
找到sd卡中的xxx.trace文件,导入AS,分析子函数耗时。
性能分析的指标:
CPU使用率:测量应用程序占用CPU的百分比。
GPU使用率:测量应用程序占用GPU的百分比,GPU主要决定应用的图形绘制。
内存使用率:测量应用程序占用内存的百分比。
硬盘读写速度:测量应用程序从存储器读取和写入数据的速度。
UI响应时间:测量应用程序响应用户交互的时间。
APP响应时间:作为应用成功的核心特征之一,移动应用响应时间至关重要。用户对较长的加载时间和缓慢的处理零容忍。
流畅度:FPS,可通过工具或者安卓开发者模式中的设置查看到绘制耗时的柱状图
工具:
Android Studio自带的Profiler:可用于跟踪应用程序的CPU和内存使用率,并分析应用程序的性能瓶颈。
Monkey:一种随机事件测试工具,可模拟用户的随机操作,如点击、滑动和缩放,以测试应用程序的稳定性和响应性能。
Selendroid:一种自动化测试工具,可自动执行UI测试、功能测试和性能测试。
JMeter:一种功能强大的性能测试工具,可用于测试Android应用程序的性能,包括CPU和内存使用率、网络延迟等。
Experitest:一种专门针对移动应用程序的测试工具,可用于测试Android应用程序的性能、稳定性、安全性等。
ANR
什么是ANR?
ANR,全称为Application Not Responding。在Android 中,如果你的应用程序有一段时间没有响应,系统会向用户显示一个对话框,这个对话框称作应用程序无响应对话框。
造成ANR的原因有哪些
首先明确一点:只有当应用程序的UI线程响应超时才会引起ANR
当前的事件没有机会得到处理
当前的事件正在处理,但是由于耗时太长没能及时完成
分类
KeyDispatchTimeout:
原因是View的按键事件或触摸事件在5秒内无法得到响应。一般就是主线程卡住了,导致无法响应事件。
Input事件处理慢触发的ANR
BroadcastTimout
原因是广播接收者(BrocastReceiver)的onReceive()函数在特定时间内(10秒)无法完成处理。
ServiceTimeout
原因是服务(Service)的各个声明周期函数在特定时间(20秒)内无法完成处理。
ContentProvider Timeout:
provider在publish后需要在10秒内完成。
典型场景:
应用程序UI线程存在耗时操作,例如在UI线程中进行网络请求,数据库操作或者文件操作等,可能会导致UI线程无法及时处理用户输入等。
应用程序UI线程等待子线程释放某个锁,从而无法处理用户的请求的输入。
耗时操作的动画需要大量的计算工作,可能导致CPU负载过重。
分析方法:
anr的trace data/anr 路径下
看时间节点的业务日志,logcat
如何分析ANR
系统针对ANR,会生成一个trace.txt文件
log文件找到tid=1的线程,该线程为主线程。
可以看到是什么原因导致的ANR,查看主线程的状态,是主线程等待了、死锁了还是其他什么原因。
然后查看main log,搜索ANR关键字,查看当前的CPU状态、IO wait、message的延时时间、block、memory leak等。
如果是死锁了,可以通过trace文件看到对象被哪个线程占用着,如果内存泄漏了,需要dump内存文件进行分析。
我们也可以借助于第三方工具BlockCanary在测试的时候进行慢操作分析,该组件利用了Looper类的loop函数的特性,如下所示,loop函数在调用dispatchMessage函数(会调用handleMessage)的时候会可以打印handleMessage的执行日志,通过自定义Printer类,将printer类设置到当前looper对象,这样在就可以打印每个message的执行时间,进而找到慢操作。
如何避免ANR
数据库操作或者文件读写等IO耗时操作使用异步;
UI线程只做UI的事;
广播如果需要耗时操作可以在onReceiver启动一个IntentService;
如果要使用Service,尽量使用IntentService;
线程间交互使用Handler来做;
后台线程使用Background优先级;
图片质量劲量降低,如使用565图片;
界面层次布局降低,减少绘制时间;
数据库打开并发读写模式;release环境关闭log。
IntentService
IntentService是一个基于Service的一个类,用来处理异步的请求。
可以通过startService(Intent)来提交请求,该Service会在需要的时候创建,当完成所有的任务以后自己关闭,且请求是在工作线程处理的。
IntentService负责执行后台的耗时的任务,也因为IntentService是服务的原因,这导致它的优先级比单纯的线程要高。
优点:
一方面不需要自己去new Thread了;另一方面不需要考虑在什么时候关闭该Service。
IntentService会自己创建一个Handler线程,用于处理。
IntentService与Service的区别
IntentService可以在onHandlerIntent方法中执行耗时操作
Service不可以执行耗时操作
Android内存泄漏
通俗的来说就是堆中的一些对象已经不会再被使用了,但垃圾收集器(GC)却无法将它们从内存中清除。
阻塞内存资源并随着时间的推移降低系统性能,最终的结果将会使应用程序耗尽内存资源,无法正常服务,导致程序崩溃,抛出java.lang.OutOfMemoryError异常。
资源性对象未关闭
资源性对象,比如Cursor,File等Closeable对象,这些对象需要使用缓存区。如果只是将对象赋值为null,不去调用close,会导致缓存未关闭,而得不到释放,造成泄漏。使用try-with-resource。
注册对象未注销
如果注册对象不再使用时,未及时注销,会导致订阅者列表中维持这对象的引用,阻止垃圾回收,导致内存泄露。常见场景:动态注册BroadcastReceiver,注册PhoneStateListener,注册EventBus等等一般会在onDestory里面解注册。
非静态内部类的静态实例
非静态内部类持有外部类实例的引用,若非静态内部类的实例是静态的,便拥有app存活期整个生命周期,长期持有外部类的引用,阻止外部类实例被回收。
单例模式引起的内存泄露
由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,如果让单例无限制的持有Activity的强引用就会导致内存泄漏
解决方案:使用Activity的弱引用,或者没特殊需求时使用Application Contex
Handler临时性内存泄露
非静态Handler持有Activity或Service的引用,Message中的target指向Handler实例,所以当Message在MessageQueue中排队,长时间未得到处理时,Activity就不会被回收,导致临时性内存泄露。
解决方案:
(1)使用静态Handler内部类,然后对Handler持有的对象(Activity或Service)使用弱引用
(2)在onDestroy()中移除消息队列中的消息 mHandler.removeCallbacksAndMessages(null)
类似的:AsyncTask内部也是Handler机制,也存在同样的临时性内存泄露风险
容器中对象未及时清理导致内存泄露
容器类一般拥有较长的生命周期,若内部不再使用的对象不及时清理,内部对象边一直被容器类引用。上述2中的订阅者列表也属于容器类这中情况。另外常见的容器类还有线程池、对象池、图片缓存池等。线程池中的线程若存在ThreadLocal对象,因为线程对象一直被循环使用,ThreadLocal对象便会一直被引用,要注意对value对象的置空释放。
静态View导致内存泄露
有时,当一个Activity经常启动,但是对应的View读取非常耗时,我们可以通过静态View变量来保持对该Activity的rootView引用。这样就可以不用每次启动Activity都去读取并渲染View了。这确实是一个提高Activity启动速度的好方法!但是要注意,一旦View attach到我们的Window上,就会持有一个Context(即Activity)的引用。而我们的View有事一个静态变量,所以导致Activity不被回收。 解决办法:在使用静态View时,需要确保在资源回收时,将静态View detach掉。
属性动画未及时关闭导致内存泄露
在使用ValueAnimator或者ObjectAnimator时,如果没有及时做cancel取消动画,就可能造成内存泄露。 因为在cancel方法里,最后调用了endAnimation(); ,在endAnimation里,有个AnimationHandler的单例,会持有属性动画对象的引用
解决办法:在在onDestory时,调用动画的cancel方法
WebView内存泄露
目前Android中WebView的实现存在很大的兼容性问题,Google支持各个ROM厂商自行定制自己的WebView实现,各个ROM间差异较大,且大多都存在内存泄露问题。除了调用其内部的clearCache()、clearHistory()、removeAllViews()、freeMemory()、destroy()和置null以外,一般比较粗暴有效的解决方法是:将包含WebView的Activity放在一个单独的进程中,不需要时将进程销毁,从而释放所有所占内存。
其他的系统控件以及自定义View
在 Android Lollipop 之前使用 AlertDialog 可能会导致内存泄漏
view中有线程或者动画 要及时停止。这是为了防止内存泄漏,可以在onDetachedFromWindow方法中结束,这个方法回调的时机是 当View的Activity退出或者当前View被移除的时候 会调用 这时候是结束动画或者线程的好时机 另外还有一个对应的方法 onAttachedToWindow 这个方法调用的时机是在包含View的Activity启动时 回调 回调在onDraw方法之前
其他常见的引起内存泄漏原因
(1)构造Adapter时,没有使用缓存的 contentView
(2)Bitmap在不使用的时候没有使用recycle()释放内存
(3)警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中调用thread.getLooper().quit();才不会泄露
(4)避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放
1、什么是 Activity?从视图角度分析
Activity 并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity 包含了一个Window,Window才是真正代表一个窗口。
Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View 进行交互。
2、什么是Window?
Window 是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView 来加载Activity中设置的布局R.layout.activity_main
Window 是视图的承载器,内部持有一个DecorView,而这个DecorView才是view 的根布局。
Window 通过WindowManager 将DecorView 加载其中,并将DecorView 交给ViewRootimpl,进行视图绘制以及其他交互
3、什么是DecorView?
DecorView 是 FrameLayout 的子类,它可以被认为是 Android 视图树的根节点视图。
DecorView 作为顶级 View,一般情况下它内部包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下三个部分,上面是个 ViewStub,延迟加载的视图(应该是设置ActionBar,根据 Theme 设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。具体情况和Android版本及主体有关。
在 Activity 中通过 setContentView 所设置的布局文件其实就是被加到内容栏之中的,成为其唯一子 View,就是上面的 id 为 content 的 FrameLayout 中,在代码中可以通过 content 来得到对应加载的布局。
4、什么是 ViewRootImpl
从结构上来看,ViewRootImpl 和 ViewGroup 其实是一种东西。
相同点
它们都继承了ViewParent。
ViewParent 是一个接口,定义了一些父View 的基本行为,比如requestlayout,getparent 等。
不同点
ViewRootImpl 并不会像ViewGroup 一样被真正绘制在屏幕上
在Activity 中,它是专门用来绘制DecorView 的,核心方法是setView
5、Activity,window,View 三者之间的关系是什么?
window 是 activity 的一个成员变量,window 和 View 是“显示器”和“显示内容”的关系
Window 抽象类,PhoneWindow 唯一实现类,用于加载Activity 的顶级View –DecorView
一个 Activity 对应一个 Window 也就是 PhoneWindow,一个 PhoneWindow 持有一个 DecorView 的实例,DecorView 本身是一个 FrameLayout
View 绘制之前,系统会先对 View 进行测量,以确定 View 的大小和位置。
简单的可以说,如 measure,layout,draw 分别对应测量,布局,绘制三个过程。
在整个 Activity 的生命周期中,setContentView 是在 onCreate 中调用的,它实现了对资源文件的解析,完成了 xml 文件到 View 的转化。
而真正将view绘制出来是在onResume之后进行的。
在onResume之后,从 Activity 中的 Window 实例中获取Decorview
调用activity 中windowmanager 的addView 方法,将decorView 传入到ViewRootImpl 的setView 方法中
通过ViewRootImpl.setView() 来完成View 的绘制。
setView
首先会检查当前线程是否是view的创建线程,【view是否一定要在主线程创建和更新?】
通过内存屏幕保证view的任务是最优先的
调用 performTraversals 完成measure,layout,draw的绘制
view是否一定要在主线程创建和更新
并不一定的,由于view的更新都会调用到ViewRootImpl的setview,然后会调用android.view.ViewRootImpl.checkThread,当我们不在主线程更新view的时候会抛出异常,异常的堆栈指向的也是android.view.ViewRootImpl.checkThread抛出CalledFromWrongThreadException,这个方法检查的就是当前线程是否是ViewRootImpl的mThread,而mThread是在ViewRootImpl初始化时赋值的。
而ViewRootImpl的初始化是在Activity创建时赋值的。
而当我们强制在子线程给WindowManagerImpl中addview,那么添加的view的ViewRootImpl就会在当前线程被创建。但是其他线程和主线程都没法更新这次添加的button。
new Thread(() -> {
Looper.prepare();
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
getWindowManager().addView(button,layoutParams);
Looper.loop();
}).start();
总结:view的更新可在子线程进行,但是需要保证和view被添加时的线程一致。
onMeasure
测量了View的大小,系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。
在测量大小时,MeasureSpec概括了从父布局传递给子view布局要求。每一个MeasureSpec代表了宽度或者高度要求,它由size(尺寸)和mode(模式)组成。
可能有三种模式,
UNSPECIFIED:未定义模式,父布局不对子布局强加限制,一般用于系统中,很少用到。 0 << 30
EXACTLY:精准模式,给定确定值,如layout_width 知道多少dp,或者 match parent。match parent也是给定了确定值,跟随父view的大小。 1 << 30
AT_MOST:限制最大值模式,子view可以到的最大宽度,如wrap_content就是这种。2 << 30
onLayout
布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。
onDraw
绘制。确定好位置后,就将这些控件绘制到屏幕上。
自定义视图
需要在进行了可能会改变外观的操作之后调用invalidate()函数和requestLayout()函数,来使得系统重新绘制view,确定大小和形状。这也确保了视图行为的可靠性和可用性。
重载onDraw()方法
自定义视图最重要的部分便是它的外观。我们通过重载onDraw()方法来实现自定义的外观。传递给onDraw()方法的参数是Canvas对象,Canvas类定义了绘制文字、线、位图以及其他基本图形的方法。
在调用所有的绘制方法之前,我们需要创建一个Paint对象。
Canvas好比是一个画布(实际是一个bitmap, 我们自定义的视图都是在这个Bitmap上进行绘制的)
而Paint是一个画笔(确定了颜色和样式等信息)。Canvas提供了绘制一条线的方法,而Piant提供了定义这条线颜色的方法。Canvas提供了绘制一个矩形的方法,而Paint定义了这个矩形是否需要填充。总结起来,Canvas定义了绘制在屏幕上的形状,而Paint定义了颜色、字体、样式等。
自定义View的种类各不相同,须要根据实际须要选择一种简单低成本的方式来实现,尽可能的减少UI的层级,view的种类如下,开发中需要尽可能的选择适合自己的。
继承View重写onDraw方法
主要用于实现不规则的效果,也就是说这种效果不适宜采用布局的组合方式来实现。也就是需要使用canvas,Paint,运用算法去“绘制”了。采用这种方式须要本身支持wrap_content,padding也须要本身处理canvas。
继承ViewGroup派生特殊的Layout
主要用于实现自定义的布局,看起来很像几种View组合在一块儿的时候,可使用这种方式。这种方式须要合适地处理ViewGroup的测量和布局,尤其在测量的时候需要注意padding 和margin,比如:自定义一个自动换行的LinerLayout等。
继承特定的View,好比TextView
这种方法主要是用于扩展某种已有的View,增长一些特定的功能。这种方法比较简单,也不须要本身支持wrap_content和padding。这种效果就相当于我们使用imageView来实现一个圆形的imageView。
继承特定的ViewGroup,好比LinearLayout
这种方式也比较常见,也就是基于原来已经存在的layout,在其上去添加新的功能。和上面的第2种方法比较相似,第2种方法更佳接近View的底层。
View需要支持padding
直接继承View的控件需要在onDraw方法中处理padding,否则用户设置padding属性就不会起作用。
直接继承ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
invaliate 和 requestlayout 方法的区别
ViewRootImpl 作为顶级 View 负责 View 的绘制。
所以简单来说,requestlayout 和 invaliate 最终都会向上回溯调用到 ViewRootImpl 的 postTranversals 方法来绘制 View。
不同的是 requestlayout 会绘制 View 的 measure,layout 和 draw 过程。
invaliate 因为只添加了绘制 draw 的标志位,只会绘制 draw 过程
基本的动画共有三种类型:
View 动画:
也叫视图动画或者补间动画,主要是指android.view.animation包下面的一些类,只能被用来设置给 View,缺点是比如当控件移动之后,接收点击的控件的位置不会跟随移动,并且能够实现的效果只有移动、缩放、旋转和淡入淡出操作四种及其组合。
Drawable 动画:
也叫 Frame 动画或者帧动画,其实可以划分到视图动画的类别,实现方式是将一系列的 Drawable 像幻灯片一样一个一个地显示。
Property 动画:
属性动画主要是指 android.animation包下面的一些类,只对 API 11 以上版本的Android 系统才有效,但我们可以通过兼容库做低版本兼容。这种动画可以设置给任何 Object,包括那些还没有渲染到屏幕上的对象。这种动画是可扩展的,可以让你自定义任何类型和属性的动画。
Handler
3首先答案是可以,但是不能直接创建。handler的创建依赖looper,否则会抛出异常Can't create handler inside thread that has not called Looper.prepare()。
因此可以主动调用Looper.prepare(); 再new Handler。
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
Looper.loop();
}
})
或者,我们获取主线程的looper。Handler handler = new Handler(Looper.getMainLooper()){...}这个Handler虽然是在子线程的代码中创建的,但是可以用来更新UI,因为looper使用的是主线的,事件会被放到主线程中去处理。
在主线程中我们没有主动调用Looper.prepare()
这是因为系统替我们做过了,在ActivityThread中,主动调用了Looper.prepareMainLooper()。
即使是在同一个线程中创建的不同handler,他们之间也不会互相通知消息。
handler:A在主线程创建,handler:B在子线程创建的主线程Handler。A发送的msg并不会派发给B。android.os.Message#target
准备:
Looper.prepare()是给这个Thread创建Looper对象,一个Thead只有一个Looper对象。
Looper对象需要一个MessageQueue对象一个Looper实例也只有一个MessageQueue。
调用Looper.loop(); 不断遍历MessageQueue中是否有消息。
发送:
Handler 作用是发消息到MessageQueue,从而回调Handler 的HandleMessage的回掉方法。
MessageQueue插入新的Message并唤醒阻塞。由于MessageQueue没有Message时,会进入阻塞状态因此当新消息来时,会激活。
处理:
重新检查MessageQueue并获取新插入的消息Message
Looper获取Message后,通过Message的target即Handler调用dispatchMessage(Message msg)方法分发提取到的Message,然后回收Message并循环获取下一个Message
Handler使用handlerMessage(Message msg)方法处理Messag
等待
MessageQueue没有Message时,重新进入阻塞状态
主线程的死循环是否一致耗费CPU资源?
Handler底层采用Linux的pipe/epoll机制,MessageQueue没有消息的时候,便阻塞在Looper.mQueue.next方法中,此时主线程会释放CPU资源进入休眠,直到下个事件到达,当有新消息的时候,通过往pipe管道写数据来唤醒主线程工作。所以主线程大多数时候处于休眠状态,不会阻塞。
冷启动、热启动、温启动
冷启动:系统不存在App进程(如APP首次启动或APP被完全杀死)时启动App称为冷启动。
热启动:按了Home键或其它情况app被切换到后台,再次启动App的过程。
温启动:温启动包含了冷启动的一些操作,不过App进程依然存在,这代表着它比热启动有更多的开销。
MVC,MVP,MVVM
模块和模块之间的数据通信方式构成不同的架构。在这3种架构中,都是把系统整体划分成了3个模块:视图层,数据层,业务层。 他们之间的区别在于,模块之间的通信方式(数据流动方向)不一致。
插件classLoader
1、DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
2、PathClassLoader只能加载系统中已经安装过的apk
插件化:需要在宿主manifest里面占位,然后分发。通过这种方式可以跳转到对应的插件的activity
classloader类型
Bootstrap ClassLoader(启动类加载器):该类加载器由C++实现的。负责加载Java基础类,对应加载的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等。
Extension ClassLoader(标准扩展类加载器):继承URLClassLoader。对应加载的文件是%JRE_HOME/lib/ext 目录下的jar和class等。
App ClassLoader(系统类加载器):继承URLClassLoader。对应加载的应用程序classpath目录下的所有jar和class等。
还有一种就是我们自定义的 ClassLoader,由Java实现。我们可以自定义类加载器,并可以指定这个类加载器 要 加载哪个路径下的class文件
PathCLassLoader
BaseDexClassLoader的子类,是整个程序中的类加载器,相当于 java 中的 AppClassLoader ,
也就是说我们整个项目的除了系统的类是用BootClassLoader加载的 其他都是用 PathCLassLoader加载的 ,也就是说,根据 双亲委派 。 PathCLassLoader去加载类的时候 先判断 BootClassLoader 是否加载过,没有那就是 自己去加载了 也就是 PathCLassLoader自己去加载。
DexClassLoader BaseDexClassLoader的子类, 相当于 java 的CustomClassLoader。
Java相关
Synchronized
Java用Synchronized来实现线程同步,该关键字可以加在方法上,也可以加在对象上,
如果他的作用的对象是非静态的,则它取得的锁是对象;
如果作用的对象是静态方法或者类,则它取得的锁是类对象(Class对象)。
每个对象有一把锁,谁取得这个锁,谁就可以访问对象的方法。
可重入性:
假设类A有两个方法a,b都用Synchronized修饰,当调用a()时需要请求类A的锁,调用B的时候,
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
Thread thread1 = new Thread(() -> {
test.a("thread-1");
test.b("thread-1");
});
Thread thread2 = new Thread(() -> {
// synchronized (test.count) {
test.b("thread-2");
test.a("thread-2");
// }
});
thread1.start();
thread2.start();
thread1.join(10000L);
}
public synchronized void a(String name) {
System.out.println(name + ":a");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b(String name) {
synchronized (count) {
System.out.println(name + ":b");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
thread-1:a
thread-1:b
thread-2:b
thread-2:a
在线程1执行时,线程1持有类锁,线程2无法进入方法,等待;当a执行完,执行b方法,锁还依然持有,线程2继续等待。
不可继承性:
子类声明集成自父亲的同步方法可以不必声明成同步的。
对象锁和类锁:
Java的所有对象都含有一个锁,这个锁由JVM自动获取和释放,线程进入Synchronized代码块会去获取该对象的锁,如果已经有线程获取了对象锁,那个当前线程就会等待。
对象锁是控制实例方法的锁
类锁是用来控制静态代码的同步的,相当于Class对象的锁。
类锁和对象锁不同,一个是类的Class对象的锁,一个是类的实例对象的锁,也就是说一个线程访问静态的Synchronized时,另外的线程可以访问对象实例的Synchronized方法。
Java引用类型
强引用----------- 垃圾收集器不会收集它,宁可抛出OOM
软引用(Soft Reference)-----------当垃圾收集器工作时,如果内存足够,就不回收只被软引用关联的对象,如果内存不够,则会进行回收。一旦被回收,这个软引用就会被加入到关联的ReferenceQueue中。
弱引用(Weak Reference) -------------- 对象只能生存到下一次GC,当垃圾收集器工作时无论内存是否足够都会收集只被弱引用关联的对象。一旦一个弱引用被回收,便会加入一个注册引用队列ReferenceQueue中。
虚引用(Phantom Reference) ------------ 最弱的引用类型,get方法返回null。主要用来跟踪对象被垃圾回收的状态。虚引用被回收时会放入ReferenceQueue,从而达到跟踪对象垃圾回收的作用。
ReferenceQueue-----------是这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理等。
WeakReference weakReference=new WeakReference(obj,ReferenceQueue)将引用队列传入即可进行观察。
结论上来说,一般一个对象 equals相等,则 hashcode 一定也是相同的。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。
但是hashCode是对象的散列值,默认的hashcode方法是对堆上的对象产生独特值。如果没有重写 hashcode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
相同hashcode但是对象不一定相等,是因为hashcode是通过hash方法对对象进行散列,缩短了查找的成本。一个实际例子,如果对象不相等,hashcode就不同,那么在hashmap在,每个空间都只会存在一个值,这显然不实际。
equals方法的定义在Object.java中是直接通过判断两个对象的地址是否相等(判断是否为同一个对象)来区分的,等价于【==】的判断。
基础类型都复写了equals方法,判断对象的内容是否相等。
查阅大量的资料显示,理论上来说,java只有值传递。
但是从功能定义上来说,我认为:
Java 中的基本类型,属于值传递。
Java 中的引用类型,属于引用传递。
什么是值传递
指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
什么是引用传递
是指在调用函数时将实际参数的地址直接传递到函数中(的形参),那么在函数中对参数所进行的修改,将会影响到实际参数。
重载发生在一个类中,同名的方法如果有不同的参数列表(类型不同、个数 不同、顺序不同)则视为重载。
重写发生在子类与父类之间,重写要求子类重写之后的方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常。
ArrayList和LinkedList
ArrayList:
基于动态数组,连续内存存储,适合下标访问(随机访问)
扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)
LinkedList:
基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。
for、forEach,Iterator在ArrayList、LinkedList上的性能比较
································here for loop······················ArrayList····················
execution time: 0.6844 ms.
································here forEach loop··················ArrayList····················
execution time: 1.9834 ms.
································here iterator loop·················ArrayList····················
execution time: 0.8786 ms.
································································································
·····After many tests, in most cases, the for the most efficiency, followed by the iterator.····
································································································
································here for loop·····················LinkedList····················
execution time:3035.3940 ms.
································here forEach loop·················LinkedList····················
execution time: 1.4215 ms.
································here iterator loop················LinkedList····················
execution time: 0.8353 ms.
for对于ArrayList的遍历效率略胜一筹,与iterator差不多;
对于LinkedList,for效率最差,iterator最高。
String 和 StringBuffer、StringBuilder 的区别是什么?
可变性线程安全性能
Stringfinal修饰,不可变线程安全,常量对象不可变赋值生成一个新的对象,指针指向新对象
StringBuffer继承AbstractStringBuilder类,值保存在char[]value中,可变线程安全,对方法添加了同步锁相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险
StringBuilder线程不安全
抽象类与接口
抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象, 是一种行为的规范。
实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
实现数量:类可以实现很多个接口;但只能继承一个抽象类【java只支持单继承】。
访问修饰符:抽象类中的抽象方法可以使用Public 和 Protected修饰,如果抽象方法修饰符为Private,则报错:The abstract method 方法名 in type Test can only set a visibility modifier, one of public or protected。接口中的方法默认使用 public 修饰; 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定 。
有抽象方法的一定是抽象类,抽象类不一定有抽象方法。编码报错:Abstract method in non-abstract class
定义:
程序:程序是指按照一定的规则和顺序的任务执行过程,就是指令和数据的集合。指令是一系列的命令或则代码,数据是指一堆二进制的代码。它一种二进制文件,存储在磁盘中,静态的不占用系统资源(cpu内存);
进程:进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程它是操作系统进行资源分配的调度的基本单元。一个任务运行可以激活多个进程,这些进程相互合作一起完成该任务。
进程具有并发性、动态性、交互性和独立性等主要特征。
线程:被称为轻量级进程。线程可以对进程的内存空间和资源进行访问,并与同一进程中其他线程共享,是任务调度和执行的最小单位。一个进程有多个线程,其中每个线程共享该进程所拥有的资源。
进程是资源调度的基本单位
线程是处理器调度和分派的基本单位,是程序执行的最小单位
一个进程由多个线程执行,线程是一个进程中代码的不同执行路线
进程间相互独立,同一进程下的线程共享进程的内存空间,且对其他进程不可见;
进程状态:
就绪状态 --> 运行状态:当处于就绪状态的进程被调度后,获得处理机资源,此时进程就由就绪状态转换成运行状态。
运行状态-->阻塞状态:处于运行状态的进程在时间片用完之后,不得不让出处理机,此时进程就有运行状态转成阻塞状态。
阻塞状态-->就绪状态:当进程等待的事件到来时,中断处理程序必须把相应进程的状态由阻塞状态转换成就绪状态。
线程中状态转换:
Java线程中状态转换:
RUNNABLE与BLOCKED的状态转换:
只有线程在等待synchronized的隐式锁时,synchronized修饰的方法、代码块同一时刻只能允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从RUNNABLE转换到BLOCKED状态。而当等待获得到synchronized隐式锁时,就又会从BLOCKED转换到RUNNABLE状态。
并且java层面上不关心操作系统进程的调度状态,因为在JVM看来,等待CPU使用权和等待IO没有区别,都是在等待某个资源,因此都被归为RUNNABLE状态。
RUNNABLE与WAITING的状态转换
获取到synchronized的隐式锁的进程,调用无参数的Object.wait()方法
调用无参数的Thread.join()方法
调用LOCKSupport.park()方法。LockSupport.unpark(Thread thread)课唤醒目标线程,目标线程的状态又会从WAITNG状态转换成RUNNABLE状态
RUNNABLE与TIMED_WEAITING的状态转换
调用带超时参数的Thread.sleep(long millis)方法
获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法
调用带超时参数的LockSupport.parkNanos(Object blocker,long dealing)方法
调用带超时参数锁的Thread.join(long millis)方法
调用带超时参数的LockSupport.parkUnitl(long deadline)方法
从NEW到RUNNABLE状态
线程在创建成功后,调用其对应的start方法就会从RUNNABLE状态转换成
从RUNNABLE到TERMINATED状态
通过调用Thread.interrupt()方法,也可以调用stop()方法,但当前方法被废止了。
创建线程有哪几种方式
创建线程有三种方式:
1)继承 Thread 重写 run 方法;
2)实现 Runnable 接口;
3)实现 Callable 接口。
runnable 和 callable有什么区别?
runnable 没有返回值,callable 可以拿到有返回值,callable 可看作是 runnable 的补充。
sleep() 和wait() 有什么区别
类的不同:sleep() 来自Thread,wait() 来自Object。
释放锁:sleep() 不释放锁;wait()释放锁。
用 法 不 同 : sleep() 时 间 到 会 自 动 恢 复 ; wait() 可 以 使 用 notify()/notifyAll()直接唤醒。
notify()和 notifyAll()有什么区别?
notify()和notifyAll()都可以用于线程的唤醒
唤醒数量不同
notify()方法只会随机唤醒等待队列中的一个线程,而notifyAll()方法则会唤醒等待队列中的所有线程。
调用方式不同
notify()和notifyAll()方法都必须在同步代码块中调用,并且必须包含在synchronized块中
且必须是该对象的监视器对象才能够调用。
而且只有在获取了锁之后才能调用,否则会抛出IllegalMonitorStateException异常。
竞争情况不同
notify()方法只会唤醒等待队列中的一个线程,并使其与其他线程竞争获取锁,这可能会导致某些线程无法被唤醒或者一直处于等待状态。
notifyAll()方法则会唤醒等待队列中的所有线程,并使它们竞争获取锁,这样可以使所有线程都有机会获取锁并进入运行状态,从而避免了一些线程一直处于等待状态。
run() 和 start() 有什么区别?
start() 方法用于启动线程,start() 只能调用一次;执行多次会报错IllegalThreadStateException
run() 方法用于执行线程的运行时代码。run() 可以重复调用。只是在当前线程下执行这段代码块,并不会启动一个新得线程。
线程池创建时的参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务。
maximumPoolSize:最大线程数
理想的线程数,使用 2倍cpu核心数
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制。
keepAliveTime:空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。
unit:空闲线程存活时间单位
workQueue:工作队列
存放待执行任务的队列(五种):当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。
threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等。
handler:拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。
处理策略:
比如5个核心线程,10个最大线程,工作队列20。
情况一:
5个核心线程在工作,还有19个任务等待执行,那么19个排进队列
情况二:
5个核心线程在工作,还有23个任务等待执行,其中20个排进队列,剩下3个开启新线程执行
情况三:
5个核心线程在工作,还有26个任务等待执行,其中20个排进队列,剩下5个开启新线程执行,多出的一个就执行设置的拒绝策略
submit() 和 execute() 方法区别
execute():只能执行 Runnable 类型的任务。
submit():可以执行Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而Runnable执行无返回值。
保证多线程的运行安全
使用安全类,比如 Java. util. concurrent 包下的类。
使用自动锁 synchronized。
使用手动锁 Lock
session 和 cookie 有什么区别?
存储位置不同:session 存储在服务器端;cookie 存储在浏览器端。
安全性不同:cookie 安全性一般,在浏览器存储,可以被伪造和修改。
容量和个数限制:cookie 有容量限制,每个站点下的 cookie 也有个数限制。
存储的多样性:session 可以存储在 Redis 中、数据库中、应用程序中;而 cookie 只能存储在浏览器中
throw 和 throws 的区别?
throw:是真实抛出一个异常。
throws:是声明可能会抛出一个异常。
final、finally、finalize 有什么区别?
final:是修饰符,如果修饰类,此类不能被继承;如果修饰方法和变量,则 表示此方法和此变量不能在被改变,只能使用。
finally:是 try{} catch{} finally{} 最后一部分,表示不论 发生任何情况 都会执行,finally 部分可以省略,但如果 finally 部分存在,则一定会执行 finally 里面的代码。
finalize: 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法。
finally语句什么时候不会执行
try语句没有被执行到,如在try语句之前就返回了,这样finally语句就不会执行,这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到。
在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。
Java中violate关键字
Java中的内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。
结果:
thread2:2
thread2:3
thread1:2
thread2:4
thread1:5
可以看到,出现了两个2的打印,一个线程修改i,另外一个线程也读取到了这个修改后的i,而不是刚才此线程+1后的1;而且在线程1读取到2时,线程2的值已经修改为了3。并且多次运行这段代码可能带来不同的运行结果
如果给操作加上锁,则可以保证运行与预期一致。
并发编程的三大概念:原子性,有序性,可见性
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
其实只有语句1是原子性操作,其他三个语句都不是原子性操作。
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
上面的例子也阐述了,多线程对于同一个变量的可见性问题。
violate保证了可见性:
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性
即程序执行的顺序按照代码的先后顺序执行。编译器在编译代码时会进行指令重排序(Instruction Reorder)。
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
Java内存模型
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
举个简单的例子:在java中,执行下面这个语句:
i = 10;
执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。
violate
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
springBoot相关
Spring原始注解
@Component:使用在类上用于实例化Bean
@Component不带参数,默认值为类名首字母小写。如:在类User上使用@Component, 相当于@Component("user")。
代参数@Component("xxx"),可以自定义,xxx在spring容器 必须中唯一。
@Repository:使用在dao层类上用于实例化bean
与@Component用法一致。
@Controller:使用在web层类上用于实例化bean
与@Component用法一致。
@Service:使用在service层类上用于实例化bean
与@Component用法一致。
@Autowired :使用在字段上用于根据类型依赖注入
@Autowired的注入方式为ByType(根据类型进行匹配注入)。简单来说:优先根据接口类型进行进行匹配(接口只有一个实现类)。
@Qualifier :结合@Autowired一起使用根据名称进行依赖注入
@Qualifier要结合@Autowired用,不可以单独使用。
当一个接口有多个实现类的时候,就要用到@Qualifier去指明需要注入的名称。
[这里的名称就是 @Service("xxx") 中的 xxx] 这时就会根据名称ByName进行匹配并注入。
@Resource:相当于@Autowired和@Qualifier,按照名称进行注入
@Resource的默认注入方式为ByName(根据名称进行匹配),若名称匹配不了,则会ByType(根据类型进行匹配注入)。@Resourece相当于@Autowired + @Qualifier。
注* 1、2、3、4都是使用在类上而不是接口上; 5、6、7一般使用在字段上
Dart 没有 「public」「private」等关键字,默认就是公开的,私有变量使用 下划线 _开头。
Dart 是单线程模型,如何运行的看这张图:
Future
Future,字面意思「未来」,是用来处理异步的工具。
刚才也说过:
Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。
Future 默认情况下其实就是往「事件队列」里插入一个事件,当有空余时间的时候就去执行,当执行完毕后会回调 Future.then(v) 方法。
而我们也可以通过使用 Future.microtask 方法来向 「微任务队列」中插入一个任务,这样就会提高他执行的效率。
因为在 Dart 每一个 isolate 当中,执行优先级为 : Main > MicroTask > EventQueue
Stream
Stream 和 Feature 一样,都是用来处理异步的工具。
但是 Stream 和 Feature 不同的地方是 Stream 可以接收多个异步结果,而Feature 只有一个。
Stream 的创建可以使用 Stream.fromFuture,也可以使用 StreamController 来创建和控制。
还有一个注意点是:普通的 Stream 只可以有一个订阅者,如果想要多订阅的话,要使用 asBroadcastStream()。
main()和runApp()函数在flutter的作用分别是什么?有什么关系吗?
main函数是类似于java语言的程序运行入口函数
runApp函数是渲染根widget树的函数
一般情况下runApp函数会在main函数里执行
什么是widget? 在flutter里有几种类型的widget?分别有什么区别?能分别说一下生命周期吗?
widget在flutter里基本是一些UI组件有两种类型的widget,
分别是statefulWidget 和statelessWidget两种,statelessWidget不会自己重新构建自己,但是statefulWidget会