一:说说View/ViewGroup的绘制流程
View的绘制流程是从ViewRoot的performTraversals开始的,它经过measure,layout,draw三个过程最终将View绘制出来。performTraversals会依次调用performMeasure,performLayout,performDraw三个方法,他们会依次调用measure,layout,draw方法,然后又调用了onMeasure,onLayout,dispatchDraw。
-
measure :对于自定义的单一view的测量,只需要根据父 view 传递的MeasureSpec进行计算大小。
对于ViewGroup的测量,一般要重写onMeasure方法,在onMeasure方法中,父容器会对所有的子View进行Measure,子元素又会作为父容器,重复对它自己的子元素进行Measure,这样Measure过程就从DecorView一级一级传递下去了,也就是要遍历所有子View的的尺寸,最终得出总的viewGroup的尺寸。Layout和Draw方法也是如此。
-
layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
对于自定义的单一view,计算本身的位置即可。
对于ViewGroup来说,需要重写onlayout方法。除了计算自己View的位置,还需要确定每一个子View在父容器的位置以及子view的宽高(getMeasuredWidth和getMeasuredHeight),最后调用所有子view的layout方法来设定子view的位置。
-
draw :把 View 对象绘制到屏幕上。
draw()会依次调用四个方法:
1)drawBackground(),根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界。
2)onDraw(),绘制View本身的内容,一般自定义单一view会重写这个方法,实现一些绘制逻辑。
3) dispatchDraw(),绘制子View
4)onDrawScrollBars(canvas),绘制装饰,如 滚动指示器、滚动条、和前景.
二:说说你理解的MeasureSpec
MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求,这个测量要求就是MeasureSpec。
首先,MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中前两位是mode,后面30位存的是size
/ 获取测量模式
int specMode = MeasureSpec.getMode(measureSpec)
// 获取测量大小
int specSize = MeasureSpec.getSize(measureSpec)
// 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
其次,每个子View的MeasureSpec值根据子View的布局参数和父容器的MeasureSpec值计算得来的,所以就有一个父布局测量模式,子视图布局参数,以及子view本身的MeasureSpec关系图:
其实也就是getChildMeasureSpec方法的源码逻辑,会根据子View的布局参数和父容器的MeasureSpec计算出来单个子view的MeasureSpec。
最后是实际应用时:
对于自定义的单一view,一般可以不处理onMeasure方法,如果要对宽高进行自定义,就重写onMeasure方法,并将算好的宽高通过setMeasuredDimension方法传进去。对于自定义的ViewGroup,一般需要重写onMeasure方法,并且调用measureChildren方法遍历所有子View并进行测量(measureChild方法是测量具体某一个view的宽高),然后可以通过getMeasuredWidth/getMeasuredHeight获取宽高,最后通过setMeasuredDimension方法存储本身的总宽高。
三:Scroller是怎么实现View的弹性滑动?
- 在MotionEvent.ACTION_UP事件触发时调用startScroll()方法,该方法并没有进行实际的滑动操作,而是记录滑动相关量(滑动距离、滑动时间)
- 接着调用invalidate/postInvalidate()方法,请求View重绘,导致View.draw方法被执行
- 当View重绘后会在draw方法中调用computeScroll方法,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,和之前流程一样,如此反复导致View不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过成结束。
mScroller = new Scroller(context);
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// 滚动开始时X的坐标,滚动开始时Y的坐标,横向滚动的距离,纵向滚动的距离
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
// 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、 解释二:
View有scrollTo、scrollBy方法,但是滑动是瞬时的,比较僵硬。可以通过Scroller来实现View的弹性滑动。
ScrollView就有smoothScrollTo和smoothScrollBy方法,这两个方法实现的就是弹性滑动的效果,它们内部实际也是用了Scroller。
Scroller的典型使用方法:
private Scroller mScroller = new Scroller(getContext());
private void smoothScrollTo(int destX, int destY, int scrollTime){
mScroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), scrollTime);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
上述代码就能实现慢慢滑动的弹性效果,为什么呢?
从源码看,startScroll这个方法内部其实什么也没有做,只是保存了传递的参数:
invalidate方法会使view重绘,view的draw方法会去调用computeScroll方法,我们复写了这个方法,在里面调用了computeScrollOffset,这个函数的意义是根据当前时间的流逝计算出当前的scrollX和scrollY应该是多少(根据之前startScroll所传入的时间),如果这个方法返回true,证明滑动还没有结束。所以只要判断还在滑动,就调用scrollTo,滑动到当前的scrollX和scrollY的位置,然后再调用postInvalidate,又会使得view重绘,然后再次调用computeScroll方法,直到computeScrollOffset返回false滑动结束。
因此,Scroller本身其实并不能使view滑动,还需配合view的computeScroll方法才能实现。
四:OKHttp有哪些拦截器,分别起什么作用
OKHTTP的拦截器是把所有的拦截器放到一个list里,然后每次依次执行拦截器,并且在每个拦截器分成三部分:
- 预处理拦截器内容
- 通过proceed方法把请求交给下一个拦截器
- 下一个拦截器处理完成并返回,后续处理工作。
这样依次下去就形成了一个链式调用,看看源码,具体有哪些拦截器:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
根据源码可知,一共七个拦截器:
- addInterceptor(Interceptor),这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。
- RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,以及请求失败的充实工作,重定向的后续请求工作。跟他的名字一样,就是做重试工作还有一些连接跟踪工作。
- BridgeInterceptor,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。
- CacheInterceptor,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
- ConnectInterceptor,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
- networkInterceptors,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,所以用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。
-
CallServerInterceptor,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。
五:OkHttp怎么实现连接池
为什么需要连接池?
频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,所以HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。
keepalive机制是什么呢?也就是可以在一次TCP连接中可以持续发送多份数据而不会断开连接。所以连接的多次使用,也就是复用就变得格外重要了,而复用连接就需要对连接进行管理,于是就有了连接池的概念。
OkHttp中使用ConectionPool实现连接池,对连接进行回收和管理。,默认支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)。
怎么实现的?
1)首先,ConectionPool中维护了一个双端队列 ArrayDeque,也就是两端都可以进出的队列,用来存储连接。
2)然后在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
也就是遍历了双端队列,如果连接有效,就会调用acquire方法计数并返回这个连接。
3)如果没找到可用连接,就会创建新连接,并会把这个建立的连接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了ConectionPool的put方法。
public final class ConnectionPool {
void put(RealConnection connection) {
if (!cleanupRunning) {
//没有连接的时候调用
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
4)其实这个线程池中只有一个线程是用来清理连接的,也就是上述的cleanupRunnable
private final Runnable cleanupRunnable = new Runnable() {
@Override
public void run() {
while (true) {
//执行清理,并返回下次需要清理的时间。
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
//在timeout时间内释放锁
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
这个runnable会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,然后进入wait等待。
怎么清理的呢?看看源码:
long cleanup(long now) {
synchronized (this) {
//遍历连接
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//检查连接是否是空闲状态,
//不是,则inUseConnectionCount + 1
//是 ,则idleConnectionCount + 1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//如果超过keepAliveDurationNs或maxIdleConnections,
//从双端队列connections中移除
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) { //如果空闲连接次数>0,返回将要到期的时间
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 连接依然在使用中,返回保持连接的周期5分钟
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
也就是当如果空闲连接maxIdleConnections超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
5)这里有个问题,怎样属于空闲连接?
其实就是有关刚才说到的一个方法acquire计数方法:
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
在RealConnection中,有一个StreamAllocation虚引用列表allocations。每创建一个连接,就会把连接对应的StreamAllocationReference添加进该列表中,如果连接关闭以后就将该对象移除。
6)连接池的工作就这么多,并不负责,主要就是管理双端队列Deque<RealConnection>,可以用的连接就直接用,然后定期清理连接,同时通过对StreamAllocation的引用计数实现自动回收。
六:OkHttp里面用到了什么设计模式
- 责任链模式
这个不要太明显,可以说是okhttp的精髓所在了,主要体现就是拦截器的使用,具体代码可以看看上述的拦截器介绍。
- 建造者模式
在Okhttp中,建造者模式也是用的挺多的,主要用处是将对象的创建与表示相分离,用Builder组装各项配置。比如Request:
public class Request {
public static class Builder {
@Nullable HttpUrl url;
String method;
Headers.Builder headers;
@Nullable RequestBody body;
public Request build() {
return new Request(this);
}
}
}
- 工厂模式
工厂模式和建造者模式类似,区别就在于工厂模式侧重点在于对象的生成过程,而建造者模式主要是侧重对象的各个参数配置。例子有CacheInterceptor拦截器中有个CacheStrategy对象:
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
- 观察者模式
Okhttp中websocket的使用,由于webSocket属于长连接,所以需要进行监听,这里是用到了观察者模式:
final WebSocketListener listener;
@Override public void onReadMessage(String text) throws IOException {
listener.onMessage(this, text);
}
另外有的博客还说到了策略模式,门面模式等,这些大家可以网上搜搜,毕竟每个人的想法看法都会不同,细心找找可能就会发现。
七:介绍一下你们之前做的项目的架构
这个问题大家就真实回答就好,重点是要说完后提出对自己项目架构的认同或不认同的观点,也就是要有自己的思考和想法。
MVP,MVVM,MVC 区别
- MVC
架构介绍
Model:数据模型,比如我们从数据库或者网络获取数据;
View:视图,也就是我们的xml布局文件;
Controller:控制器,也就是我们的Activity
模型联系
View --> Controller,也就是反应View的一些用户事件(点击触摸事件)到Activity上。
Controller --> Model, 也就是Activity去读写一些我们需要的数据。
Controller --> View, 也就是Activity在获取数据之后,将更新内容反映到View上。
这样一个完整的项目架构就出来了,也是我们早期进行开发比较常用的项目架构。
优缺点
这种缺点还是比较明显的,主要表现就是我们的Activity太重了,经常一写就是几百上千行了。造成这种问题的原因就是Controller层和View层的关系太过紧密,也就是Activity中有太多操作View的代码了。
但是!但是!其实Android这种并称不上传统的MVC结构,因为Activity又可以叫View层又可以叫Controller层,所以我觉得这种Android默认的开发结构,其实称不上什么MVC项目架构,因为他本身就是Android一开始默认的开发形式,所有东西都往Activity中丢,然后能封装的封装一下,根本分不出来这些层级。当然这是我个人看法,可以都来讨论下。
- MVP
MVP 思想 : MVP中Activity/fragment属于View ,因此要有一个View的接口,然后Activity/fragment实现View接口,
P 中创建业务层 Presenter中定义功能,实现类PresenterImpl中操作功能/逻辑,
在Activity/fragment中声明初始化 p,
只能
new SplashPresenter 的实现类,不能new SplashPresenter接口,
P 中一般要回调Activity/fragment 中的方法,所以要传入“this”,(PresenterImpl 是 Presenter的实现类)
在 p 中生成构造函数,xxView(因为是面向接口,生成的是xxView构造函数,而不是xxActivity构造函数),
Activity/fragment 中的回调方法一般都添加到View中去,而真正调用的是在PresenterImpl中,在Activity/fragment实现逻辑操作
View: 对于View层也是视图层,在View层中只负责对数据的展示,提供友好的界面与用户进行交互。
在Android开发中通常将Activity或者Fragment作为View层。View与UI相关的
Model: 对于Model层也是数据层。它区别于MVC架构中的Model,在这里不仅仅只是数据模型。
在MVP架构中Model它负责对数据的存取操作,例如对数据库的读写,网络的数据的请求等。
Presenter:对于Presenter层他是连接View层与Model层的桥梁并对业务逻辑进行处理。
在MVP架构中Model与View无法直接进行交互。所以在Presenter层它会从Model层获得所需要的数据,
进行一些适当的处理后交由View层进行显示。这样通过Presenter将View 与Model进行隔离,
使得View和Model之间不存在耦合,同时也将业务逻辑从View中抽离。
架构介绍
之前不就是因为Activity中有操作view,又做Controller工作吗。所以其实MVP架构就是从原来的Activity层把view和Controller区分开,单独抽出来一层Presenter作为原来Controller的职位。然后最后演化成,将View层写成接口的形式,然后Activity去实现View接口,最后在Presenter类中去实现方法。
Model:数据模型,比如我们从数据库或者网络获取数据。
View:视图,也就是我们的xml布局文件和Activity。
Presenter:主持人,单独的类,只做调度工作。
模型联系
View --> Presenter,反应View的一些用户事件到Presenter上。
Presenter --> Model, Presenter去读写操作一些我们需要的数据。
Presenter--> View, Presenter在获取数据之后,将更新内容反馈给Activity,进行view更新。
优缺点
这种的优点就是确实大大减少了Activity的负担,让Activity主要承担一个更新View的工作,然后把跟Model交互的工作转移给了Presenter,从而由Presenter方来控制和交互Model方以及View方。所以让项目更加明确简单,顺序性思维开发。
缺点也很明显:首先就是代码量大大增加了,每个页面或者说功能点,都要专门写一个Presenter类,并且由于是面向接口编程,需要增加大量接口,会有大量繁琐的回调。其次,由于Presenter里持有了Activity对象,所以可能会导致内存泄漏或者view空指针,这也是需要注意的地方。
- MVVM
架构介绍
MVVM的特点就是双向绑定,并且有Google官方加持,更新了Jetpack中很多架构组件,比如ViewModel,Livedata,DataBinding等等,所以这个是现在的主流框架和官方推崇的框架。
Model:数据模型,比如我们从数据库或者网络获取数据。
View:视图,也就是我们的xml布局文件和Activity。
ViewModel:关联层,将Model和View绑定,使他们之间可以相互绑定实时更新
模型联系
View --> ViewModel -->View,双向绑定,数据改动可以反映到界面,界面的修改可以反映到数据。
ViewModel --> Model, 操作一些我们需要的数据。
优缺点
优点就是官方大力支持,所以也更新了很多相关库,让MVVM架构更强更好用,而且双向绑定的特点可以让我们省去很多View和Model的交互。也基本解决了上面两个架构的问题。
八:具体说说你理解的MVVM
1)先说说MVVM是怎么解决了其他两个架构所在的缺陷和问题:
- 解决了各个层级之间耦合度太高的问题,也就是更好的完成了解耦。 MVP层中,Presenter还是会持有View的引用,但是在MVVM中,View和Model进行双向绑定,从而使viewModel基本只需要处理业务逻辑,无需关系界面相关的元素了。
- 解决了代码量太多,或者模式化代码太多的问题。 由于双向绑定,所以UI相关的代码就少了很多,这也是代码量少的关键。而这其中起到比较关键的组件就是DataBinding,使所有的UI变动都交给了被观察的数据模型。
- 解决了可能会有的内存泄漏问题。 MVVM架构组件中有一个组件是LiveData,它具有生命周期感知能力,可以感知到Activity等的生命周期,所以就可以在其关联的生命周期遭到销毁后自行清理,就大大减少了内存泄漏问题。
- 解决了因为Activity停止而导致的View空指针问题。 在MVVM中使用了LiveData,那么在需要更新View的时候,如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。也就是他会保证在界面可见的时候才会进行响应,这样就解决了空指针问题。
- 解决了生命周期管理问题。 这主要得益于Lifecycle组件,它使得一些控件可以对生命周期进行观察,就能随时随地进行生命周期事件。
2)再说说响应式编程3)最后再说说MVVM为什么这么强大?
响应式编程,说白了就是我先构建好事物之间的关系,然后就可以不用管了。他们之间会因为这层关系而互相驱动。其实也就是我们常说的观察者模式,或者说订阅发布模式。
为什么说这个呢,因为MVVM的本质思想就是类似这种。不管是双向绑定,还是生命周期感知,其实都是一种观察者模式,使所有事物变得可观察,那么我们只需要把这种观察关系给稳定住,那么项目也就稳健了。
3)最后再说说MVVM为什么这么强大?
MVVM强大不是因为这个架构本身,而是因为这种响应式编程的优势比较大,再加上Google官方的大力支持,出了这么多支持的组件,来维系MVVM架构,其实也是官方想进行项目架构的统一。
优秀的架构思想+官方支持=强大