android面试笔记总结

作者: 的一幕 | 来源:发表于2019-04-16 19:14 被阅读133次

    Activity中的几种启动模式


    standard:这个是android默认的Activity启动模式,每启动一个Activity都会被实例化一个Activity,并且新创建的Activity在堆栈中会在栈顶。
    singleTop:如果当前要启动的Activity就是在栈顶的位置,那么此时就会复用该Activity,并且不会重走onCreate方法,会直接它的onNewIntent方法,如果不在栈顶,就跟standard一样的。
    singleTask:该种情况下就比singleTop厉害了,不管在不在栈顶,在Activity的堆栈中永远保持一个。
    singleInstance:该种情况就用得比较少了,主要是指在该activity永远只在一个单独的栈中。

    android消息机制

    消息机制就是指的Handler、Looper、MessageQueue、Message之间如何工作的,首先是Looper会创建好带有Looper的线程等待其他线程发送消息过来,一旦有消息之后,Looper取出MessageQueue对头的Message,之后调用了Handler的dispatchMessage方法,把消息传给了Handler,之后消息到了工作线程(主线程)。
    这里会问到,一个线程会有几个Looper,几个Handler,以及Looper会存在线程哪里?
    一个线程一个Looper,可以有多个Handler,Looper会存在线程的ThreadLocal对象里,该对象是线程的缓存区

    事件分发

    事件分发主要分三块:分发、拦截、消费;
    当我们触摸到屏幕的时候,默认会先走Activity的分发,接着走ViewGroup的分发,然后到ViewGroup的拦截,后面再到View的分发事件,最后会传到View的消费事件,如果View不消费,紧接着回传到ViewGroup的消费事件,如果ViewGroup也不消费,最后回到View的消费事件
    这里会问到事件冲突的问题?
    事件遵循一个原则,就是看他有没有事件消费。比如一个LinearLayout里面有一个Button,点击LinearLayout会触发到Button吗,这里就看LinearLayout有没有设置点击事件,如果有就不会传递到Button,如果没有就会传递给Button。

    android性能优化、内存优化

    性能优化:可以从界面、apk瘦身、混淆说起,界面优化多可以使用include、merge、ViewStub、约束布局来做起,include可以提取公共的布局,merge可以减少布局层次、ViewStub是使用的时候才去创建View,减少空间的占用、约束布局一来可以减少布局的层次、二来可以提高开发的效率;apk瘦身可以用android studio的lint检测工具检测资源文件等,混淆可以起到文件大小减少的作用。

    内存优化

    内存优化通常指的内存溢出,主要涉及到的问题还是该释放的资源,没有及时让GC处理器回收,通常主要表现是动画、上下文对象、EventBus、AsycTask、Handler、单例Bitmap都会影响,通常要做的是释放他们未终止的动作,释放锁定的上下文对象。

    View的绘制

    view的绘制主要指view的onMeasure、onLayout、onDraw几个方法,其实要了解几个方法,需要追溯到android中本身界面的结构,首先整体是一个PhoneWindow的对象,然后是一个DecorView,DecorView里面包括一个ViewStub的ToolBar,然后下面是一个FramLayout,也就是我们经常在Activity中setContentView中的content内容。说完了android界面的结构,下面就是说下如何绘制的,绘制首先是触发到DecorView的onMeasure方法,它的测量规则包含了手机屏的宽高,并且测量模式是MeasureSpec.EXACTLY。所以这里明白了DecorView(FrameLayout)的测量参数是什么意思了,紧接着就是测量它下面的ViewGroup了,其中ViewGroup里面有个measureChild方法去测量孩子,这里会问到几种父布局的测量模式和子View的测量模式组合:

    ViewGroup的测量mode MeasureSpec.EXACTLY MeasureSpec.AT_MOST MeasureSpec.UNSPECIFIED
    childDimension>0 size=childDimension;mode=EXACTLY size= childDimension;mode=EXACTLY size= childDimension;mode=EXACTLY
    childDimension == LayoutParams.MATCH_PARENT size=Viewgroup的size;mode=EXACTLY size=Viewgroup的size;mode=AT_MOST size=Viewgroup的size;mode=UNSPECIFIED
    childDimension == LayoutParams.WRAP_CONTENT size=Viewgroup的size;mode=AT_MOST size=Viewgroup的size;mode=AT_MOST size=Viewgroup的size;mode=UNSPECIFIED

    测量处理完了之后,紧接着就是View的onLayout,其中onLayout的作用是给View固定好位置,该方法传进来的几个参数是相对于自己的parent的位置,左上角是(0,0)的坐标。最后就是我们的onDraw,该方法是我们需要在画布上画东西的方法,一般包括画背景、画图层等等。

    Eventbus原理

    EventBus是一款在android开发中使用的发布/订阅事件的总线框架,基于观察者模式,将事件的接收者和发送者分开,基本包括了如下几个步骤:
    注册事件的订阅方法:该步骤主要是找到订阅者下面有哪些方法需要被订阅
    订阅操作:将需要被订阅的方法放到类似HashMap的数据结构中存储起来,方便后面发送事件和取消注册等资源的释放的时候使用
    发送事件:该步骤首先遍历事件队列,然后从队列中取出事件,并且将事件从队列中移除,拿到事件后,判断事件处于的什么线程,如果是非UI线程,则需要Handler去处理,如果是的话,则直接通过反射调用被观察的方法。
    反注册:该步骤就没什么好说的,主要是上面存储到HashMap中的被订阅的方法的移除,释放在内存中的资源。

    Rxjava的操作符有哪些,说说他们的作用

    just:将同种数据源组合放到被观察者上面
    from:将类似数组、集合的数据源放到被观察者上面
    map:将一种数据源,转化成另外一种
    flatmap:将一种数据源,转化成另外一种数据,并且被转化的数据是乱序排列的
    concatmap:将一种数据源,转化成另外一种数据,并且被转化的数据是按照先前的数据源顺序排序的
    toList:将数组的形式转化成List集合
    subscribeOn:设置Observable的call方法所在的线程,也就是数据来源的线程
    observeOn:设置subscribe的call方法所在的线程,也就是数据处理的线程
    filter:在被观察者的数据层过滤数据
    onErrorResumeNext:出错的时候,可以指定出错的时候的被观察者
    retryWhen:出错的时候,重新走一遍被订阅的过程
    concat:合并相同类型的被观察者到一个被观察者身上,有点类似集合、数组拼接数据。
    zip:处理多种不同结果集的数据发射,一般用得多的地方是多个网络请求组合然后统一处理业务逻辑。
    还有很多操作符就自己去看,这些操作符已经够面试用的了。

    线程锁 锁方法和类对象啥的有啥区别

    线程锁锁方法:是需要等到该线程用完了该方法才能释放同步锁
    线程锁锁类对象:是需要等到该线程用完了该类对象才能释放同步锁
    区别:是锁方法的区域要小 锁类对象包括了该类的所有属性

    AsyncTask原理

    AsyncTask主要是对android中java的线程池的封装,该类中默认开启了两个线程池,一个线程池负责任务的排队处理,保证任务被单个处理,另外一个线程池用来专门处理任务,最后任务处理完了,交给Handler发送消息到主线程,然后Handler处理线程,交给了onPostExecute方法。

    说说MVP和MVVM的特点

    MVP:主要是分离了M层和V层的代码,通过P层来建立他们的关联,实现M层和V层的解耦。缺点就是每增加一个功能,需要增加相应的接口回调。没办法,MVP的核心就是通过接口实现隔离,将相关的业务层交给了P层。
    MVVM:主要是用到了观察者模式,通过数据的改变来通知相应的View改变的过程。M层和上面的MVP中的M层是一样的,都是网络请求+数据缓存来实现该层的,里面的双V,一个指的ViewModel实现的,另外一个AndroidDataBinding实现V层,ViewModel层获取到M层的数据后,通过观察者模式通知AndroidDataBinding在UI上的改变。缺点的话,只能吐糟下AndroidDataBinding了,在xml中写逻辑的时候,一点提示代码都没有,感觉完全是在写js似的,可读性肯定对于初级的来说还是有点难看懂的。

    android中用到的观察者模式有哪些地方

    观察者模式是由一个发送者(发送者是笔者自己的称呼,觉较之被观察者贴切得多)和一个观察者构成的、发送者在状态改变时(用户操作、程序主动改变等)主动通知所有观察者作相应的刷新。
    android中最经典要说ListView的数据源发生变化了,刷新列表的事例。在setAdapter的时候,生成一个AdapterDataSetObserver,紧接着就是订阅上该观察者,该观察者onChange方法里面有requestLayout方法,该方法是触发UI发生变化的方法。在BaseAdapter里面可以看到notifyDataSetChanged实际上触发的是DataSetObservable被观察者的notifyChanged方法,notifyChanged会触发AdapterDataSetObserveronChange方法。所以最终会走listView的requestLayout,最后刷新了UI。

    说说google新出的Lifecycle框架

    将类的生命周期方法移交到Lifecycle中管理,实现对类的生命周期的监听,从而在Lifecycle中处理生命周期的逻辑代码。这里涉及到几个对象:
    LifecycleObserver接口( Lifecycle观察者):实现该接口的类,通过注解的方式,可以通过被LifecycleOwner类的addObserver(LifecycleObserver o)方法注册,被注册后,LifecycleObserver便可以观察到LifecycleOwner的生命周期事件。
    LifecycleOwner接口(Lifecycle持有者):实现该接口的类持有生命周期(Lifecycle对象),该接口的生命周期(Lifecycle对象)的改变会被其注册的观察者LifecycleObserver观察到并触发其对应的事件。
    Lifecycle(生命周期):和LifecycleOwner不同的是,LifecycleOwner本身持有Lifecycle对象,LifecycleOwner通过其Lifecycle getLifecycle()的接口获取内部Lifecycle对象。
    State(当前生命周期所处状态):几种事件状态。
    Event(当前生命周期改变对应的事件):当Lifecycle发生改变,事件状态的回调event。

    okhttp原理

    okhttp主要实现了异步、同步的网络操作,创建了不同的call对象,这里的call对象是一个个的runnable对象,由于我们的任务是很多的,因此这里有Dispatcher包装了线程池来处理不同的call,其中该类中创建了三种队列,分别用于存放正在执行的异步任务,同步队列,以及准备的队列。最后在执行每个任务的时候,采用队列的先进先出原则,处理每一个任务,都是交给了后面的各种拦截器来处理,有请求准备的拦截器、缓存拦截器、网络连接的拦截器,每一个拦截器组成了一个责任链的形式。到最后返回response信息。
    OkHttp的底层是通过Java的Socket发送HTTP请求与接受响应的(这也好理解,HTTP就是基于TCP协议的),但是OkHttp实现了连接池的概念,即对于同一主机的多个请求,其实可以公用一个Socket连接,而不是每次发送完HTTP请求就关闭底层的Socket,这样就实现了连接池的概念。而OkHttp对Socket的读写操作使用的OkIo库进行了一层封装。

    JVM工作流程

    首先我们编写的java源程序(.java)文件,由java编译器(Java Compiler)编译成字节码文件(.class),然后由java虚拟机的类加载器加载各个类的字节码文件,加载完毕后交给jvm的执行引擎执行。

    示意图

    GC回收机制

    GC回收内存主要指运行区中的堆内存回收,其中堆内存中的区域又可以分为年轻代、老年代、元空间(1.8以后)/永久代(1.8以前)
    一、 当现在有一个新的对象产生,那么对象一定需要内存空间,于是现在就需要为该对象进行内存空间的申请。

    二、 首先会判断伊甸园区是否有内存空间,如果此时内存空间充足则将对象保存在伊甸园区。

    三、 但是如果此时伊甸园内存空间不足,那么会自动执行一个MinorGC,将伊甸园区的无用内存空间进行清理,清理之后会继续判断伊甸园区的内存空间是否充足,如果充足则将新的对象进行内存分配。

    四、 如果执行了MinorGc之后伊甸园区空间依然不足,那么这个时候会进行存活区的判断,如果存活区有剩余空间,则会将伊甸园区的部分活跃对象保存到存活区,

    那么随后会继续判断伊甸园区内存空间是否充足,如果空间充足则进行新对象进行内存分配。

    五、 如果此时存活区也已没有内存空间了,则将继续判断老年区,如果此时老年区空间充足,则将存活区的活跃对象保存到老年代,而后存活区会出现空余空间,而后伊甸园区将活跃对象保存在存活区之中,而在伊甸园区会为新对象进行内存分配。

    六、 如果这个时候老年代也满了,那么这个时候会产生MajorGC(FullGC)进行老年代的内存清理。

    七、 如果老年代执行FullGC后依然无法进行进行新对象的创建。则抛出OOM异常,OutOfMemary内存溢出。

    算法题

    两个很大的数字字符串相加,在只能使用int类型的变量下实现字符串相加的问题

    public void addString() {
            String a = "12345678";
            String b = "2356";
            //取位数多的字符串作为位数
            int length = Math.max(a.length(), b.length());
            //借助StringBuilder的reverse方法将两个字符串倒叙,因为加的时候是从个位相加的
            StringBuilder aBuilder = new StringBuilder(a);
            StringBuilder bBuilder = new StringBuilder(b);
            String aRe = aBuilder.reverse().toString();
            String bRe = bBuilder.reverse().toString();
            //定义一个变量来确定是不是加起来大于10
            boolean isMax10 = false;
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < length; i++) {
                int currentA;
                if (aRe.length() > i) {
                    currentA = Integer.parseInt(aRe.substring(i, i + 1));
                } else {
                    currentA = 0;
                }
                int currentB;
                if (bRe.length() > i) {
                    currentB = Integer.parseInt(bRe.substring(i, i + 1));
                } else {
                    currentB = 0;
                }
                int current = currentA + currentB;
               //如果前面的数加起来是大于10的话,默认该为等于1
                int left = isMax10 ? 1 : 0;
                if (current + left >= 10) {
                    left = (current + left) - 10;
                    isMax10 = true;
                } else {
                    left = current + left;
                    isMax10 = false;
                }
                result.append(left + "");
            }
            System.out.println("result:" + result.reverse().toString());
     }
    

    二分法查找
    说到该二分法的时候,确实不想写这个代码,因为在这次面试被问到该问题,而自己只知道原理,非要写出来,那真的是个硬伤,所以自己给总结出来,方便以后再面试不会出现同样的问题。
    找出想要找的数在数组中的位置

    public void dichotomous() {
            int[] a = {1, 3, 5, 7, 9, 11};
            int find = 5;
            int resultIndex = -1;
            int min = 0;
            int max = a.length - 1;
            while (min <= max) {
                //找到中间的索引
                int average = (min + max) / 2;
                //如果中间的数大于要找的数,那就需要在左边一半的数中去找
                if (a[average] > find) {
                    max = average - 1;
                } else if (a[average] < find) {
                    //到右边一半的数中去找
                    min = average + 1;
                } else {
                    //如果相等说明就找到了该数的索引
                    resultIndex = average;
                    break;
                }
            }
            System.out.println("index:" + resultIndex);
    }
    

    [用两个栈实现队列 - Java],栈的特点是先进后出,队列是先进先出
    说起这个面试题的时候真的是惭愧,大致的思路是有的,但是面试是需要手动写出结果,所以这里给整理出来,方便后面可以看到:

    public class StackQueue {
        
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();
        
        public void push(int node){
            //实现队列的push直接将元素放到第一个栈中
            stack1.push(node);
        }
        
        public int pop(){
            //如果第2个栈是空的,需要将第一个栈的数据依次取出来
            if(stack2.empty()){
                //直到第一个栈中的数据取完了
                while(!stack1.empty())
                    stack2.push(stack1.pop());
            }
            return stack2.pop();
        }
    }
    

    冒泡排序

    //冒泡排序,从小到到大排序
    public static void order() {
        int[] a = {3, 0, 1, 2, 11, 7};
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = i; j < a.length; j++) {
                int temp;
                if (a[i] > a[j]) {
                    temp = a[j];
                    a[j] = a[i];
                    a[i] = temp;
                }
            }
        }
        for (int i = 0; i < a.length; i++) {
            System.out.println("result:" + a[i]);
        }
    }
    

    (手写算法)用三个线程,顺序打印字母A-Z,输出结果是1A、2B、3C、1D 2E...,思路:加锁进行限制,并配合wait()和notifyAll()

    static char c = 'A';
    static int i = 0;
    public static void run() {
        Runnable runnable = new Runnable() {
            public void run() {
                synchronized (this) {//加锁
                    try {
                        int threadId = Integer.parseInt(Thread.currentThread().getName());
                        while (i < 26) {
                            //只有在当前顺序=线程名字-1的时候,让i的值加1,否则该线程处于等待中
                            if (i % 3 == threadId - 1) {
                                System.out.println(threadId + "" + (char) c++);
                                i++;
                                notifyAll();// 唤醒处于等待状态的线程
                            } else {
                                wait();// 释放当前锁并进入等待状态
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }//执行结束释放当前锁
            }
        };
        Thread t1 = new Thread(runnable, "1");
        Thread t2 = new Thread(runnable, "2");
        Thread t3 = new Thread(runnable, "3");
        t1.start();
        t2.start();
        t3.start();
    }
    

    翻转单词顺序列,如输入“student. a am I”,输出“I am a student.”

    //方法2:使用java提供的字符串方法split
    public class Solution {
        public String ReverseSentence(String str) {
            if(str==null||str.trim().equals("")){
                return str;
            }
            //将字符串按照空格分割成字符串数组
            String[] a = str.split(" ");
            StringBuffer o = new StringBuffer();
            int i;
            //倒序输出字符串数组
            for (i = a.length; i >0;i--){
                o.append(a[i-1]);
                //每一个单词需要分隔开
                if(i > 1){
                    o.append(" ");
                }
            }
            return o.toString();
        }
    }
    

    请实现一个函数用来找出字符流中第一个只出现一次的字符,若没有返回#。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g";当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

    public class Check {
        int[] hashtable = new int[256];
        StringBuffer s = new StringBuffer();
    
        public char checks(String str) {
            char[] chars = str.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                Insert(chars[i]);
            }
            return FirstAppearingOnce();
        }
    
        private void Insert(char ch) {
            //按照键值对的形式,将value存到StringBuffer里面
            s.append(ch);
            //没获取到对应的数组,将该char的索引对应的数组值加1
            hashtable[ch]++;
        }
    
        private char FirstAppearingOnce() {
            char[] str = s.toString().toCharArray();
            for (char c : str) {
                //取的时候,如果对应的数组值等于1说明在该字符串中是唯一的,此时直接返回对应的char
                if (hashtable[c] == 1)
                    return c;
            }
            return '#';
        }
    }
    

    实现数组的快排

    public class QuickSort {
        // 简化使用方法 传入一个参数即可
        public static void quickSortHelp(int arr[]) {
            quickSort(arr, 0, arr.length - 1);
        }
    
        public static void quickSort(int arr[], int low, int high) {
            if (low < high) {
                // 获取排序后最中间那个元素的角标
                int partition = partition(arr, low, high);
                // 递归 把这个元素左边的所有数看作一个数组继续进行快排
                quickSort(arr, low, partition - 1);
                // 递归 把这个元素右边的所有数看作一个数组继续进行快排
                quickSort(arr, partition + 1, high);
    
            }
        }
    
        // 对数组进行排序
        public static int partition(int arr[], int low, int high) {
            // 当low=high时说明排序已经进行完了
            while (low < high) {
                //7>2
                //2, 8, 9, 6, 10, 5, 4, 66, 11, 15, 3, 18, 7
                // 寻找low右边比arr[low]小的值
                while (arr[high] >= arr[low] && high > low) {
                    high--;
                }
                // 把找到的值都放到low的左边
                swap(arr, low, high);
                // 寻找high左边比arr[high]大的值
                while (arr[low] <= arr[high] && low < high) {
                    low++;
                }
                // 把找到的值都放到high的右边
                swap(arr, low, high);
            }
            // 这个low就是排序后最中间元素的角标
            return low;
    
        }
    
        // 把两个数的位置调换
        public static void swap(int arr[], int low, int high) {
            int temp = arr[low];
            arr[low] = arr[high];
            arr[high] = temp;
        }
    
        public static void main(String[] args) {
    
            int[] arr = {2, 8, 9, 6, 10, 5, 4, 66, 11, 15, 3, 18, 7};
            quickSortHelp(arr);
            for (int i : arr) {
                System.out.print(i + " ");
            }
        }
    
    }
    

    通过数字输出大写的人命币

    public class Monney {
        public static void main(String[] args) {
            String[] unit = {"元", "十", "百", "千", "万", "十万", "百万", "千万", "亿"};
            String[] shu = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
            String[] unit1 = {"角", "分"};
            float monney = 245.567f;
            String s = monney + "";
            String[] split = s.split("\\.");
            int zhengshu = 0;
            int xiaoshu;
            if (split.length == 2) {
                zhengshu = Integer.parseInt(split[0]);
                xiaoshu = Integer.parseInt(split[1]);
            } else {
                xiaoshu = 0;
                zhengshu = (int) monney;
            }
            int length = 0;
            int zhengshu_z = zhengshu;
            StringBuilder sb = new StringBuilder();
            while (zhengshu_z > 0) {
                length++;
                zhengshu_z = zhengshu_z / 10;
            }
            if (length > 9) {
                throw new RuntimeException("数字过大");
            } else {
                String[] string = new String[length];
                for (int i = 0; i < length; i++) {
                    int shuzi = zhengshu % 10;
                    zhengshu = zhengshu / 10;
                    if (shuzi > 0) {
                        string[i] = shu[shuzi] + "" + unit[i];
                    } else {
                        string[i] = "";
                    }
                }
                for (int i = string.length - 1; i >= 0; i--) {
                    sb.append(string[i]);
                }
            }
            if (xiaoshu > 0) {
                if (xiaoshu >= 100) {
                    xiaoshu = xiaoshu / 10;
                }
                int size = 0;
                int temp = 1;
                while (temp > 0) {//34
                    temp = xiaoshu / 10;//3/10=
                    xiaoshu = xiaoshu % 10;
                    if (temp > 0) {
                        sb.append(shu[temp] + "" + unit1[size]);
                    } else {
                        sb.append(shu[xiaoshu] + "" + unit1[size]);
                    }
                    size++;
                }
            }
            System.out.println("result:" + sb.toString());
        }
    }
    

    相关文章

      网友评论

        本文标题:android面试笔记总结

        本文链接:https://www.haomeiwen.com/subject/vavbwqtx.html