【Android面试周】快问快答之 Android 篇

更新时间:2018/03/21


  • 多进程有什么好处
  • 跨进程通信方式
  • Activity保活思路
  • Binder通信原理
  • 什么是用户态、内核态
  • 什么是IdleHandler,有什么用?
  • HandlerThread,IntentService有什么用途?
  • Handler里的ThreadLocal是什么,有什么用?
  • Dalvik vs ART vs JVM
  • Handler造成内存泄漏的原因 ...

多进程有什么好处

  • 稳定性:子进程的崩溃闪退不会影响主进程;
  • 不占用主进程的内存,如一些大图展示;
  • 安全隔离,不会因为子进程的安全漏洞危害主进程;
  • 基于多进程可以做到Activity级别的保活。

跨进程通信方式

  • 同步AIDL方式:创建AIDL接口,自动生成代理类,通过ServiceManager对外部进程提供Service,通过Bindler实现数据通信;缺点是:每次添加一个接口都要修改AIDL文件,如果通信接口很多就比较麻烦;
  • 异步Messager方式:底层依赖AIDL,实现了异步跨进程通信;
  • ContentProvider方式:提供对外进程的数据Query,类似数据库方式,

Activity保活思路
点击后退键时,调用moveTaskToBack而不是finish,再次进入时,如果仍在内存就调用moveTaskToFront。如果二次打开有新的参数,可以用 onNewIntent。

Binder通信原理
Binder主要用于多进程间通信,即函数互相调用和数据传递。涉及到以下几个元素:

  1. ServiceManager;每个进程如果能对外提供函数接口,需要向底层ServiceManager进行注册,告诉它你的接口能力;
  2. Client进程想要调用Server进程某个接口时,需要去请求ServiceManager,这时ServiceManager会生成一个动态代理对象直接返回给Client进程去使用,当Client进程发起函数调用后,SM会去找到Server进程,并调用它的对应函数。

《Binder学习指南》

什么是用户态、内核态
首先要了解“进程隔离”,由于进程使用的是虚拟空间,这让每个进程都认为自己占用了整个系统的所有资源,实际上进程之间的物理空间是互相不可见的。这可以提高进程的安全性,互相隔离。

对于Linux系统而言(Android基于Linux),用户空间就是应用程序进程所能访问到的虚拟空间,而内核空间则是高安全性、受保护的物理空间。用户空间无法直接访问内核空间,除非通过系统对外提供的安全系统调用这个统一入口。

当用户进程在执行系统调用进入内核空间时,处于内核运行态;
当用户进程在执行自己的代码时,处于用户态。

什么是IdleHandler,有什么用?
我们知道,Looper会不断从MessageQueue里拿Message进行处理,当MessageQueue为空时,Looper便进入了Idle状态,这时它就会取出IdleHandler来处理。

比如当用户停住某个界面无手势交互时,主线程进入空闲,其Looper就会去执行IdleHandler,很多优先级不高的事可以在这里去执行,比如,就可以让IdleHandler做一些预加载的事情。

Looper.myQueue().addIdleHandler(new TestIdleHandler()); // 向Looper中添加一个IdleHandler
    class TestIdleHandler implements MessageQueue.IdleHandler{  
        /** 
         *返回值为true,则保持此Idle一直在Handler中,否则,执行一次后就从Handler线程中remove掉。 
         */  
        @Override  
        public boolean queueIdle() {  
            Log.d("android-interview","空闲线程开始执行!");  
            return true;  
        }  
    }  

例子:在LeakCanary里,当Activity OnDestroy后,会把该Activity的WeakReference存起来后面来检测是否已回收。为了不影响主线程运行,LeakCanary选择了IdleHandler,在主线程空闲时去检测Activity的回收情况。

HandlerThread,IntentService有什么用途?
HandlerThread本质是一个线程,不过我们知道线程在运行完特定任务后会自动结束,而HandlerThread 就像一个持续运行的等待任务的线程,随时有任务都可以让他来处理,而且特别适用于跨线程任务。它内部使用了一个Looper来持续遍历MessageQueue,每次有任务了就可以通过Handler向里面发送Message即可。

IntentService本质是一个Service,不过我们知道常规的Service存在两个问题:

  • 默认在主线程执行;
  • 不会自动关闭,需要手动停止。 由于我们使用Service一般是用来执行一些耗时任务,所以我们希望它在后台自动执行,执行完后自动关闭,这就是 IntentService。它内部维护了一个 HandlerThread,运行在非主线程里,可以持续处理外部消息,当所有任务执行完毕,会自动停止。

Handler里的ThreadLocal是什么,有什么用?
ThreadLocal本身用来包装某个变量,这个变量的值在不同的线程里是不同的。其实了解了原理就很简单,看似ThreadLocal包装的是单个变量,而实际上内部是一个Map,key是某个线程,因此每次在取ThreadLocal的值时,会根据不同的线程取出属于它的值。

在Looper里用到了ThreadLocal,首先我们知道,Looper是每个线程最多只有一个的,(主线程默认会创建一个Looper,而子线程可通过Looper.prepare()来创建),这与ThreadLocal的特性是一致的,通过一个ThreadLocal<Looper>就可以实现为不同线程提供一个对应的Looper对象

apk是如何打包的?

  • 资源文件:通过aapt打包得到一个R.java文件,其实是资源文件索引表,有了这个以后可以在Java运行时查找到某个资源;(aapt: Android Asset Packaging tool)
  • AIDL文件:这些App是对外进程提供的接口,会生成对应的interface文件;
  • Java代码:常规Java编译成 .class 文件 ->通过 dx 工具打包成 Android系统识别的 dex 文件;
  • APK builder:有了dex文件和编译的资源文件,就可以打包成apk了;
  • 签名Jarsigner:为了防止别人跟你打包一个一样的apk,你需要对apk进行签名表明你才是正版;
  • zipalign:对签名后的apk文件进行对齐处理,使apk中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问apk文件时会更快。

最终得到一个Signed and Aligned .apk

aar与jar有什么区别?
jar是普通java打包生成的,只包括.class和清单文件;
aar除了.class文件,还包括AndroidManifest.xml、资源文件、res文件、R文件等;

Dalvik 垃圾回收机制

  • Dalvik 的 GC 类型:
    • GC_FOR_MALLOC:分配对象时发现内存不够引发GC;
    • GC_CONCURRENT:空闲是可用堆内存小于阈值;
    • GC_BEFORE_OOM:在OOM之前最后的挣扎回收;
    • GC_EXPLICIT:应用程序显示调用System.gc(), Runtime.gc()去触发回收。
  • 回收方式:
    • 非并行回收:在回收时会锁住堆,如果其他线程需要使用堆,会被一直挂起到GC结束,这就是Stop-the-World;不过不访问堆的线程不会受到影响;
    • 并行回收:回收的过程中,有条件的挂起和唤醒非GC线程,可以为程序带来更好的响应性,但耗费了更多CPU。
  • 回收算法:
    • 标记-清理Mark and Sweep算法
    • 这种算法会导致内存碎片,有时即使内存看似足够但无法分配时,很有可能是内存碎片太多,所以要尽量创建很多临时小对象。

Dalvik vs JVM 区别

  • JVM是基于栈,而DVM是基于寄存器,可以更快速,适用于cpu 内存有限的移动端;

Dalvik vs art

  • 编译机制
    • Dalvik采用JIT机制,运行时才将字节码转换为机器码,速度较慢;
    • ART(Android Runtime)在安装时就将字节码转为机器码进行存储,在后续的首次打开和运行速度都会更快,不过机器码占用空间更大,可以看作空间换时间。
  • GC机制改善
    • Dalvik只有一种垃圾回收机制,默认是标记-清理,(当然也可以在编译Dalvik时选择Copy复制算法);标记-清理算法会导致内存碎片出现。
    • ART的堆分成了四部分(Dalvik只有两部分),不同部分采用了不同强度的回收算法:
      • Image Space:存储预加载类,不会进行回收;
      • Zygote Space:Zygote进程及其fork出来的应用程序进程共享的空间;
      • Allocation Space:应用程序进程“独占”空间;
      • Large Object Space:大对象空间,单独管理大内存对象,提高了内存的分配效率和回收效率;
    • ART有三种回收方式,力度依次减弱:
      • 标记-清理:用于回收Zygote Space,Allocation Space和Large Object Space;
      • 部分Partial Mark Sweep:用于回收Allocation Space和Large Object Space;
      • Sticky Mark Sweep:只回收上一次GC后新产生的垃圾对象,回收Allocation Space。
    • ART在回收过程中,减少应用被打断的次数,减少挂起非GC线程次数,更多的唤醒以提高响应性;
    • 从谷歌公布的信息来看,平均暂停时间已经降低到了3ms,这比Dalvik模式下的54ms快了很多。

扩展:上面我们看到,ART专门开辟了一块 Large Object Space(LOS)用来存放大对象,很多人用过F家的图片加载框架:Fresco,它在5.0以上的机器即ART上,就是在LOS这块空间里存放Bitmap,而在5.0以下的Dalvik上,把bitmap存放在了 匿名共享内存 区域。

Handler造成内存泄漏的原因

  1. 非静态内部类 和 匿名内部类会自动引用外部类,如Activity;
  2. 如果该内部类Handler未被及时回收,它会造成Activity的泄漏;
  3. 什么情况下Handler不会被及时回收?
    • Handler内部在执行耗时任务,在OnDestroy时该任务仍然执行;
    • 有延时Message存活,内存泄漏链:Activity <-- Handler <-- Message <-- MessageQueue <-- Looper,而我们知道,在主线程里会默认创建一个Looper,而且这个Looper是伴随整个应用程序生命周期,从而造成了内存泄漏。 > Message在创建时,会保存目标Handler引用:msg.target.dispatchMessage(msg);

SharedPreference
SharedPreference默认存储在 data/data/com.package/shared_prefs/目录下面,它采用了缓存机制,从文件里读取内容到内存里,在读的时候直接走内存,写的时候才更新到文件。其中,commit是同步写入文件中,而apply仅仅是写入内存,然后把写入文件的任务丢到一个异步队列去执行。

在多线程下是可以正常工作的,因为SP的读写都是加锁的;不过在多进程下就会出问题了,互相不知道对方在写入从而发生数据错误。

  • 多进程共享SP方式一:通过属性Context.MODE_MULTI_PROCESS,它会在获取SharedPreferences实例时,检查SP文件是否被改动,如果改动则重新加载文件。
    public static boolean putString(Context context, String key, String value) {
        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_MULTI_PROCESS);
        SharedPreferences.Editor editor = settings.edit();
        editor.putString(key, value);
        return editor.commit();
    }

    public static String getString(Context context, String key, String defaultValue) {
        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_MULTI_PROCESS);
        return settings.getString(key, defaultValue);
    }

下面看下Context.MODE_MULTI_PROCESS的大致实现:可以看出优先从缓存里取出SP实例,然后看这个实例内的文件是否发生变化,如果变化了就重新加载。

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}
    void startReloadIfChangedUnexpectedly() {
        synchronized (this) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

不过,它存在一个问题:前面提到的apply方法是异步的,如果过个进程频繁去apply,这些变更会放入一个队列里依次执行,那两个进程的队列交叉执行就会出问题。

  • 多进程共享SP方式二:借助ContentProvider提供读写接口

打包

  1. R文件怎么生成的
  2. resources.arsc是什么文件,做什么用的

生命周期
onSaveInstance onRestoreInstance

事件传递
cancel事件;
子view不让父view拦截,disallow,何时不生效
touch里的down被拦截了,为什么后面的事件不会继续传递了,源码;

© 著作权归作者所有
这个作品真棒,我要支持一下!
我是wingjay,目前就职于阿里。 本系列会基于我的工作与学习经验,通过横向、纵向的Android与Java知...
0条评论
top Created with Sketch.