美文网首页Android开发经验谈Android高级进阶
Android面试刨根问底之常用源码篇(一):Android优化

Android面试刨根问底之常用源码篇(一):Android优化

作者: 小小小小怪兽_666 | 来源:发表于2021-01-13 21:33 被阅读0次

    本着针对面试负责任的态度,记录面试过程中各个知识点,而不是入门系列,如果有不懂的自行学习。

    目前总结了以下几个方面:

    • Android优化
    • HashMap分析
    • Handler源码分析
    • OkHttp分析
    • Retrofit分析
    • 自定义View

    由于篇幅原因拆分成两个部分分享,今天就先讲解前面三个方面,剩下的在后面继续持续更新。

    Android优化

    大致分为四点去回答。快、稳、小、省

    1. 快

    启动快,加载快,避免卡顿

    基本操作

    • 主线程不做耗时操作
    • application里对必要的三方库延迟初始化(延迟加载,异步加载,分布加载)
    • 启动白屏优化

    View优化

    • View 布局(viewstub,include,merge,层级深)
    • 复杂页面细分优化
    • 过度绘制的优化
    • xml中无用的背景不设置
    • 控件无用属性删除

    内存优化

    • 页面切换,前后台切换
    • fragment的懒加载
    • 必要的缓存
    • 空间换时间
    • 四大引用的合理使用
    • 减小不必要的内存开销
    • 数据bean的合理定义
    • ArrayList、HashMap的使用
    • 线程池、bitmap、view的复用
    • 不用的大对象主动设置null

    代码优化

    • for循环内不定义对象
    • 使用文件IO代替数据库
    • 自定义Drawable不在draw()里面创建对象操作
    • 类中没有使用到成员变量的方法可以设置static

    2. 稳

    稳定不崩溃,减小crash,避免anr

    • 主线程不做耗时操作
    • activity 5秒、broadcast 10秒、service 20秒
    • 资源对象及时关闭(Cursor,File)
    • Handler的处理
    • 避免内存泄露
    • crash上传机制
    • WebView的内存泄露

    3. 小

    安装包小

    • 代码混淆(proguard)
    • 资源优化(lint)
    • 图片优化(mipmap/webp)

    4. 省

    省电省流量

    • 接口定义
    • 接口缓存

    性能分析工具:MAT/TracView/LeakCanary/blockCanary/MemoryMonitor/HeapViewer

    HashMap分析

    • 基础知识

      1. 可以接受null键和值,而Hashtable则不能
      2. 非synchronized,所以很快
      3. 存储的是键值对
      4. 使用数组+链表的方式存储数据
      5. 对key进行hash(散列)算法,所以顺序不固定
      6. 实际使用Node存储
    • 常量&变量

    
     // public class HashMap extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}
      /**
       * The default initial capacity - MUST be a power of two.
       默认数组长度
       */
      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
      /**
       * The maximum capacity, used if a higher value is implicitly specified
       * by either of the constructors with arguments.
       * MUST be a power of two <= 1<<30.
       * 数组最大长度
       */
      static final int MAXIMUM_CAPACITY = 1 << 30;
    
      /**
       * The load factor used when none specified in constructor.
       * 默认装填因子
       */
      static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
      static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
      }
    
        /**
       * The number of key-value mappings contained in this map.
       */
      transient int size;
    
      /**
       * 阈值
       * The next size value at which to resize (capacity * load factor).
       *
       * @serial
       */
      // (The javadoc description is true upon serialization.
      // Additionally, if the table array has not been allocated, this
      // field holds the initial array capacity, or zero signifying
      // DEFAULT_INITIAL_CAPACITY.)
      int threshold;
    
      public V put(K key, V value) {
          return putVal(hash(key), key, value, false, true);
      }
      //实际存储方法
      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {}
      //扩容方法
      final Node<K,V>[] resize() {}
    
      public V get(Object key) {
          Node<K,V> e;
          return (e = getNode(hash(key), key)) == null ? null : e.value;
      }
      //实际取值方法
      final Node<K,V> getNode(int hash, Object key) {}
    
    
    • 用法

      1. put(key,value)
        调用hashCode(key),使用node存储hash,key,value,如果hashcode存在则使用链表存储。

      2. get(key)
        根据key的hashcode找到Entry,然后获取值对象,如果根据hashcode找到的是个链表,再去根据key.equals()判断,链表中正确的节点。

    • 关于扩容

      当HashMap的大小超过了阈值(size> threshold)的时候(默认的装填因子为0.75,也就是说当一个map填满了它定义容量的75%就会去扩容),HashMap大小会扩大到原来的2倍。整个过程类似于创建新的数组,将原数组的元素重新hash后放到新数组中(rehashing)。

    HashMap是非同步的,所以在多线程中使用时需要注意扩容等问题

    • 相关概念
      • hashing的概念
      • HashMap中解决碰撞的方法
      • equals()和hashCode()的应用,以及它们在HashMap中的重要性
      • 不可变对象的好处
      • HashMap多线程的条件竞争
      • 重新调整HashMap的大小

    参考地址:http://www.importnew.com/7099.html

    以上是网上能搜到的解释,下面是个人总结的知识点提要

    如面试遇到此问题,第一步,反问面试官,您说的是哪个版本的HashMap

    • hashmap底层使用 数组+链表 的数据结构,实现存储数据,使用拉链法解决碰撞问题。
    • map.put(key,value)的时候,内部会对key进行一次hash算法,得到一个hash值,对这个hash值&操作得到元素在数组中的位置。
    • 如果该位置没有元素,那么直接加入,如果发生碰撞了,那么用拉链法,需要遍历链表比较key和hash值,如果有就覆盖,没有就到表尾了,所以会插到表尾。
    • 初始容量为16,加载因子0.75,当map添加的元素超过12个的时候会触发扩容机制。数组的容量翻倍,已经存入的元素做rehash的操作,重新在数组中找位置存储。
    • java8后改为碰撞链表元素超过8个,用红黑树实现
    • java8在表尾,java7是在链表头插入

    思考点:
    什么情况下考虑使用SparseArray和ArrayMap替换HashMap的情况


    相关面试题

    1. 为什么HashMap的容量总是2x

    从源码中可以看到,当putVal方法中,是通过tab[i = (n - 1) & hash]得到在数组中位置的。
    依稀记得当年在学校中,学到hash算法的时候,学的都是n%size运算,来确定数值在数组中的位置,而HashMap中为什么要用到&运算呢。

    原因如下

    1. 大家都知道&运算要比%运算速度快,虽然可能是几毫米的差别。
    2. 在n为2x时,(n-1)&hash == hash%n

    为什么容量总是2x
    首先,Hash算法要解决的一个最大的问题,就是hash冲突,既然不能避免hash冲突,那么就要有个好的算法解决。

    而在做&运算时,如果选用非2n的数时,n-1转换为二进制,不能保证后几位全为1,这样做在&hash的运算中,不能做到均匀分布。违背了(n-1)&hash的初衷。

    (16)10 = 24 = (10000)2
    (16-1)10 =(1111)2
    假设n的值非2x值,10
    (10-1)10 =(1001)2
    (19-1)10 =(10011)2

    10011
    &1111
    =(11)2=(3)10

    10011
    &1001
    =(1)2=(1)10

    同样的%运算,19%16 = 3 ,19%10 = 9。

    任意一个数与(1111)2做&运算,都不会因为(1111)2的值而影响到运算结果。

    2. 如果初始化HashMap的时候定义大小为非2x会影响到计算吗?

    答案是,肯定不会,这种情况JAVA的工程师肯定考虑到了。

    源码中我们可以看到,传入的capacity只是影响到了threshold的值,而threshold的值还是通过tableSizeFor()确定的。

      public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
    public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
            this.loadFactor = loadFactor;
            this.threshold = tableSizeFor(initialCapacity);
        }
    
    

    在tableSizeFor()方法中。

     static final int tableSizeFor(int cap) {
          // cap=10
            int n = cap - 1;
            // n  =9   1001
            n |= n >>> 1;
        // (1001)|(0100)=1101
            n |= n >>> 2;
        //(1101)|(0011)=1111
            n |= n >>> 4;
         // (1111)|(0000)=1111
            n |= n >>> 8;
        // (1111)|(0000)=1111
            n |= n >>> 16;
        // (1111)|(0000)=1111
          //return n+1 = (10000)=16
      //确保threshold 为16, 2的4次幂
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        }
    
    

    在putVal()方法中,如果第一次添加值,那么table==null,会进入到resize()方法中,这个时候,就会用到threshold创建新的Node数组。

    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
          //第一次添加值,table==null; oldCap = 0;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
          //将threshold的值设置为oldThr,下面创建table的时候用到
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
               ....
            }
            else if (oldThr > 0) 
              //通过threshold设置新数组容量
                newCap = oldThr;
            else { 
                ....
            }
            if (newThr == 0) {
               ....
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            //通过threshold设置table的初始容量
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
           ....
            return newTab;
        }
    
    

    通过以上操作,不论初始化HashMap的时候,传入的容量是多少,都能保证HashMap的容量是2x

    Handler源码分析

    一直在纠结一个事,因为自己不爱看大段的文字。

    自己写总结的时候到底要不要贴上部分源码。

    后来硬着头皮加上了,因为源码里很多东西比自己写的清楚。

    RTFSC

    相关概念

    Handler Message MessageQueue Looper ThreadLocal

    Handler机制的完整流程

    1. Message#obtain()
    2. Handler#
    3. Handler#send/post
    4. MQ#enqueueMessage() *消息的排序
    5. Looper#prepareMainLooper()
    6. Looper#prepare()
    7. ThreadLocal机制
    8. Looper#loop()
    9. MQ#next() *延迟消息的处理
    10. Handler#dispatchMessage()

    Message#obtain()

    message中的变量自己去看源码,target,callback,when

    从handler或者是message的源码中都可以看到,生成Message的最终方法都是调用obtain。

    ps:如果你非要用Message的构造方法,那么看清楚他的注释,构造方法上面的注释写的也很清楚,

        /** 
         * Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
         */
        public Message() {
        }
    
    

    下面来分析一波obtain()方法:

    1. 为什么上来就是一个同步?

      任意线程都可以创建message,所以为了维护好内部的messge池,加锁

    2. sPool是个什么东西

    字面上看是个池子,但是从定义上看,是一个Message。为什么还要说成一个message池呢?因为Message内部有个next变量,Message做成了一个链表的形式。这个池子怎么存储message呢?稍后分析源码。

    通过读obtain()的源码,结合链表的知识,很容易理解Message中Spool的原理。

    
        public static final Object sPoolSync = new Object();
        private static Message sPool;
        private static int sPoolSize = 0;
    
        /**
         * Return a new Message instance from the global pool. Allows us to
         * avoid allocating new objects in many cases.
         */
        public static Message obtain() {
            synchronized (sPoolSync) {
                if (sPool != null) {
                    Message m = sPool;
                    sPool = m.next;
                    m.next = null;
                    m.flags = 0; // clear in-use flag
                    sPoolSize--;
                    return m;
                }
            }
            return new Message();
        }
    
    

    通过查看调用链,我们能够看到在MQ中enqueueMessage调用了recycle(),而recyle中也是通过链表的形式对sPool进行维护。源码简单易懂

    下面来看下sPool是怎么维护的。

    在recycleUnchecked()同样也是加了锁的。然后就是用链表的形式维护这个池子,size++

        public void recycle() {
            if (isInUse()) {
                if (gCheckRecycle) {
                    ...
                }
                return;
            }
            recycleUnchecked();
        }
    
        /**
         * Recycles a Message that may be in-use.
         * Used internally by the MessageQueue and Looper when disposing of queued Messages.
         */
        void recycleUnchecked() {
        ...
         synchronized (sPoolSync) {
                if (sPoolSize < MAX_POOL_SIZE) {
                    next = sPool;
                    sPool = this;
                    sPoolSize++;
                }
            }
    
        }
    
    

    Handler

    Handler类的源码总共不超过1000行,并且大部分都是注释,所以我们看该类源码的时候,更多的是看他的注释。静下心来看源码

    * 构造方法
    * callback对象
    * dispatchMessage
    
    

    Handler发送消息(send/post)

    Handler发送消息的方式分为两种:

    1.post
    2.send

    不论是post还是send(其他方法)方式,最终都会调用到sendMessageAtTime/sendMessageAtFrontOfQueue。执行equeueMessage,最终调用MQ#enqueueMessage(),加入到MQ中。

    1. post方式

    以post方式发送消息,参数基本上都是Runnable(Runnable到底是什么,这个要搞懂)。post方式发送的的消息,都会调用getPostMessage(),将runnable封装到Message的callbak中,调用send的相关方法发送出去。

    ps:个人简单、误导性的科普Runnable,就是封装了一段代码,哪个线程执行这个runnable,就是那个线程。

    2. send方式

    以send方式发送消息,在众多的重载方法中,有一类比较容易引起歧义的方法,sendEmptyMessageXxx(),这类方法并不是说没有用到message,只是在使用的时候不需要传递,方法内部帮我们包装了一个Message。另一个需要关注的点是: xxxDelayed() xxxAtTime()

    1.xxxDelayed()

    借助xx翻译,得知 delayed:延迟的,定时的,推迟 的意思,也就是说,借助这个方法我们能做到将消息延迟发送。e.g:延迟三秒让View消失。ps:在我年幼无知的时候,总是搞懵这个方法,不会用。

    在这个方法的参数中,我们看到如果传入的是毫秒值,那么会在delayMillis的基础上与SystemClock.uptimeMillis()做个加法。然后执行sendMessageAtTime()。
    SystemClock.uptimeMillis() 与 System.currentTimeMillis()的区别自己去研究。

        public final boolean sendMessageDelayed(Message msg, long delayMillis)
        {
            if (delayMillis < 0) {
                delayMillis = 0;
            }
            return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
        }
    
    

    2.xxxAtTime()

    在这个方法就更简单易懂了,执行的具体时间需要使用者自己去计算。

    在Handler内的equeueMessage中,第一行的msg.target = this;,将handler自身赋值到msg.target,标记了这个msg从哪来,这个要注意后面会用到

    MQ#enqueueMessage()

    这个方法那是相当的关键

    在此之前,我们一直鼓捣一个参数delayMillis/uptimeMillis,在这个方法里参数名变为了when,标明这个message何时执行,也是MQ对Message排序存储的依据。MQ是按照when的时间排序的,并且第一个Message最先执行。

    在省去了众多目前不关心的代码后,加上仅存的一点数据结构的知识,得到msg在MQ中的存储形式。
    mMessages位于队列第一位置的msg,新加入到msg会跟他比较,然后找到合适的位置加入到队列中。

    ps:记得在一次面试中,面试官问到延迟消息的实现思路,我照着源码说了一下。但是被问到:每次新加入消息,都要循环队列,找到合适的位置插入消息,那么怎么保证执行效率。我不知道他这么问是想考我优化这个东西的思路,还是他觉得我说错了。就犹豫了一下,没有怼回去。

     boolean enqueueMessage(Message msg, long when) {
            ...
            ...
             synchronized (this) {
                ...
                ...
                msg.markInUse();
                msg.when = when;
                Message p = mMessages;
                boolean needWake;
                if (p == null || when == 0 || when < p.when) {
                    msg.next = p;
                    mMessages = msg;
                    needWake = mBlocked;
                } else {
                    needWake = mBlocked && p.target == null && msg.isAsynchronous();
                    Message prev;
                    for (;;) {
                        prev = p;
                        p = p.next;
                        if (p == null || when < p.when) {
                            break;
                        }
                        if (needWake && p.isAsynchronous()) {
                            needWake = false;
                        }
                    }
                    msg.next = p; // invariant: p == prev.next
                    prev.next = msg;
                }
                ...
    
                ...
            }
            return true;
        }
    
    

    以上几步,我们只是将要执行的msg加入到了队列中。接下来分析下什么时候执行msg。

    再接再厉,马上就看到暑光了。

    Looper#prepareMainLooper()

    借助十几年英语学习积累下来的词汇量,加上我出色的看源码能力。看懂了这个方法的注释及Android系统在哪里执行了此方法。

    面试被问到怎么在子线程创建Looper?

    仔细看注释。Initialize the current thread as a looper....See also: {@link #prepare()}

    这个方法,作为开发人员不需要调用它,但是作为一个高级技工还是要多少了解一点的,系统在三个位置调用了此方法,但是我只关心了AndroidThread这个类,AndroidThread是个啥,自己去看吧。

        /**
         * Initialize the current thread as a looper, marking it as an
         * application's main looper. The main looper for your application
         * is created by the Android environment, so you should never need
         * to call this function yourself.  See also: {@link #prepare()}
         */
        public static void prepareMainLooper() {
            prepare(false);
            synchronized (Looper.class) {
                if (sMainLooper != null) {
                    throw new IllegalStateException("The main Looper has already been prepared.");
                }
                sMainLooper = myLooper();
            }
        }
    
    

    Looper#prepare()

    面试的时候经常被问到一个线程可以有多个looper吗?

    看源码注释就得到了答案。
    throw new RuntimeException("Only one Looper may be created per thread");
    怎么保证每个线程只有一个looper呢?这里用到了ThreadLocal。

    在自己创建的子线程中,如果想创建Looper,那么只需要调用Looper.prepare(),就会为当前线程创建一个looper了。

        private static void prepare(boolean quitAllowed) {  
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            sThreadLocal.set(new Looper(quitAllowed));
        }
    
    

    ThreadLocal机制

    ThreadLocal是个什么东西呢,他是个复杂的机制,毕竟从JAVA1.2就加入了机制,保证了每个线程自己的变量....

    本人简单的、带有误导性的科普是:

    类似一个Map,key是当前线程id,value就是你要保存的值

    一定要自己深入了解该机制

    Looper#loop()

    这个方法也很关键,消息能够执行,起了很大作用。虽然个人感觉能看的代码很少,但是都很精炼啊。

    1. 获取looper,得到MQ
    2. 循环MQ得到可执行的msg
    3. 通过msg自身,去到他该去的地方msg.target.dispatchMessage(msg);
    4. recycleUnchecked(),维护Message池

    ps:曾经年少的我一度认为Looper就是主线程,完全因为这个loop()方法,当时看到在AndroidThread#main()中执行了Looper.loop(),而学过JAVA的都知道main()里面,如果没有耗时、子线程等其他操作,基本上执行到最后一行,就结束了。

    但是为什么APP起来了,main()里面那么几行代码执行结束后,没有死掉呢。就是因为loop()里面有个for(;;),当MQ中没有msg,那么会一直循环下去。

    现在想来,还是太年轻了。这个只是一方面原因,其他线程也会调用Looper.prepare(),为自己创建looper,然后执行Looper.loop(),循环自己的MQ。

    发现还是要多了解,多学习。

    MQ#next()

    这个方法负责把队列中的msg取出来,给到looper去执行。

    这个方法也是一个for(;;),当取到第一个msg的时候,如果没有到他该执行的时间,那么就等着,一直等,死等。得到可以执行的msg后,给到Looper。里面还有些native的方法,大家自己去看next()源码吧。

    Handler#dispatchMessage()

    在Looper#loop()中MQ#next()得到了msg,有这么一行msg.target.dispatchMessage(msg);,在之前讲到了这个target是发送msg的那个handler(多个handler的情况下区分)。根据不同情况,对msg进行分发。如果有callback对象(post方式发送消息,或者new Handler(runnable)),就去执行Runnable.run()。其他情况回调到handleMessage(),在创建handler的地方处理这个msg。

     /**
         * Handle system messages here.
         */
        public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
        }
    
    

    以上就是本人对Handler的总结。

    写了这么多,已经累瘫在办公桌前,啥都不想干了。这可能是在高考语文结束后,想的最多的一次文字。

    最后啰嗦一句
    RTFSC(Read The Fucking Source Code)

    为什么android设计只能UI线程更新UI

    1. 解决多线程并发的问题。多个线程更新UI可能发生并发问题,如果在多个线程中加锁,会导致程序页面有可能非常卡顿
    2. 提高界面更新的性能问题
    3. 架构设计的简单,因为android中封装了所有更新UI的操作,在开发中只需要在非UI中发送一个消息,就可以更新UI,对于开发人员来说节省了不少时间.

    相关面试题

    1. 子线程Looper和Handler

    2. 延迟消息怎么处理

    3. ThreadLocal作用

    4. 自己实现Handler机制

    5. for (;;) 与while(true) 区别

       看了些文章,自己动手试了试,.class文件。一毛钱的区别都没有。
       有人说根据编译器不同会有差别,在我目前的能力认知范围内没差别。
      
      
    6. 同步消息屏障

    Message next() {
            ......
            synchronized (this) {
              // Try to retrieve the next message.  Return if found.
              final long now = SystemClock.uptimeMillis();
                    Message prevMsg = null;
                    Message msg = mMessages;
                    if (msg != null && msg.target == null) {
                        // Stalled by a barrier.  Find the next asynchronous message in the queue.
                        do {
                            prevMsg = msg;
                            msg = msg.next;
                        } while (msg != null && !msg.isAsynchronous());
                    }
               ......
                }
          }
    
    

    在view绘制的时候,post到MQ的消息是不会被执行的,优先执行绘制时候的异步消息。

    7.IdleHandler的实现原理

    Message next() {
            ......
            synchronized (this) {
              // Try to retrieve the next message.  Return if found.
               ......
                // If first time idle, then get the number of idlers to run.
                  // Idle handles only run if the queue is empty or if the first message
                    // in the queue (possibly a barrier) is due to be handled in the future.
                    if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
                        pendingIdleHandlerCount = mIdleHandlers.size();
                    }
                    if (pendingIdleHandlerCount <= 0) {
                        // No idle handlers to run.  Loop and wait some more.
                        mBlocked = true;
                        continue;
                    }
    
                    if (mPendingIdleHandlers == null) {
                        mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                    }
                    mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
                }
                // Run the idle handlers.
                // We only ever reach this code block during the first iteration.
                for (int i = 0; i < pendingIdleHandlerCount; i++) {
                    final IdleHandler idler = mPendingIdleHandlers[i];
                    mPendingIdleHandlers[i] = null; // release the reference to the handler
    
                    boolean keep = false;
                    try {
                        keep = idler.queueIdle();
                    } catch (Throwable t) {
                        Log.wtf(TAG, "IdleHandler threw exception", t);
                    }
    
                    if (!keep) {
                        synchronized (this) {
                            mIdleHandlers.remove(idler);
                        }
                    }
              // Reset the idle handler count to 0 so we do not run them again.
                pendingIdleHandlerCount = 0;
    
                // While calling an idle handler, a new message could have been delivered
                // so go back and look again for a pending message without waiting.
                nextPollTimeoutMillis = 0;
                }
          }
    

    面试大厂复习路线

    多余的话就不讲了,接下来将分享面试的一个复习路线,如果你也在准备面试但是不知道怎么高效复习,可以参考一下我的复习路线,有任何问题也欢迎一起互相交流,加油吧!

    这里给大家提供一个方向,进行体系化的学习:

    1、看视频进行系统学习

    前几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。

    另外,我自己也珍藏了好几套视频,有需要的我也可以分享给你。

    2、进行系统梳理知识,提升储备

    客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

    系统学习方向:

    • 架构师筑基必备技能:深入Java泛型+注解深入浅出+并发编程+数据传输与序列化+Java虚拟机原理+反射与类加载+动态代理+高效IO

    • Android高级UI与FrameWork源码:高级UI晋升+Framework内核解析+Android组件内核+数据持久化

    • 360°全方面性能调优:设计思想与代码质量优化+程序性能优化+开发效率优化

    • 解读开源框架设计思想:热修复设计+插件化框架解读+组件化框架设计+图片加载框架+网络访问框架设计+RXJava响应式编程框架设计+IOC架构设计+Android架构组件Jetpack

    • NDK模块开发:NDK基础知识体系+底层图片处理+音视频开发

    • 微信小程序:小程序介绍+UI开发+API操作+微信对接

    • Hybrid 开发与Flutter:Html5项目实战+Flutter进阶

    知识梳理完之后,就需要进行查漏补缺,所以针对这些知识点,我手头上也准备了不少的电子书和笔记,这些笔记将各个知识点进行了完美的总结。

    3、读源码,看实战笔记,学习大神思路

    “编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。

    主要内含微信 MMKV 源码、AsyncTask 源码、Volley 源码、Retrofit源码、OkHttp 源码等等。

    4、面试前夕,刷题冲刺

    面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

    关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

    总结

    改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

    以上内容均免费分享给大家,需要完整版的朋友,点这里可以看到全部内容。或者关注主页扫描加 微信 获取。

    相关文章

      网友评论

        本文标题:Android面试刨根问底之常用源码篇(一):Android优化

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