Android进阶之路的绊脚石

写在前面

标题谈进阶,属实有一些夸大。
我一直在思考什么样的文章才是一篇好文章,我的定义是首先要有人看,那么一个好的题目就成功了一半,所以各位看官,别着急喷,看完下面的内容,你会转变想法,喷的更厉害~哈哈。
声明,我是一个应届生,虽然我只是一个应届生,但是我善于总结别人的经验,不要脸的去向别人请教。我实习的公司有蚂蚁金服技术专家,360浏览器技术负责人...在与他们的请教(后来他们见到我都躲着走,哈哈)中,我学到了很多东西。因此我会结合这一系列的内容,把这篇文章写出来。

算是一种供我个人复盘的场所吧。

《Android篇-上卷》

上卷的话,会以常见且基础,但又不简单的面试题着手,比如View,比如Service...等等。
下卷的话,会以比较深的内容,比如AMS、AIDL...等等。

UI相关

1、View的绘制流程

这是一个比较大的话题,先让我们简单捋一捋:
假设我们这个View写在了一个layout里边,那么当AMS回调我们特定的Activity的onCreate方法是,我们的setContent就会别调用,内部就会初始化解析我们的layout布局,找到根View。初始化DecorView,进而实例化ViewRootImpl,将根View传进去。在这个过程中,ViewRootImpl,以及执行requestLayout,然后会嗲用checkoutThread也就是判断是否在主线程中。接着会依次循环遍历,调用performMeasure()、performLayout、performDraw()方法。最终会对应到View中的onMeasure、onLayout、onDraw。
流程的话,这个View的流程差不过就是如此,但是针对这个流程会有很多细节之处可以问。

1.1、谈一谈对onMeasure的理解

这个问题,无非是想看看你是否有处理onMeasure的经验,以及对MeasureSpec的三种模式的理解。
简单说,如果我们的View不要支持wrapcontent,或者是padding属性是,我们肯定是要onMeasure对去三种MeasureSpece进行处理。如果只需要支持matchparent和固定的dp,那么不需要重写onMeasure。

  • EXACTLY:对应精确值和match_parent
  • AT_MOST:对应warp_content

1.2、MeasureSpec由谁决定

DecorView的MeasureSpec由窗口大小和其LayoutParams决定,其他View由父View的MeasureSpec和本View的LayoutParams决定。

1.3、onCreate中能否在子线程更新UI,为什么

这个问题的答案在1、View的绘制流程时就已经提到了。onCreate里是可以更新UI的(前提是子线程不能延时),因为子线程更新UI的异常,是在ViewRootImpl的checkThread方法中判断并抛出的,但是这个方法同样执行再onCreate,而我们启动子线程,那么这里就涉及到谁先抢占到CPU资源的问题了,如果子线程先ViewRootImpl抢到,那么就是可以执行更新UI的。

1.4、onDraw能够进行耗时操作,如果不行有什么其他方案

因为我们不能再子线程更新UI,那么我们的onDraw相应的也是执行再主线程之后,而我们知道主线程是不允许有耗时操作的。
除了我们自己可以在onDraw里面开子线程(注意同步问题)以外,可以直接使用SurfaceView去做对应的效果(虽然这样有些大材小用)。


2、事件分发

事件分发同样是重头戏,首先我们的Event事件会从屏幕传到Activity传到PhoneWindow传到DecorView,然后传到我们的ViewGroup。
我们的ViewGroup会调用dispatchTouchEvent进行分发,方法内部会先判断是否为DOWN事件。如果是,初始化状态。判断自身是否被子View调用过请求禁止拦截。如果没有,判断自身自否拦截(默认是false)事件:

  • 如果是true(只会调用判断一次),后续事件直接交由自己onTouchEvent去处理,并且不再进行任何判断。
  • 如果false,事件传递给下一层View。

dispatchTouchEvent的返回值作用,如果直接返回true,所有后续事件只会由此方法处理,后续所有对应方法不被回调。直接返回false,后续事件交由上层去处理,自己已经下层不再过问。

如果下一层是ViewGroup重复上述过程。如果是View,在dispatchTouchEvent之中会先判断是否有OnTouchListener,如果有先执行ouTouch方法,并且如果不返回true,就会执行onTouchEvent,这个方法内部会调用onClick,因此如果onTouch返回true,onTouchEvent和onClick(onClick的回调在onTouchEvent中的performClick中,如果onTouchEvent的ACTION_UP被调用,回调performClick)都不会执行。

要onTouchEvent返回值,如果是true处理事件,判断那么此事件后续操作皆传给此View处理。
如果false,回传给上级的onTouchEvet,直至Activity的onTouchEvent方法中。

2.1、如何确定是哪一个View被点击

在dispatchTouchEvent之中,经过一系列判断决定分发给子View时,会获取所有子View然后根据Event的x,y坐标进行匹配是哪个View被点击。
(遍历View,调用isTransformedTouchPointInView(x,y,childview))

2.2、给你一个View的id,从根GroupView开始写一个算法找到这个id对应的View

我在面试美团的时候被问到过,其实就是一个递归调用的思想吧。如果有更好的解决方式,欢迎评论区留言。

public static View find(ViewGroup vg, int id){
    if(vg == null) return null;
    int size = vg.getChildCount();
    //循环遍历所有孩子
    for(int i = 0 ; i< size ;i++){
        View v = vg.getChildAt(i);
        //如果当前孩子的id相同,那么返回
        if(v.getId == id) return v;
        //如果当前孩子id不同,但是是一个ViewGroup,那么我们递归往下找
        if(v instance of ViewGroup){
            //递归
            View temp = find((ViewGroup)v,id);
            //如果找到了,就返回temp,如果没有找到,继续当前的for循环
            if(temp != null){
                return temp;
            }
        }
    }
    //到最后还没用找到,代表该ViewGroup vg 并不包含一个有该id的孩子,返回空
    return null;
}

View相关的相关的过程就先写这么多吧~如果以后有什么补充,那就再加一个《补充篇》。


Handler机制

1、Handler的理解

先通一下Handler机制,首先我们为什么需要Handler机制,因为我们需要一个消息队列,用于子线程和主线程之间进行Meassage的传递。
首先我们的主线程会初始化一个Looper对象,这个Looper使用LocalThread和主线程进行唯一绑定。main方法启动时,Looper对象的loop方法开始启动,内部会轮询MessageQueue,从中拿到在子线程中post的Message,Message会含有我们在主线程初始化的Handler,我们的就可以调用我们Handler内部的回调方法,在其中更新UI。
这样我们就完成了,子线程发送Message到主线程去执行的过程。
这其中有几个需要注意的点:

  • handler.sendMessage(message);会将message与handler进行绑定。(message中target对象会指向handler)
  • sendMessage会把message发送至MessageQueue之中。MessQueue在Looper初始化的时候和Looper一起绑定一起初始化,而我们的Handler初始化的时候会初始化Looper因此也就获得MessageQueue。
  • MessageQueue内部没有使用集合去存Message,而是让Message以链表的形式按时间的先后进行存。

这部分内容可能有些绕,建议各位宝宝可以自己结合代码进行梳理。

1.1、loop为什么是死循环,并且没有阻塞住主线程

1、为什么是死循环?
这里为什么是死循环,因为死循环才能保证此线程(主线程)不会因为线程执行完毕被杀死。
2、没有阻塞住主线程?
所谓的阻塞主线程就是说对应的Activity的生命周期里执行了耗时操作。loop没有在onCreate之中启动,所以不会阻塞住onCreate方法的执行。而其他的生命周期方法,是通过AMS进行回调。
ActivityThread对象,在其初始化代码中会创建一个H(Handler)对象和一个AppplicationThread(Binder)对象(此H会通过不同的表示形式回调不同的Activity生命周期)Binder负责接远程AMS的IPC调用,收到消息后,通过Handler将消息发送到消息队列,UI主线程会异步的从消息队列中取出消息并执行操作。

1.2、主线程死循环是否很耗费CPU资源

loop启动时会创建一个Binder线程,用于在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。

1.2、Handler的延时是怎么做到的

简单说,是通过Message中的when变量进行计时。这里如果时间没有达到要求,就会阻塞住当前任务队列。此时如果有新的任务被提交到MessageQueue,此时nativeWake方法被调用,新的任务被插入到队列头部,即延时任务的前面,对应执行此任务,然后进行阻塞知道时间达到要求。
下面总结,结合源码食用更过瘾:

  • 1、假设我们postDelay()一个1秒钟Message、消息进队,MessageQueue开始阻塞,Looper阻塞,mBlocked为true,在enqueueMessage的if中将needWake = mBlocked。
  • 2、然后post一个新的任务、消息进队,判断现在A时间还没到、正在阻塞,把新的任务插入消息队列的头部(MyTask任务的前面),然后此时needWake为true调用nativeWake()方法唤醒线程。
  • 3、MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper。
  • 4、Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续阻塞;
  • 5、直到阻塞时间到或者下一次有Message进队。

1.3、Handler内存泄漏

因为内部匿名类会持有外部的引用,而我们通常会在子线程持有handler,如果子线程进行耗时操作,那么此时我们Activity恰好要被回收,但是犹豫这条引用链没有断,因此Activity不会被回收,就会存在内存泄漏。
处理方式:


private final MHandler mHandler = new MHandler(this);

private static final class MHandler extends Handler {
    //因为是静态类,需要持有一个外部类的引用
    private final WeakReference<MainActivity > mActivcity;

    public MHandler(MainActivity activity) {
        mActivcity= new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(final Message msg) {}
}


Service相关

1、Serivce的生命周期

  • 只是用startService()启动服务:onCreate() -> onStartCommand() -> onDestory
  • 只是用bindService()绑定服务:onCreate() -> onBind() -> onUnBind() -> onDestory
  • 同时使用startService()启动服务与bindService()绑定服务:onCreate() -> onStartCommnad() -> onBind() -> onUnBind() -> onDestory

1.1、有俩个进程,进程A有一个线程,进程B有一个服务,那个进程被先回收

这个问题比较的有趣,当时我在面百度的时候别问到的。其实这里考察的知识点很简单,就是问进程的优先级的~
A进程容易被回收,因为一个进程如果没有任何四大组件(例如只有一个application),很容易被系统回收。所以后台工作,需要借助Service之类的,提高进程优先级。

1.2、不同情况下的生命周期回调

  • 首次启动调用onCreate(),再次启动只会重复调用onStartCommand(bind不会调用onStartCommand,并且onBind()也只会调用一次,但会重复回调onServiceConnect和onServiceDisConnect)
  • 先以startService方式启动服务,然后再用bindService绑定到这个服务,之后使用unbindService解除绑定,此时服务并不会因此而终止,而是继续运行,直到我们使用stopService来停止这个服务。

启动模式

1、四种启动模式

概念性的东西就不在重复了,一般面试官都会结合各种各样的场景去考察去singleTop,singleTask,singleInstance的理解。

1.1、onNewIntent被复用,那参数Intent是新的还是旧的

onNewIntent中Intent是最新的,但是如果不调用setIntent,那么其他方法调用getIntent将还是老的……如果此activity被onPause,那么onNewintent以后,onStart(),onResume()依然执行,如果不set就不是最新的intent。

1.2、简述IntentFilter得过滤规则

  • action:(字符串)Intent需要有且至少匹配一条
  • category:(字符串)Intent可以不add此参数。但是如果add了,所add的全部catagory必须与IntentFilter中全部匹配。PS:为什么可以不add,如果不add,系统会默认添加android.intent.category.DEFAULT,因此我们在自定义intent-fliter时,如果添加了category,必须增加android.intent.category.DEFAULT
  • data:(非字符串)与cation类似,必须有且至少匹配一个

尾声

暂时能想到的,比较基本但又很考验日常积累的常考知识点差不多就这么多。日后如果又想起来其他的内容,会在《补充篇》中继续拓展。
以上内容,皆出自我的学习记录,如果有不同见解,或是有错误之处,欢迎评论区指出~


我们共同维护的公众号,不感兴趣的就直接无视掉吧

因为身边的同学从事互联网相关职业的比较多,并且大家闲时聊天时总会吐槽找工作有很多坑,所以打算把身边同学找工作的经验,统统收集起来。提供给想从事这方面同学,希望圈内好友可以共同进步,共同少踩坑。

Android进阶之路的绊脚石
IT面试填坑小分队
上一篇:(周期计划-6)从公司项目配置看Gradle(2.0)


下一篇:[动态代理三部曲:上] - 动态代理是如何"坑掉了"我4500块钱