面霸养成记;50万字Android面试文档致敬2022摸爬滚打的一年

4,885 阅读31分钟

前言

"抗寒",2022专有词....任正非大佬都说了要把寒气传给每一个人,确实22年过得很艰辛,摸爬滚打,总算是安定下来了。

回头想想,抽空吧面试过程中被问到的问题和平时看的文档整理分享出来,希望能给面试的小伙伴一点借鉴。

一共50W字的文档,面试专题12W字只是一小部分,字数限制,分几篇更。

关注公众号:Android苦做舟

提前解锁 《整套50W字Android体系PDF》,让学习更贴近未来实战。

总共囊括

1.腾讯Android开发笔记(33W字)

2.2022最新Android十一位大厂面试专题(12W字)

3.音视频经典面试题(6W字)

4.Jetpack全家桶

5.Android 性能监控框架Matrix

6.JVM

7.车载应用开发

共十一模块,今天来更新第4.5专题京东和字节篇

四丶京东篇

1.service的意义

Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如:onCreate,onStart 这些函数在被系统调用的时候都是在主进程的 main 线程上运行的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。因此请不要把 Service 理解成线程,它跟线程半毛钱的关系都没有!

既然这样,那么我们为什么要用 Service 呢?其实这跟 android 的系统机制有关,我们先拿 Thread 来说。Thread 的运行是独立于 Activity 的,也就是说当一个 Activity 被 finish 之后,如果你没有主动停止 Thread 或者 Thread 里的 run 方法没有执行完毕的话,Thread 也会一直执行。因此这里会出现一个问题:当 Activity 被 finish 之后,你不再持有该 Thread 的引用。另一方面,你没有办法在不同的 Activity 中对同一 Thread 进行控制。  

举个例子:如果你的 Thread 需要不停地隔一段时间就要连接服务器做某种同步的话,该 Thread 需要在 Activity 没有start的时候也在运行。这个时候当你 start 一个 Activity 就没有办法在该 Activity 里面控制之前创建的 Thread。因此你便需要创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题(因为任何 Activity 都可以控制同一 Service,而系统也只会创建一个对应 Service 的实例)。   因此你可以把 Service 想象成一种消息服务,而你可以在任何有 Context 的地方调用 Context.startService、Context.stopService、Context.bindService,Context.unbindService,来控制它,你也可以在 Service 里注册 BroadcastReceiver,在其他地方通过发送 broadcast 来控制它,当然这些都是 Thread 做不到的。

2.Handler内存泄漏相关

(第二章第3题)


3.OkHttp源码相关

(第一章第1题)

4.手写Singleton

单例模式分为懒汉式饿汉式

特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

①懒汉式单例

public class Singleton {
    private Singleton() {}
    private static Singleton single;
    //静态工厂方法 
    public static Singleton duxi() {
         if (single == null) {  
             single = new Singleton();
         }  
        return single;
    }
}

②饿汉式单例

public class Singleton {
    private Singleton() {}
    private static final Singleton single = new Singleton();
    //静态工厂方法 
    public static Singleton duxi() {
        return single;
    }
}

五丶字节篇

1.弱引用与软引用

  • 软引用
    • 只有在内存不足时,JVM才会回收该对象。
    • 当内存不足时,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用包裹的对象给干掉。
  • 弱引用
    • 不管内存是否足够,只要发生GC,弱引用就会被回收。

2.单例常见的实现方式

①单例模式

单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

②单例模式的要求

  • 构造方法必须私有化(确保只有自己能创建)
  • 以静态方法返回实例(外界不能通过new来获取到对象)
  • 确保对象实例只有一个(只对类进行一次实例化,以后都直接获取第一次实例化的对象)

③单例模式的实现

1、懒汉式(线程不安全)

描述:这种方式是最基本的实现方式,但是不支持多线程。因为没有加锁,在多线程不能正常工作

//懒汉式(线程不安全)
public class Singleton {  
    private static Singleton instance;  

    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

2、懒汉式(线程安全)

描述:能够在多线程下正常工作,但是,效率极低

//懒汉式(线程安全)
public class Singleton {  
    private static Singleton instance;  

    private Singleton (){}  

    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

3、双重校验锁(DCL,即double-checked locking)(线程安全)

描述:对懒汉式(线程安全)的优化,采用双锁的机制,安全且在多线程情况下能保持高性能

//双重校验锁
public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4、饿汉式(线程安全)

描述:这种方式比较常用,但容易产生垃圾对象

//饿汉式
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
        return instance;
    }
}

5、静态内部类(线程安全)

描述:这种方式达到跟双重校验锁一样的效果,这种方式只适用于静态域的情况,双重校验锁可在实例域需要延迟初始化时使用

//静态内部类
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {

    }

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

6、枚举(线程安全)

描述:这种方式还没有被广泛采用,但是这种实现是单例模式的最佳方法。更简洁、自动支持序列化机制、绝对防止多次实例化

//枚举
public enum Singleton {
    INSTANCE;

    public void whateverMethod() {

    }
}

④总结

一般情况下,不建议使用第1种和第2种懒汉方式,建议使用第4种饿汉方式。只有在明确实现lazy loading时,才会使用第5种静态内部类方式。如果涉及到反序列化创建对象时,可以使用第6种枚举方式。如果有其他需求,可以考虑使用第3种双重校验锁方式。

3.ReentrantLock如何实现公平锁的

我先解释一下个公平和非公平的概念。

公平,指的是竞争锁资源的线程,严格按照请求顺序来分配锁。

非公平,表示竞争锁资源的线程,允许插队来抢占锁资源。

ReentrantLock默认采用了非公平锁的策略来实现锁的竞争逻辑。

其次,ReentrantLock内部使用了AQS来实现锁资源的竞争,

没有竞争到锁资源的线程,会加入到AQS的同步队列里面,这个队列是一个FIFO的双向链表。

image.png

在这样的一个背景下,公平锁的实现方式就是,线程在竞争锁资源的时候判断AQS同步队列里面有没有等待的线程。

如果有,就加入到队列的尾部等待。

而非公平锁的实现方式,就是不管队列里面有没有线程等待,它都会先去尝试抢占锁资源,如果抢不到,再加入到

AQS同步队列等待。

ReentrantLock和Synchronized默认都是非公平锁的策略,之所以要这么设计,我认为还是考虑到了性能这个方面的原因。

因为一个竞争锁的线程如果按照公平的策略去阻塞等待,同时AQS再把等待队列里面的线程唤醒,这里会涉及到内核态的切换,对性能的影响比较大。

如果是非公平策略,当前线程正好在上一个线程释放锁的临界点抢占到了锁,就意味着这个线程不需要切换到内核态,

虽然对原本应该要被唤醒的线程不公平,但是提升了锁竞争的性能。

4.线程池里面非核心线程什么时候会被回收

ThreadPoolExecutor回收工作线程,一条线程getTask()返回null,就会被回收。

分两种场景。

1.未调用shutdown() ,RUNNING状态下全部任务执行完成的场景

线程数量大于corePoolSize,线程超时阻塞,超时唤醒后CAS减少工作线程数,如果CAS成功,返回null,线程回收。否则进入下一次循环。当工作者线程数量小于等于corePoolSize,就可以一直阻塞了。

2.调用shutdown() ,全部任务执行完成的场景

shutdown() 会向所有线程发出中断信号,这时有两种可能。

  • 所有线程都在阻塞

中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。

  • 任务还没有完全执行完

至少会有一条线程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里会调用tryTerminate(),向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。

5.handler机制

(第二章第3题)

6.AsyncTask原理,

原理简述:

AsyncTask内部持有一个2~4线程大小的线程池,当执行asynctask的execute(Runnable)方法时会将任务提交到内部的ArrayDeque的任务容器内,并执行SerialExecutor的execute方法,从arraydeque里取出任务并串行执行

当执行execute(params...)方法时会执行executeonexecutor,此时的执行是并行的,任务会被封装成一个WorkerRunnable,WorkerRunnable内部持有一个params数组,并被最终封装成了FutureTask来进行处理,由内部线程池来进行处理,携带的参数会在doinBackground时进行处理中回调,此回调会在子线程回调,处理完成后会通过postResult来进行结果通知。postResult里会获取由创建Asynctask时候传递的handler来处理,不传handler默认在主线程,传了handler则在handler传入的线程进行postResult回调。

7.binder机制

(第二章第5题)

8.ANR处理方法

崩溃率是不是就能完全等价于应用的稳定性呢?答案是肯定不行。处理了崩溃,我们还会经常遇到 ANR(Application Not Responding,程序没有响应)这个问题。

出现 ANR 的时候,系统还会弹出对话框打断用户的操作,这是用户非常不能忍受的。

ANR 处理方法: 使用 FileObserver 监听 /data/anr/traces.txt 的变化。非常不幸的是,很多高版本的 ROM,已经没有读取这个文件的权限了。这个时候你可能只能思考其他路径,海外可以使用 Google Play 服务,而国内微信利用 Hardcoder 框架(HC 框架是一套独立于安卓系统实现的通信框架,它让 App 和厂商 ROM 能够实时 “对话” 了,目标就是充分调度系统资源来提升 App 的运行速度和画质,切实提高大家的手机使用体验)向厂商获取了更大的权限。也可以将手机 ROOT 掉,然后取得 traces.txt 文件。

9.RxJava 使用详解

RxJava是一种异步数据处理库,也是一种扩展的观察者模式。对于Android开发者来说,使用RxJava时也会搭配RxAndroid,它是RxJava针对Android平台的一个扩展,用于Android 开发,它提供了响应式扩展组件,使用RxAndroid的调度器可以解决Android多线程问题。

①观察者模式

四大要素:Observable(被观察者),Observer (观察者),subscribe (订阅),事件。 观察者订阅被观察者,一旦被观察者发出事件,观察者就可以接收到。

image.png

扩展的观察者模式

image.png

当事件完成时会回调onComplete(),在完成过程中发生了异常会回调onError(),onError()和onComplete()只会回调一个。

引入依赖

    implementation 'io.reactivex.rxjava3:rxjava:3.1.3'
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
        //创建被观察者
        Observable<String> observable = Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Throwable {
                emitter.onNext("Hello Uncle Xing");
                emitter.onComplete();
            }
        });
        //创建观察者
        Observer<String> observer = new Observer<String>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {
                Log.i(tag, "onSubscribe");
            }

            @Override
            public void onNext(@NonNull String s) {
                Log.i(tag, "onNext:" + s);
            }

            @Override
            public void onError(@NonNull Throwable e) {
                Log.i(tag, "onError:" + e.getMessage());
            }

            @Override
            public void onComplete() {
                Log.i(tag, "onComplete");
            }
        };
        //订阅事件
        observable.subscribe(observer);

② 操作符

创建Observable

  • create:用于创建Observable
        Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Throwable {
                emitter.onNext("Hello Uncle Xing");
                emitter.onComplete();
            }
        }).subscribe(new Observer<String>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {
                Log.i(tag, "onSubscribe");
            }

            @Override
            public void onNext(@NonNull String s) {
                Log.i(tag, "onNext:" + s);
            }

            @Override
            public void onError(@NonNull Throwable e) {
                Log.i(tag, "onError:" + e.getMessage());
            }

            @Override
            public void onComplete() {
                Log.i(tag, "onComplete");
            }
        });
  • just:创建一个Observable并自动调用onNext发射数据,just中传递的参数将直接在Observer的onNext方法中接收到
        Observable.just("Uncle Xing").subscribe(new Observer<String>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {
                Log.i(tag, "onSubscribe");
            }

            @Override
            public void onNext(@NonNull String s) {
                Log.i(tag, "onNext:" + s);
            }

            @Override
            public void onError(@NonNull Throwable e) {
                Log.i(tag, "onError:" + e.getMessage());
            }

            @Override
            public void onComplete() {
                Log.i(tag, "onComplete");
            }
        });
  • interval:创建一个按固定时间间隔发射整数序列的Observable,可用作定时器。
        Observable.interval(1000, TimeUnit.MILLISECONDS).subscribe(new Observer<Long>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {
            }

            @Override
            public void onNext(@NonNull Long aLong) {
                Log.i(tag, "count:" + aLong); //这里是非主线程,会隔1s打印出0,1,2,3....
            }

            @Override
            public void onError(@NonNull Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
  • timer:创建一个Observable,它在一个特定延迟后发射一个值
        Observable.timer(1000, TimeUnit.MILLISECONDS).subscribe(new Observer<Long>() {
            @Override
            public void onSubscribe(Disposable d) {

            }

            @Override
            public void onNext(Long aLong) {
                Log.i(tag, "count:" + aLong);
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });

转换Observable

  • map:对数据进行变换后,可以返回任意值,对数据的变换是1对1进行的。
        Observable.just(666).map(new Function<Integer, String>() {
            @Override
            public String apply(Integer integer) throws Throwable {
                return integer.toString();
            }
        }).subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Throwable {
                Log.i(tag, "map:" + s);
            }
        });
  • flatMap:对数据变换后,返回ObservableSource对象,可以对数据进行一对多,多对多的变换。
        Observable.just(1, 2, 3, 4, 5).flatMap(new Function<Integer, ObservableSource<String>>() {
            @Override
            public ObservableSource<String> apply(Integer integer) throws Throwable {
                return Observable.just(integer.toString());
            }
        }).subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Throwable {
                Log.i(tag, "accept:" + s);
            }
        });
  • buffer:把Observable的数据放进一个数据包裹,然后发射这些数据包裹,而不是一次发射一个值
        Observable.just(1, 2, 3, 4, 5, 6).buffer(3).subscribe(new Consumer<List<Integer>>() {
            @Override
            public void accept(List<Integer> integers) throws Throwable {
                Log.i(tag, integers.toString());
            }
        });

Log会分两次打印,第一次打印 [1, 2, 3],第二次打印 [4, 5, 6]

过滤Observable

  • distinct:去掉重复数据
        Observable.just(1, 2, 3, 4, 2, 3).distinct().subscribe(new Observer<Integer>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {

            }

            @Override
            public void onNext(@NonNull Integer integer) {
                Log.i(tag, "distinct:" + integer);
            }
 
            @Override
            public void onError(@NonNull Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
  • elementAt:取出指定位置的数据
        Observable.just(1, 2, 3, 4).elementAt(1).subscribe(new MaybeObserver<Integer>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {

            }

            @Override
            public void onSuccess(@NonNull Integer integer) {
                Log.i(tag, "onSuccess:" + integer);
            }

            @Override
            public void onError(@NonNull Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
  • filter:对数据进行指定规则的过滤
        Observable.just(1, 2, 3, 4).filter(new Predicate<Integer>() {
            @Override
            public boolean test(Integer integer) throws Throwable {
                return integer > 1;
            }
        }).subscribe(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) throws Throwable {
                Log.i(tag, "filter:" + integer);
            }
        });

组合Observable

  • zip:通过一个函数将多个Observable的发射物结合到一起,基于这个函数的结果为每个结合体发射单个数据项
        Observable<Integer> observable = Observable.just(10, 20, 30, 40);
        Observable<Integer> observable2 = Observable.just(1, 2, 3);
        Observable.zip(observable, observable2, new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) throws Throwable {
                return integer + integer2;
            }
        }).subscribe(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) throws Throwable {
                Log.i(tag, "zip:" + integer);
            }
        });

注意:当其中一个Observable发送数据结束或异常,另外一个也停止发送,所以这里只会打印出11,22,33

  • merge:合并多个Observable的发射物
        Observable<Integer> observable = Observable.just(10, 20, 30, 40);
        Observable<Integer> observable2 = Observable.just(1, 2, 3);
        Observable.merge(observable, observable2).subscribe(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) throws Throwable {
                Log.i(tag, "merge:" + integer);//会打印出10,20,30,1,2,3
            }
        });

错误处理 1.onErrorReturn:让Observable遇到错误时发射一个特殊的项并且正常终止 2.onErrorResumeNext:让Observable在遇到错误时开始发射第二个Observable的数据序列

Schedulers(调度器):解决多线程问题 1.io():用于I/O操作; 2.computation():计算工作默认的调度器; 3.immediate():立即执行,允许立即在当前线程执行你指定的工作; 4.newThread():创建新线程; 5.trampoline():顺序处理,按需处理队列,并运行队列的每一个任务。

AndroidSchedulers:RxAndroid提供在Android平台的调度器,指定观察者在主线程。

SubscribeOn用于每个Observable对象,ObserveOn用于每个Observer对象

        Observable.create(new ObservableOnSubscribe<Integer>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<Integer> emitter) throws Throwable {
                emitter.onNext(100);
                emitter.onComplete();
                Log.i(tag, "subscribe thread:" + Thread.currentThread().getName());//打印subscribe thread:RxNewThreadScheduler-1
            }
        }).subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull Integer integer) {
                        Log.i(tag, "onNext thread:" + Thread.currentThread().getName());//打印onNext thread:main
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });

③管理RxJava的生命周期

在使用RxJava的时候,如果没有及时解除订阅,在退出Activity的时候,异步线程还在执行,对Activity的引用还在,此时就会产生内存泄露问题。

可使用RxLifecycle

引入依赖

    implementation 'com.trello.rxlifecycle4:rxlifecycle:4.0.2'
    implementation 'com.trello.rxlifecycle4:rxlifecycle-components:4.0.2'

让你的Activity继承RxAppCompatActivity,Fragment继承RxFragment,其余类似,然后使用bindUntilEvent或者bindToLifecycle

        Observable.interval(1000, TimeUnit.MILLISECONDS)
                .compose(bindUntilEvent(ActivityEvent.DESTROY)) //当前Activity执行到onDestroy时,Observable取消订阅
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Throwable {
                        Log.i(tag, "accept:" + aLong);
                    }
                });
        Observable.interval(1000, TimeUnit.MILLISECONDS)
                .compose(bindToLifecycle())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Throwable {
                        Log.i(tag, "accept:" + aLong);
                    }
                });

使用bindToLifecycle: 如果Observable在onCreate执行,那么当执行到onDestroy时取消订阅。 如果Observable在onStart执行,那么当执行到onStop时取消订阅。 如果Observable在onResume执行,那么当执行到onPause时取消订阅。

④RxJava + Retrofit完成网络请求

public interface MyService {
    @GET("gallery/{imageType}/response")
    Observable<List<String>> getImages(@Path("imageType") String imageType);
}
        Retrofit retrofit = new Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .baseUrl(BASE_URL)
                .build();
        MyService service = retrofit.create(MyService.class);

        service.getImages("banner")
                .compose(bindToLifecycle())
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<String>>() {
                    @Override
                    public void accept(List<String> strings) throws Throwable {
                        //todo
                    }
                });

10.OKHTTP拦截器

(第九章第4题)

11.Activity的启动流程

(第六章第1题)

12.OKhttp,Retrofit

(第一章第1题)

13.布局优化,内存优化,崩溃优化

①布局优化

经过上面的一顿操作,发现占时间大块的少不了setContentView。说明布局渲染视图还是挺费时的。

减少层级

自定义Viewmeasure、layout、draw这三个过程,都需要对整个视图树自顶向下的遍历,而且很多情况都会多次触发整个遍历过程(Linearlayout 的 weight等),所以如果层级太深,就会让整个绘制过程变慢,从而造成启动速度慢、卡顿等问题。 而onDraw在频繁刷新时可能多次触发,因此 onDraw更不能做耗时操作,同时需要注意内存抖动。对于布局性能的检测,依然可以使用systrace与traceview按 照绘制流程检查绘制耗时函数。

工具Layout Inspector DecorView开始。content往下才是自己写的布局。

image.png

重复的布局使用include。 一个布局+到另一个上,如果加上以后,有两一样的ViewGroup,可以把被加的顶层控件的ViewGroup换成merge ViewStub:失败提示框等。需要展示的时候才创建,放在那不占位置。

过度渲染

一块区域内(一个像素),如果被绘制了好几次,就是过度渲染。 过度绘制不可避免,但是应该尽量的减少。 手机->开发者模式->GPU 过度绘制 打开以后能看到不同颜色的块,越红就说明过度绘制的越严重。对于严重的地方得减少过度绘制。

1.移除布局中不需要的背景。 2.降低透明度。 3.使视图层次结构扁平化。

布局加载优化

异步加载布局,视情况而用。

new AsyncLayoutInflater(this)
.inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
//......
}
});

②崩溃优化

前言

开发人员碰到 APP 崩溃(闪退)什么办?不少人会说根据 Log,找到闪退的代码,捕获异常,“消化” 掉了所有 Java 崩溃。至于程序是否会出现其他异常表现,那是上帝要管的事情。是的,这种方法对于紧急情况下不失为一种解决办法,但闪退的真相是什么?是否从根源上解决问题呢?

崩溃

崩溃率是衡量一个应用质量高低的基本指标,那么,该怎样客观地衡量崩溃这个指标,以及又该如何看待和崩溃相关的稳定性。

Android 的两种崩溃:

  • Java 崩溃
  • Native 崩溃

简单来说,Java 崩溃就是在 Java 代码中,出现了未捕获异常,导致程序异常退出。那 Native 崩溃一般都是因为在 Native 代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动 Abort,这些都会产生相应的 Signal 信号,导致程序异常退出。

崩溃的收集

“崩溃” 就是程序出现异常,而一个产品的崩溃率,跟我们如何捕获、处理这些异常有比较大的关系。对于很多中小型公司来说,可以选择一些第三方的服务。目前各种平台也是百花齐放,包括阿里的友盟、腾讯的 Bugly、网易云捕、Google 的 Firebase 等等。要懂得借力!

ANR

崩溃率是不是就能完全等价于应用的稳定性呢?答案是肯定不行。处理了崩溃,我们还会经常遇到 ANR(Application Not Responding,程序没有响应)这个问题。

出现 ANR 的时候,系统还会弹出对话框打断用户的操作,这是用户非常不能忍受的。

ANR 处理方法: 使用 FileObserver 监听 /data/anr/traces.txt 的变化。非常不幸的是,很多高版本的 ROM,已经没有读取这个文件的权限了。这个时候你可能只能思考其他路径,海外可以使用 Google Play 服务,而国内微信利用 Hardcoder 框架(HC 框架是一套独立于安卓系统实现的通信框架,它让 App 和厂商 ROM 能够实时 “对话” 了,目标就是充分调度系统资源来提升 App 的运行速度和画质,切实提高大家的手机使用体验)向厂商获取了更大的权限。也可以将手机 ROOT 掉,然后取得 traces.txt 文件。

应用退出

除了常见的崩溃,还有一些会导致应用异常退出的情况,例如:

  • 主动自杀。Process.killProcess ()、exit () 等
  • 崩溃。出现了 Java 或 Native 崩溃
  • 系统重启。系统出现异常、断电、用户主动重启等,我们可以通过比较应用开机运行时间是否比之前记录的值更小
  • 被系统杀死。被 low memory killer 杀掉、从系统的任务管理器中划掉等
  • ANR

我们可以在应用启动的时候设定一个标志,在主动自杀或崩溃后更新标志,这样下次启动时通过检测这个标志就能确认运行期间是否发生过异常退出。对应上面的五种退出场景,我们排除掉主动自杀和崩溃(崩溃会单独的统计)这两种场景,希望可以监控到剩下三种的异常退出,理论上这个异常捕获机制是可以达到 100% 覆盖的。

通过这个异常退出的检测,可以反映如 ANR、low memory killer、系统强杀、死机、断电等其他无法正常捕获到的问题。当然异常率会存在一些误报,比如用户从系统的任务管理器中划掉应用。对于线上的大数据来说,还是可以帮助我们发现代码中的一些隐藏问题。

根据应用的前后台状态,我们可以把异常退出分为前台异常退出和后台异常退出。“被系统杀死” 是后台异常退出的主要原因,当然我们会更关注前台的异常退出的情况,这会跟 ANR、OOM 等异常情况有更大的关联。

崩溃处理

我们每天工作也会遇到各种各样的疑难问题,“崩溃” 就是其中比较常见的一种问题。解决问题跟破案一样需要经验,我们分析的问题越多越熟练,定位问题就会越快越准。当然这里也有很多套路,比如对于 “案发现场” 我们应该留意哪些信息?怎样找到更多的 “证人” 和 “线索” ? “侦查案件” 的一般流程是什么?对不同类型的 “案件” 分别应该使用什么样的调查方式?

要相信 “真相永远只有一个”,崩溃也并不可怕。

崩溃现场

崩溃现场是我们的 “第一案发现场”,它保留着很多有价值的线索。现在可以挖掘到的信息越多,下一步分析的方向就越清晰,而不是去靠盲目猜测。

崩溃信息

从崩溃的基本信息,我们可以对崩溃有初步的判断。进程名、线程名。崩溃的进程是前台进程还是后台进程,崩溃是不是发生在 UI 线程。

崩溃堆栈和类型。崩溃是属于 Java 崩溃、Native 崩溃,还是 ANR,对于不同类型的崩溃关注的点也不太一样。特别需要看崩溃堆栈的栈顶,看具体崩溃在系统的代码,还是 APP 代码里面。

关键字:FATAL

 FATAL EXCEPTION: main
 Process: com.cchip.csmart, PID: 27456
 java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(int)' on a null object reference
    at com.cchip.alicsmart.activity.SplashActivity$1.handleMessage(SplashActivity.java:67)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:179)
    at android.app.ActivityThread.main(ActivityThread.java:5672)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:674)

系统信息

系统的信息有时候会带有一些关键的线索,对我们解决问题有非常大的帮助。

Logcat。这里包括应用、系统的运行日志。由于系统权限问题,获取到的 Logcat 可能只包含与当前 APP 相关的。其中系统的 event logcat 会记录 APP 运行的一些基本情况,记录在文件 /system/etc/event-log-tags 中。

//system logcat:
10-25 17:13:47.788 21430 21430 D dalvikvm: Trying to load lib ... 

//event logcat:
10-25 17:13:47.788 21430 21430 I am_on_resume_called: 生命周期
10-25 17:13:47.788 21430 21430 I am_low_memory: 系统内存不足
10-25 17:13:47.788 21430 21430 I am_destroy_activity: 销毁 Activty
10-25 17:13:47.888 21430 21430 I am_anr: ANR 以及原因
10-25 17:13:47.888 21430 21430 I am_kill: APP 被杀以及原因

机型、系统、厂商、CPU、ABI、Linux 版本等。通过采集多达几十个维度,这对寻找共性问题会很有帮助。

内存信息

OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系。如果把用户的手机内存分为 “2GB 以下” 和 “2GB 以上” 两个区,就会发现 “2GB 以下” 用户的崩溃率是 “2GB 以上” 用户的几倍。

系统剩余内存。关于系统内存状态,可以直接读取文件 /proc/meminfo。当系统可用内存很小(低于 MemTotal 的 10%)时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现。

应用使用内存。包括 Java 内存、RSS(Resident Set Size)、PSS(Proportional Set Size),我们可以得出应用本身内存的占用大小和分布。PSS 和 RSS 通过 /proc/self/smap 计算,可以进一步得到例如 apk、dex、so 等更加详细的分类统计。

虚拟内存。虚拟内存可以通过 /proc/self/status 得到,通过 /proc/self/maps 文件可以得到具体的分布情况。有时候我们一般不太重视虚拟内存,但是很多类似 OOM、tgkill 等问题都是虚拟内存不足导致的。

Name:     com.xmamiga.name   // 进程名
FDSize:   800               // 当前进程申请的文件句柄个数
VmPeak:   3004628 kB        // 当前进程的虚拟内存峰值大小
VmSize:   2997032 kB        // 当前进程的虚拟内存大小
Threads:  600               // 当前进程包含的线程个数

一般来说,对于 32 位进程,如果是 32 位的 CPU,虚拟内存达到 3GB 就可能会引起内存申请失败的问题。如果是 64 位的 CPU,虚拟内存一般在 3~4GB 之间。当然如果我们支持 64 位进程,虚拟内存就不会成为问题。Google Play 要求 2019 年 8 月一定要支持 64 位,在国内虽然支持 64 位的设备已经在 90% 以上了,但是商店都不支持区分 CPU 架构类型发布,普及起来需要更长的时间。

资源信息

有的时候会发现应用堆内存和设备内存都非常充足,还是会出现内存分配失败的情况,这跟资源泄漏可能有比较大的关系。

文件句柄 fd。文件句柄的限制可以通过 /proc/self/limits 获得,一般单个进程允许打开的最大文件句柄个数为 1024。但是如果文件句柄超过 800 个就比较危险,需要将所有的 fd 以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。

opened files count 812:
0 -> /dev/null
1 -> /dev/log/main4 
2 -> /dev/binder
3 -> /data/data/com.xmamiga.sample/files/test.config
...

线程数。当前线程数大小可以通过上面的 status 文件得到,一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据我的经验来说,如果线程数超过 400 个就比较危险。需要将所有的线程 id 以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题。

threads count 412:
 1820 com.xmamiga.crashsdk
 1844 ReferenceQueueD
 1869 FinalizerDaemon
 ...

JNI。使用 JNI 时,如果不注意很容易出现引用失效、引用爆表等一些崩溃。

应用信息

除了系统,其实我们的应用更懂自己,可以留下很多相关的信息。崩溃场景。崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中;关键操作路径,不同于开发过程详细的打点日志,我们可以记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助。其他自定义信息。不同的应用关心的重点可能不太一样。

崩溃分析

有了这么多现场信息之后,就可以开始真正的 “破案” 之旅了。绝大部分的 “案件” 只要肯花功夫,最后都能真相大白。不要畏惧问题,经过耐心和细心地分析,总能敏锐地发现一些异常或关键点,并且还要敢于怀疑和验证。

第一步:确定重点

确认和分析重点,关键在于终过日志中找到重要的信息,对问题有一个大致判断。一般来说,我建议在确定重点这一步可以关注以下几点。

  • 确认严重程度。解决崩溃也要看性价比,我们优先解决 Top 崩溃或者对业务有重大影响,例如主要功能的崩溃。不要花几天去解决了一个边角的崩溃,有可能下个版本就把功能删除了。
  • 崩溃基本信息。确定崩溃的类型以及异常描述,对崩溃有大致的判断。 一般来说,大部分的简单崩溃经过这一步已经可以得到结论。

Java 崩溃。Java 崩溃类型比较明显,比如 NullPointerException 是空指针,OutOfMemoryError 是资源不足,这个时候需要去进一步查看日志中的 “内存信息” 和 “资源信息”。

Native 崩溃。需要观察 signal、code、fault addr 等内容,以及崩溃时 Java 的堆栈。关于各 signal 含义的介绍,你可以查看崩溃信号介绍。比较常见的是有 SIGSEGV 和 SIGABRT,前者一般是由于空指针、非法指针造成,后者主要因为 ANR 和调用 abort () 退出所导致。

ANR。先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题,或是 CPU 竞争问题,还是由于大量 GC 导致卡死。

第二步:查找共性

如果使用了上面的方法还是不能有效定位问题,我们可以尝试查找这类崩溃有没有什么共性。找到了共性,也就可以进一步找到差异,离解决问题也就更进一步。

机型、系统、ROM、厂商、ABI,这些采集到的系统信息都可以作为维度聚合,共性问题例如是不是只出现在 x86 的手机,是不是只有三星这款机型,是不是只在 Android 8.0 的系统上。应用信息也可以作为维度来聚合,比如正在打开的链接、正在播放的视频、国家、地区等。

找到了共性,可以对你下一步复现问题有更明确的指引。

第三步:尝试复现

如果我们已经大概知道了崩溃的原因,为了进一步确认更多信息,就需要尝试复现崩溃。如果我们对崩溃完全没有头绪,也希望通过用户操作路径来尝试重现,然后再去分析崩溃原因。

“只要能本地复现,我就能解”,相信这是很多开发跟测试说过的话。有这样的底气主要是因为在稳定的复现路径上面,我们可以采用增加日志或使用 Debugger、GDB 等各种各样的手段或工具做进一步分析。

我们可能会遇到了各种各样的奇葩问题。比如某个厂商改了底层实现、新的 Android 系统实现有所更改,都需要去 Google、翻源码,有时候还需要去抠厂商的 ROM 或手动刷 ROM。很多疑难问题需要我们耐得住寂寞,反复猜测、反复发灰度、反复验证。–但这种问题还是要看问题的严重程序,不可捡了芝麻丢了西瓜。

系统崩溃

系统崩溃常常令我们感到非常无助,它可能是某个 Android 版本的 Bug,也可能是某个厂商修改 ROM 导致。这种情况下的崩溃堆栈可能完全没有我们自己的代码,很难直接定位问题。能做的有:

  • 查找可能的原因。通过上面的共性归类,我们先看看是某个系统版本的问题,还是某个厂商特定 ROM 的问题。虽然崩溃日志可能没有我们自己的代码,但通过操作路径和日志,可以找到一些怀疑的点。
  • 尝试规避。查看可疑的代码调用,是否使用了不恰当的 API,是否可以更换其他的实现方式规避。
  • Hook 解决。这里分为 Java Hook 和 Native Hook。它可能只出现在 Android 7.0 的系统中,参考 Android 8.0 的做法,直接 catch 住这个异常。 如果做到了上面说的这些,以上大部分的崩溃应该都能解决或者规避,大部分的系统崩溃也是如此。当然总有一些疑难问题需要依赖到用户的真实环境,这些需要具备类似动态跟踪和调试的能力。

总结

崩溃攻防是一个长期的过程,我们尽可能地提前预防崩溃的发生,将它消灭在萌芽阶段。作为技术人员,我们不应该盲目追求崩溃率这一个数字,应该以用户体验为先,如果强行去掩盖一些问题往往更加适得其反。我们不应该随意使用 try catch 去隐藏真正的问题,要从源头入手,了解崩溃的本质原因,保证后面的运行流程。在解决崩溃的过程,也要做到由点到面,不能只针对这个崩溃去解决,而应该要考虑这一类崩溃怎么解决和预防。

③内存优化

前面讲过

14.kotlin,

15.常用设计模式,

参考前面

16.MVC,MVVM

第三章第一题

17.main方法执行之前发生了什么

public class Main {
    public static void main(String[] args) {
        System.out.println(Main.class.getName());

    }
}

一共50W字的文档,面试专题12W字只是一小部分,字数限制,分几篇更。

关注公众号:Android苦做舟

提前解锁 《整套50W字Android体系PDF》,让学习更贴近未来实战。

总共囊括

1.腾讯Android开发笔记(33W字)

2.2022最新Android十一位大厂面试专题(12W字)

3.音视频经典面试题(6W字)

4.Jetpack全家桶

5.Android 性能监控框架Matrix

6.JVM

7.车载应用开发