阅读 1984

Android面试题

1.原码反码补码

1.正数的原码反码补码相同,都是将数字转换为二进制形式,然后将高位补0。比如说对于8位来说:

  • 10所对应的原码反码补码都是 0000 1010

2.而对于负数,负数的原码是它的绝对值对应的二进制,而最高位是1。所以:

  • 10所对应的原码是 1000 1010

3.负数的反码是它原码最高位除外的其余所有位取反,所以:

  • 10所对应的反码是 1111 0101

4.而负数的补码是将其反码的数值+1,所以:

  • 10所对应的补码是 1111 0110

2.JAVA的8种基本数据类型所占位数


Java基础


1.HashMap实现原理,如果hashCode冲突怎么办,为什么线程不安全,与Hashtable有什么区别

  • 主要通过计算数据的hashCode来插入
  • hashCode相同的元素插入同一个链表,才用数组+链表方式存储(插入到链表时才用头插法,后插入的Entry被查找的可能性更大)
  • 可能会有多个线程同时put数据,若同时push了hashCode相同数据,后面的数据可能会将上一条数据覆盖掉 Hashtable几乎在每个方法上都加上synchronized(同步锁),实现线程安全
1.1 HashMap的初始长度

从Key映射到HashMap数组的对应位置,会用到一个Hash函数:index = HashCode(Key) & (Length - 1).HashMap的初始长度为16或者其他2的幂,Hash算法的结果就是均匀的

2.synchronized 修饰实例方法和修饰静态方法有什么不一样

public synchronized void run()  {
    System.out.println(1);
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    System.out.println(2);
}
复制代码
  • synchronized修饰普通方法时锁对象是this对象,而使用两个对象去访问,不是同一把锁
Demo demo = new Demo();
new Thread(() -> demo.run()).start();
Demo demo2 = new Demo();
new Thread(() -> demo2.run()).start();
复制代码

结果为: 1 1 2 2

不同步。但如果使用同一对象访问,结果才是同步的

Demo demo = new Demo();
new Thread(() -> demo.run()).start();
new Thread(() -> demo.run()).start();
复制代码

输出结果:1 2 1 2

  • 当synchronized修饰静态方法时,锁对象为当前类的字节码文件对象。使用不同的对象访问,结果是同步的,因为当修饰静态方法时,锁对象是class字节码文件对象,而两个对象是同一个class文件,所以使用的是一个锁

3.final 、finally、finalize 区别

  1. final关键字用于基本数据类型前:这时表明该关键字修饰的变量是一个常量,在定义后该变量的值就不能被修改。
    final关键字用于方法声明前:这时意味着该方法时最终方法,只能被调用,不能被覆盖,但是可以被重载。
    final关键字用于类名前:此时该类被称为最终类,该类不能被其他类继承。

  2. 用try{

}catch(){} 捕获异常时,无论室友有异常,finally代码块中代码都会执行。

  1. finalize方法来自于java.lang.Object,用于回收资源。

可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用

4. Java中成静态内部类和非静态内部类特点

  • 静态内部类:和外部类没有什么"强依赖"上的关系,耦合程度不高,可以单独创建实例。由于静态内部类与外部类并不会保存相互之间的引用,因此在一定程度上,还会节省那么一点内存资源
  • 内部类中需要访问有关外部类的所有属性及方法

5.强引用、弱引用、软引用和虚引用

  • 强引用:当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误也不会回收,直接new出来的就是强引用
  • 软引用:内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。\

当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收

  if(JVM内存不足) {
        // 将软引用中的对象引用置为null
        str = null;
        // 通知垃圾回收器进行回收
        System.gc();
   }
复制代码
  • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象
  • 虚引用:虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收

6.原子变量Atomic

还在用Synchronized?Atomic你了解不?

7.多线程并发问题

多线程并发问题

7.1 线程的sleep方法
  • 不会释放锁
    通过synchronized同步块实现锁机制。线程tv1睡眠三秒后,线程tv2里的run()方法代码才执行,因为它获取不到锁发生了阻塞
        var t1: Thread = object : Thread() {
        override fun run() {
              synchronized(any){
                  println("线程一开始睡眠")
                  Thread.sleep(3000)
                  println("线程一睡眠结束")
               }
            }
        }
        var t2: Thread = object : Thread() {
            override fun run() {
               synchronized(any){
                   println("线程2开始睡眠")
                   Thread.sleep(3000)
                   println("线程2睡眠结束")
               }
            }
        }
        t1.start()
        t2.start()
          
        //日志输出
        14:13:24.660 1805-1836/ I/System.out: 线程一开始睡眠
        14:13:27.663 1805-1836/ I/System.out: 线程一睡眠结束
        14:13:29.555 1805-1837/ I/System.out: 线程2开始睡眠
        14:13:32.556 1805-1837/ I/System.out: 线程2睡眠结束
    复制代码
  • 线程在sleep时是可以被中断的
    线程1在sleep时候被2中断
        var t1: Thread = object : Thread() {
            override fun run() {
                println("线程一开始睡眠")
                try {
                    Thread.sleep(30000)
                } catch (e: InterruptedException) {
                    println("===error=== ")
                }
                println("线程一睡眠结束")
            }
        }
        var t2: Thread = object : Thread() {
            override fun run() {
                Thread.sleep(3000)
                t1.interrupt()
            }
        }
        t1.start()
        t2.start()
    复制代码
  • sleep(0)
    • 在线程没退出之前,线程有三个状态,就绪态,运行态,等待态
    • sleep(n)之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,当n秒后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争
    • 所谓的cpu调度,就是根据一定的算法(优先级,FIFO等。。。),从就绪队列中选择一个线程来分配cpu时间。
    • sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争

8.主线程是否可以直接捕获子线程的异常?

try{
    new Thread(){
        public void run(){
            if (...) throw new RuntimeException(); 
        }
    }.start();
}catch(Exception e){
}
复制代码

答案不能。

  • 线程代码不能抛出任何checked异常。所有的线程中的checked异常都只能被线程本身消化掉
  • 子线程代码抛出运行级别异常之后,线程会中断。主线程不受这个影响,不会处理这个RuntimeException,而且根本不能catch到这个异常。会继续执行自己的代码

9.线程池


Android基础知识


1.Looper总结

  • Looper通过prepare方法进行实例化,先从他的成员变量sThreadLocal中拿取,没有的话就new 一个Looper,然后放到sThreadLocal中缓存。每个线程只能创建一个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));
}
复制代码
  • Looper通过loop方法开启循环队列,里面开启了死循环,没有msg时候会阻塞
  • 在ActivityThread的main方法中也就是Activity启动的时候,已经调用了Looper.prepareMainLopper()方法

2.ThreadLocal在Looper中的使用

为了解决多个线程访问同一个数据问题,同步锁的思路是线程不能同时访问一片内存区域.而ThreadLocal的思路是,干脆给每个线程Copy一份一摸一样的对象,线程之间各自玩自己的,互相不影响对方 常见ThreadLocal应用场景:确保在每一个线程中只有一个Looper的实例对象

  • ThreadLocal的set方法
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}
复制代码
  • ThreadLocal的get方法
 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
 }
复制代码

简而言之:先拿到当前线程,再从当前线程中拿到ThreadLocalMap,通过ThreadLocalMap来存储数据。(ThreadLocalMap是Thread的成员变量)

  • handler postDelay这个延迟是怎么实现的:handler.postDelay直接将消息插入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。

3.Service 和 IntentService

Activity对事件响应不超过5秒,BroadcastReceiver执行不超过10秒,Service耗时操作为20秒。否则系统会报ANR

  • 使用startService()方法启用服务后,调用者与服务之间没有关连。调用者直接退出而没有调用stopService的话,Service会一直在后台运行
  • 使用bindService()方法启用服务,调用者与服务绑定在一起了,调用者一旦退出,服务也就自动终止
  • IntentService是Service的子类,会创建子线程来处理所有的Intent请求,其onHandleIntent()方法实现的代码,无需处理多线程问题

4.FragmentPageAdapter和FragmentPageStateAdapter的区别

  • FragmentPageAdapter在每次切换页面的的时候,没有完全销毁Fragment,适用于固定的,少量的Fragment情况。默认notifyDataSetChanged()刷新是无效的

  • FragmentPageStateAdapter在每次切换页面的时候,是将Fragment进行回收,适合页面较多的Fragment使用,这样就不会消耗更多的内存

5.Sqlite数据库,什么是事务

事务是由一个或多个sql语句组成的一个整体,如果所有语句执行成功那么修改将会全部生效,如果一条sql语句将销量+1,下一条再+1,倘若第二条失败,那么销量将撤销第一条sql语句的+1操作,只有在该事务中所有的语句都执行成功才会将修改加入数据库中
sqlite数据库相关操作,主要包括创建和增删改查,事务

6.怎么做Sqlite数据库升级

  1. 直接删除老数据库,但会造成数据丢失,一般不采用
  2. 对数据库进行升级,参考SQLite数据库版本升级

7.invalidate与requestLayout区别

  • view调用invalidate将导致当前view的重绘,viewGroup调用invalidate会使viewGroup的子view调用draw
  • requestLayout方法只会导致当前view的measure和layout,而draw不一定被执行。只有当view的位置发生改变才会执行draw方法

8.View和ViewGroup区别

  • ViewGrouponInterceptTouchEvent默认返回false,即不拦截事件,View没有拦截事件方法,View默认时消耗事件的
  • ViewGroup默认不会调用onDraw方法,View默认会调用onDraw方法。可以通过setWillNotDraw(boolean willNotDraw)来指定是否调用onDraw方法
    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
复制代码

9.android版本新特性

  • 5.0
    • 引入Material Design主题
  • 6.0
    • 运行时权限
  • 7.0
    • 文件读写权限适配FileProvider
    • 移除了对 Apache HTTP 客户端的支持,建议使用 HttpURLConnection 代替。继续使用 Apache HTTP API,必须先在 build.gradle 文件中配置:
        android {
            useLibrary 'org.apache.http.legacy'
        }
    复制代码
  • 8.0
    • 广播限制。不能在AndroidManifest文件对有些广播进行静态注册
  • 9.0

10.Android中一张图片占据的内存大小是如何计算

参考Android中一张图片占据的内存大小是如何计算

  • 图片来源是 res 内的不同资源目录时,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,做一次分辨率转换,规则如下:新分辨率 = 原图横向分辨率 * (设备的 dpi / 目录对应的 dpi ) * 原图纵向分辨率 * (设备的 dpi / 目录对应的 dpi )
  • 其他图片的来源,如磁盘,文件,流等,均按照原图的分辨率来进行计算图片的内存大小
  • 一张图片占用的内存大小的计算公式:分辨率 * 像素点大小;但分辨率不一定是原图的分辨率,需要结合一些场景来讨论,像素点大小就几种情况:ARGB_8888(4B)、RGB_565(2B) 等等

11.APP启动速度优化

  • 用adb命令可以检测启动时间,示例如下:
adb shell am start -W [packageName]/[.MainActivity]
./adb shell am start -W "com.hchstudio.dict"/".MainActivity"
复制代码

WaitTime为我们所关注的启动时间

  • app的启动流程,主要需要减少Application和启动界面的onCreate方法
  • 的app首页主题样式加上android:windowBackground,放一下app的背景图片,这样即使app启动慢,也会首先加载背景,这样就会给用户造成一种假象,认为是app已经启动
<!--AppTheme.Launcher为启动界面的主题样式-->
<style name="AppTheme.Launcher">
    <item name="android:windowBackground">@color/colorLauncher</item>
</style>
复制代码

12.内存抖动

内存抖动是由于短时间内有大量对象进出新生区导致的,它伴随着频繁的GC,gc会大量占用ui线程和cpu资源,会导致app整体卡顿。

避免发生内存抖动的几点建议:

  • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
  • 当需要大量使用Bitmap的时候,试着把它们缓存在数组或容器中实现复用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

13.Android中ClassLoader的种类&特点:

  • BootClassLoader(Java的BootStrap ClassLoader):

用于加载Android Framework层class文件。

  • PathClassLoader(Java的App ClassLoader):

只能加载已经安装过的apk的dex文件

  • DexClassLoader(Java的Custom ClassLoader):

可以从一个jar包或者未安装的apk中加载dex文件

  • BaseDexClassLoader:

是PathClassLoader和DexClassLoader的父类。

14.SharePreference为什么不能存储较大value

  • 获取数据时会卡主线程(比如getString()方法中会调用wait()方法)
    public String getString(String key, @Nullable String defValue) {
    	synchronized (this) {
        		awaitLoadedLocked();
        		String v = (String)mMap.get(key);
        		return v != null ? v : defValue;
    	}
    }
    
    private void awaitLoadedLocked() {
    	while (!mLoaded) {
        	try {
            	wait();
        	} catch (InterruptedException unused) {
    	}
    }
    复制代码
  • SharePreference存值的时候,内部会有一个静态的map保存了你所有的key和value
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    	if (sSharedPrefsCache == null) {
        		sSharedPrefsCache = new ArrayMap<>();
    	}
    
    	final String packageName = getPackageName();
    	ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    	if (packagePrefs == null) {
        		packagePrefs = new ArrayMap<>();
        		sSharedPrefsCache.put(packageName, packagePrefs);
    	}
    
    	return packagePrefs;
    }
    复制代码

Android框架知识


1.buttnife实现原理

通过注解处理器动态生成java文件,在java文件中进行findViewById和setOnClickListener操作

2. EventBus实现原理

通过观察者设计模式,先通过注册的方式将指定的类加到一个表里面,等发送消息时轮训那个表,依据注解和注解的value找到匹配的方法,然后执行该方法

3.LiveData原理

LiveData通知其他组件原理主要是观察者设计模式。在android里用的比较多的是MutableLiveData

 public class MutableLiveData<T> extends LiveData<T> {
    //非主线程中使用
    @Override
    public void postValue(T value) {
        super.postValue(value);
    }

    //主线程中使用
    @Override
    public void setValue(T value) {
        super.setValue(value);
    }
}
复制代码

通过LiveData的postValue或者setValue方法,通知观察者Observer数据的变化并请可观察的变化数据通过Observer的onChanged传导出来

其优点有

  • 遵从应用程序的生命周期,如在Activity中如果数据更新了但Activity已经是destroy状态,LivaeData就不会通知Activity(observer)
  • 不会造成内存泄漏(LiveData仅通知活跃的Observer去更新UI。 非活跃状态的Observer,即使订阅了LiveData,也不会收到更新的通知)

LiveDataobserve方法

    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        //将LifecycleBoundObserver和activity或者fragment的lifeCycle相关联
        owner.getLifecycle().addObserver(wrapper);
    }
复制代码

装饰器LifecycleBoundObserver

 class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
            super(observer);
            mOwner = owner;
        }

        @Override
        boolean shouldBeActive() {
            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
        }

        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
                //数据发生变化,如果activity或者fragment已经销毁,就解除订阅,避免了内存泄露
            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            activeStateChanged(shouldBeActive());
        }
......
复制代码
public interface Observer<T> {
    /**
     * Called when the data is changed.
     * @param t  The new data
     */
    void onChanged(@Nullable T t);
}

复制代码

第一个参数可以直接传activity,如果activity已经销毁直接return,否则owner.getLifecycle().addObserver(wrapper);,LiveData调用postValue或者setValue方法就会回调ObserveronChanged方法

  • LiveData的observe方法接收activity的getLifeCycle(可感知activity生命周期)和Observer(添加一个观察者到LiveData的内部集合里)。
  • LiveData的setValue方法将数据通知到所有观察者,如果actiivty已经销毁则之间解除订阅。否则所有观察者的onChange(T t)方法会被执行

这数据处理逻辑放在ViewModel里,需要更新UI时给观察者发送通知即可,同时不用关心内存泄漏

4.ViewModel

  • 处理逻辑
  • 存储数据,ViewModel储存的数据activity旋转后数据不会丢失
  • 数据共享

4.Lifecycle

5.FlowableObservable

RxJava1中使用ObservableObserver建立起订阅关系,但会产生背压问题。Rxjava2使用FlowableSubscriber替换RaJava1的ObservableObserverFlowable是在Observable的基础上优化后的产物,Observable能解决的问题Flowable也都能解决。但是并不代表Flowable可以完全取代Observable,Flowable运行效率要比Observable慢得多。 只有在需要处理背压问题时,才需要使用Flowable

  • 当上下游在不同的线程中,通过Observable发射,处理,响应数据流时,如果上游发射数据的速度快于下游接收处理数据的速度,这样对于那些没来得及处理的数据就会造成积压,这些数据既不会丢失,也不会被垃圾回收机制回收,而是存放在一个异步缓存池中,如果缓存池中的数据一直得不到处理,越积越多,最后就会造成内存溢出,这便是响应式编程中的背压(backpressure)问题
  • 如果能够确定:\

1.上下游运行在同一个线程中
2.上下游工作在不同的线程中,但是下游处理数据的速度不慢于上游发射数据的速度
3.上下游工作在不同的线程中,但是数据流中只有一条数, 则不会产生背压问题,就没有必要使用Flowable,以免影响性能。

6.app优化

  • 内存优化:使用leakcanary抓取内存泄露,或者使用android studio抓取内存信息,通过Profiler分析内存泄露情况
  • 体积优化
    • 不复杂图片使用svg代替png。换肤时使用着色器,可减少图片资源
    • build文件配置
      • 保留指定语言
      • 保留指定so库架构
      • 开启混淆压缩

7.Rxjava中关于Disposable

Rxjava容易遭层内存泄漏。在订阅了事件后没有及时取阅,导致在activity或者fragment销毁后仍然占用着内存,无法释放。而disposable,可以用来取消订阅
参考Rxjava关于Disposable你应该知道的事

8.Glide

  • Glide的缓存
    • Glide缓存机制大致分为三层:Lru算法缓存、弱引用缓存、磁盘缓存
    • 读取的顺序是:Lru算法缓存、弱引用缓存、磁盘缓存(据说glide最新版改了,先从弱引用中取,没有的话再从Lru中取,再放进弱引用中)
  • Glide.with(this) .load("http://www.baidu.com/img/bdlogo.png") .into(imageView);
    • with()方法是对RequestManager进行配置
    public static RequestManager with(FragmentActivity activity) {
         return getRetriever(activity).get(activity);
    }
    复制代码
    • load()方法是对RequestBuilder进行配置
    public RequestBuilder<Drawable> load(@Nullable Object model) {
         return asDrawable().load(model);
    }
    复制代码
    • into()方法是通过线程池给imageView进行图片的加载设置
  • 占用内存较小
    • 默认使用RGB_565格式
  • 支持gif
  • 与Activity生命周期绑定,不会出现内存泄露
      1. Glide绑定Activity时,生成一个无UI的Fragment
      1. 将无UI的Fragment的LifeCycle传入到RequestManager中
      1. 在RequestManager的构造方法中,将RequestManager存入到之前传入的Fragment的LifeCycle,在回调LifeCycle时会回调到Glide的相应方法

9.Android串口

参考Android串口通信:抱歉,学会它真的可以为所欲为
通过串口编程可以让Android应用和外设进行通信,通过谷歌提供的一个来开发。
通过那个库,打开串口。获取输入输出流,就可以利用串口接收数据和发送数据了
核心参数:

path:为串口的物理地址,一般硬件工程师都会告诉你的例如ttyS0、ttyS1等,或者通过SerialPortFinder类去寻找得到可用的串口地址。
baudrate:波特率,与外接设备一致
flags:设置为0,原因较复杂,见文章最底下
复制代码

10.断点续传实现

  • 在本地下载过程中要使用数据库实时存储到底存储到文件的哪个位置了
  • 下次继续传递时,才能通过HTTP的GET请求中的setRequestProperty("Range","bytes=startIndex-endIndex");方法可以告诉服务器,数据从哪里开始,到哪里结束
  • 同时在本地的文件写入时,RandomAccessFile的seek()方法也支持在文件中的任意位置进行写入操作
  • 最后通过广播或事件总线机制将子线程的进度告诉Activity的进度条。关于断线续传的HTTP状态码是206

11.RxJava 变换操作符 map flatMap concatMap zip buffer

  • map:【数据类型转换】将被观察者发送的事件转换为另一种类型的事件。适用于一对一转换
  • flatMap:【化解循环嵌套和接口嵌套】将被观察者发送的事件序列进行拆分 & 转换 后合并成一个新的事件序列,最后再进行发送。应用场景:一个接口调用时依赖另一个接口的返回值,适用于一对多
  • concatMap:【有序】与 flatMap 的 区别在于,拆分 & 重新合并生成的事件序列 的顺序与被观察者旧序列生产的顺序一致。
  • zip: 应用场景:两个接口的数据相互结合才能显示UI
  • buffer:定期从被观察者发送的事件中获取一定数量的事件并放到缓存区中,然后把这些数据集合打包发射。

12.热修复

  • 什么是dex分包
    • 把一个apk解压后,会有一个classes.dex的文件,它包含了我们项目中所有的class文件
    • dvm中存储方法id用的是short类型,所以就导致dex中方法不能超过65535个
  • 分包的原理
    • 就是将编译好的class文件,拆分打包成多个dex
    • 除了第1个dex文件外(正常apk中存在的唯一的dex文件),其他的所有dex文件都以资源的形式放到apk里面,并在Application的onCreate回调中通过系统的ClassLoader加载它们。
    • 注意:在注入之前就已经引用到的类,则必须放到第一个dex文件中,否则会提示找不到该文件
  • ClassLoader动态加载
    • 每个dex文件是一个Element,多个dex文件排列成一个有序的数组就是dexElements
    • 将我们修复Bug后的dex文件,通过反射加入到dexElements数组最前面,就可以了

设计模式


1.装饰设计模式

  • 当不适合采用生成子类的方式对已有类进行扩充时,采用装饰设计模式可以扩展一个对象的功能,可以使一个对象变得越来越强大
  • 不适合采用生成子类的方式对已有类进行扩充原因:会使类更加臃肿。子类会继承父类所有非private的变量和方法,然后再进行扩充。而使用装饰设计模式扩充的类,只需要增加扩种那部分功能即可
  • 使用场景:RecyclerView本身是不支持添加底部和头部的,那么采用装饰设计模式可以对其进行功能扩展。装饰设计模式 RecyclerView添加头部和底部

2.MVC、MCP、MVVP 的区别

1.MVC Android传统就是用MVC模式,Modle(逻辑)和V(View)直接交互,耦合度太高,MVC中是允许Model和View进行交互的

2.MVP Model与View之间的交互由Presenter完成。还有一点就是Presenter与View之间的交互是通过接口的。当View 需要更新数据时,首先去找 Presenter,然后 Presenter 去找 Model 请求数据,Model 获取到数据之后通知 Presenter,Presenter 再通知 View 更新数据,这样 Model 和 View就不会直接交互了,所有的交互都由 Presenter 进行,Presenter 充当了桥梁的角色。很显然,Presenter 必须同时持有 View 和 Model 的对象的引用,才能在它们之间进行通信 存在问题:

  • 内存泄露:由于Presenter经常性的需要执行一些耗时操作那么当我们在操作未完成时候关闭了Activity,会导致Presenter一直持有Activity的对象,造成内存泄漏
  • 随着业务逻辑的增加,UI的改变多的情况下,这样就会造成View的接口会很庞大。而MVVM就解决了这个问题

解决办法: 在Presenter中使用弱引用,将view的引用加到弱引用中去 每个Activity都有BaseActivity,BaseActivity中

3.MVVM通过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层 MvvmMvp比较相似,不同的是Mvvm中可以用DataBinding或者LiveData动态绑定数据,进一步降低耦合

  • Activity负责UI操作,ViewModel负责数据逻辑。两者通过LiveData进行关联。ViewModel返回LiveData实例和Activity绑定,ViewModel有变化时可以自动更新UI,Activity也可以通过ViewModel发起数据请求
  • 问题:

看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致

以登录为例,登录时只需要拿着用户名和密码请求服务器即可

  • MVC中,需要先找到输入框,再从输入框中拿到用户名和密码,再进行登录操作
  • MVP中,通过Presenter拿到用户名和密码,进行登录。相当于通过引入了PresenterM层和V层分离,降低耦合
  • MVVM中,用户在输入框输入完用户名和密码后,这种UI的变化直接同步到数据,直接登录

3.策略设计模式

适用场景: 某些业务中,某一个行为,会有多个实现类,并且当前业务只会选择一种实现类
参考用漫画的方式讲策略设计模式,一看就懂

4.Double Check Lock 实现单例

public static TestInstance getInstance(){ //1
    if (mInstance == null){ //2
        synchronized (TestInstance.class){ //3
            if (mInstance == null){ //4
                mInstance = new TestInstance(); //5
            }
        }
    }
    return mInstance;
}
复制代码

第一层判断主要是为了避免不必要的同步
第二层的判断则是为了在 null 的情况下创建实例。mInstance = new TestInstance(); 这个步骤,其实在jvm里面的执行分为三步:
1.在堆内存开辟内存空间;
2.初始化对象;
3.把对象指向堆内存空间;
由于在 JDK 1.5 以前 Java 编译器允许处理器乱序执行。不过在 JDK 1.5 之后,官方也发现了这个问题,故而具体化了 volatile ,即在 JDK 1.6 以后,只要定义为 private volatile static DaoManager3 sinstance ; 就可解决 DCL 失效问题


算法


1.反转单链表

class Node{
    private int data;
    private Node next;
    public Node(int data,Node next){
        this.data=data;
        this.next=next;
    }

}
复制代码
Node node4 = new Node(4, null);
Node node3 = new Node(3, node4);
Node node2 = new Node(2, node3);
Node node1 = new Node(1, node2);
Node pHead = node1;//头结点
复制代码

这组链表从1到4排序,要求反转后4到1

public static Node reverseList(Node pHead) {
        Node pReversedHead = null; //反转过后的单链表存储头结点
        Node pNode = pHead; //当前节点
        Node pPrev = null; //前一结点
        while (pNode != null) {
            //1.记录next,下一步:更新当前节点的上一节点和本身。最后移动一位
            Node pNext = pNode.next;
            if (pNext == null) {
                //到了尾节点
                pReversedHead = pNode;
            }
            pNode.next = pPrev;
            pPrev = pNode;
            pNode = pNext;
        }

        return pReversedHead;
}

 //递归方式反转(node1->node2->node3->node4->node5)
 public static ListNode reverseR(Node head){ 
    //空链表和一个结点的链表无需反转 
    if(head == null || head.next == null){ 
        return head; 
    } 
    //递归到node4(head)时回溯时,reverseR(node4.next)直接返回node5
    Node res = reverseR(head.next); 
    //递归回溯时,此时head指向node4,将node4的next(node5)的next指向node4(head)
    head.next.next = head; 
    //将node4.next指向null
    head.next = null; 
    return res; 
 }
复制代码

输出

pHead = reverseList(pHead);//反转之后头结点
while (pHead != null) {
    System.out.println(pHead.key);
    pHead = pHead.next;
}
复制代码

Android复习资料——常见面试算法题汇总(一)
Android复习资料——常见面试算法题汇总(二)
二叉树的先序遍历、中序遍历、后序遍历

2.LRU算法(最近最少使用算法)

  • 可以在存储不足时移除掉最近最少使用的数据
  • 使用哈希链表(LinkedHashMap)把数据按照最后使用时间来排序。最新使用的数据插入(移到)链表最前端

其他


1.Https

  • 使用对称密钥:加密和解密使用的是同一个密钥。弊端:最开始的时候怎么将这个对称密钥发送出去呢?如果对称密钥在发送的时候就已经被拦截,那么发送的信息还是会被篡改和窥视
  • 使用非对称密钥:双方必须协商一对密钥,一个私钥一个公钥。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据, 只有对应的私钥才能解密。A将自己的公钥发给B,B以后给A发消息时候用公钥加密后发送,A收到消息后用自己的私钥解密。弊端:非对称密钥(RSA)加密和解密速度慢
  • 非对称密钥+对称密钥:A将自己的公钥发给B,B用公钥将对称密钥加密发给B,这样双方就安全地传递了对称加密的密钥,既解决了密钥的传递问题, 又解决了RSA速度慢的问题
  • 数字证书。假如一开始A将自己的公钥发给B时,中间被拦截,拦截者替换成自己的公钥,这样还是会有安全问题。解决办法:数字证书

2.三次握手

其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常

  • 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

3.OAuth 2.0

用一个短期的令牌(token)允许用户让第三方应用访问他在某一网站上存储的私密的资源,而不需要用户名和密码

app登录成功后服务器返回一个token,下次app就通过这个token来直接登录,token如果过期就需要跳到登录页面重新登录
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异

  • 令牌是有时间限制,到期会自动失效。密码一般长期有效,除非用户修改。

  • 令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。

  • 令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。