个人认为重要的Android面试总结一

                                           一:说说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架构,其实也是官方想进行项目架构的统一。

  优秀的架构思想+官方支持=强大

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值