面试

6 阅读42分钟

Kotlin面试题

Flutter面试题

Android基础

时间复杂度:

时间复杂性,又称时间复杂度算法时间复杂度是一个函数,它定性描述该算法的运行时间。

空间复杂度:

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。

1.动态代理

  1. 动态代理为什么必须基于接口?

因为Java是单继承,动态代理类已经实现了Proxy,不能再继承其他的类,所以动态代理必须针对接口。

  1. 调用动态代理类实现的方法,为什么会自动调用InvocationHandler的invoke方法?

因为动态代理类的构造方法是参数为InvocationHandler的构造方法,在被代理接口的代理类实现方法中会调用InvocationHandler的invoke方法,所以在调用代理类中实现的方法时会调用InvocationHandler的invoke方法。

屏幕适配

方案一: 为了自适应大多数的android手机分辨率。drawable- hdpi、drawable- mdpi、drawable-ldpi的区别:

drawable-hdpi里面存放高分辨率的图片,如WVGA (480x800),FWVGA (480x854)

drawable-mdpi里面存放中等分辨率的图片,如HVGA (320x480)

drawable-ldpi里面存放低分辨率的图片,如QVGA (240x320)

android系统会根据机器的分辨率来分别到这几个文件夹里面去找对应的图片。在开发程序时为了兼容不同平台不同屏幕,建议各自文件夹根据需求均存放不同版本图片。

这个环节可以由android 程序开发人员去完成,appui设计师配合就行。

因为还有手机屏幕是横屏和竖屏的。所以我们也需要在res目录下建立layout-port和layout-land两个目录,里面分别放置竖屏和横屏两种布局文件,以适应对横屏竖屏自动切换。

方案二: 使用点9图

2.传递大内存的数据

Intent传递Bitmap容易抛出内存溢出异常,为什么putBinder不会?

  • Intent传递Bitmap默认文件描述符fd是禁用的,无法使用共享内存,只能采用拷贝到缓冲区的方式,导致缓冲区可分配内存越界异常。(通常缓冲区的大小为1M)
  • putBinder文件描述符默认是开启的,可以使用共享内存。

3.图片Bitmap大小的计算

densityDpi?

这是屏幕像素密度,一英寸屏幕有多个像素点。 通过以下方法可以获取:

float densityDpi= getResources().getDisplayMetrics().densityDpi;

ARGB8888的图片,每个像素占4个字节;

RGB565的图片,每个像素占2个字节;

  • 图片大小 = w * h * 每个像素所占的字节数

当图片资源所在文件夹与手机dpi不一致时,图片加载进来会被缩放。

  • 缩放系数 = 手机dpi / 图片资源所在文件夹dpi
序号文件夹dpi匹配dpi区间
1drawable-mdpi160120-160,包含160
2drawable-hdpi240160-240,包含240
3drawable-xhdpi320240-320,包含320
4drawable-xxhdpi480320-480,包含480
5drawable-xxxhdpi640480-640,包含640
6drawable160160

如果手机是480dpi,图片放在drawable-xxhdpi,缩放系数是480/480=1 如果手机是480dpi,图片放在drawable-xxxhdpi,缩放系数是480/640=0.75

  • 图片大小 = 长 * 宽 * 每个像素占的字节数 * 缩放系数
  1. 图片大小=长每个像素占的字节数*缩放系数;
  2. 缩放系数=手机dpi/图片资源所在文件夹dpi;
  3. 如果图片放在drawable-nodpi、raw、asset文件下,加载时不做任何缩放,原样输出。
BitmapFactory.Options

通过 BitmapFactory.Options 来缩放图片,主要用 inSampleSize 即采样率。

一般inSampleSize设置的大小以2的次方形式对图片宽高、缩放比例进行影响,对图片宽高为 1/inSampleSize,对缩放比例(像素数、占有内存大小)为 1/(inSampleSize的2次方)

当inSampleSize=1,采样后的图片大小为图片原始宽高,像素数为原图大小,占有内存大小为原图大小

当inSampleSize=2,采样后的图片大小为图片原始宽高的1/2,像素数为原图大小的1/4,占有内存大小为原图大小的1/4

如:

一张图片 10241024,假定采用 ARGB8888 格式存储,占有内存为 102410244,图片大小为 4MB 设置inSampleSize=2,采样后的图片大小为 512512*4 ,图片大小为 1MB

获取采样率缩放图片(优化图片加载内存)

设置 BitmapFactory.Options 的 inJustDecodeBounds 为true(此时不会加载图片,会对图片进行解析)

从 BitmapFactory.Options 获取图片的原始宽高信息,对应 outWidth 和 outHeight

结合所需要的图片大小计算采样率 inSampleSize

设置 BitmapFactory.Options 的inJustDecodeBounds 为false,重新加载图片

4.Binder通信概览

利用内核空间的缓冲区映射实现用户端进程之间的通信。

利用内存映射,Binder的map函数会将内核空间直接与用户空间对应,Binder驱动在Client进程和Server进程之间建立起一条通道,Server进程间接实现Binder,Client进程获取Binder的代理对象(实际是Binder引用),用户空间可以直接访问内核空间的数据,由此通过Binder实现跨进程通信。

Binder进程讲解

5.ThreadLocal

在介绍Handler消息机制之前有必要了解下ThreadLocal,ThreadLocal的作用是用来隔离多线程环境下数据的影响,保证数据在当前线程环境下不会被污染。

原理: 每一个Thread都持有一个ThreadLocalMap用来存放数据,不同线程持有的ThreadLocalMap内存空间都是不一样的,从空间隔离的维度保证数据在多线程环境下的安全。

6.Handler线程间消息同步

Handler: 处理Message。

Looper: 从队列中取出消息。

MessageQueue: 存放Message的时间有序队列。

Message: 承载消息数据。

主流程: Looper的私有构造方法中新建了ThreadLocal和MessageQueue,在调用prepareLooper初始化Looper时通过ThreadLocal建立了Looper与当前线程一一对应的关系,Looper持有MessageQueue,因此也建立了一一对应的关系。 Handler调用sendMessage的时候,获取到线程的Looper和MessageQueue,然后执行消息入队列。 Looper.loop开启循环,通过messageQueue不断通过next获取队列的消息,然后通过handler.dispatchMessage执行消息的分发。

Handler如何完成跨线程通信的? 在孵化进程的时候会给每一个进程分配一个地址空间,同一个进程中的线程共享该进程的堆内存空间,Handler实例分配在堆空间上,在线程间是共享的,所以同一个进程中可以依赖handler完成线程间的通信。

线程切换:handler所在的线程由Looper所在的线程决定,handler跟looper是绑定的,Looper跟当前线程是绑定的,Looper.loop()消息循环,在哪个线程决定了handler.handleMessage()在哪个线程处理消息。

MessageQueue阻塞的情况:

  • 延迟消息:下一条消息执行时间还未到的时候
  • 消息队列为空
  • 队列中存在同步屏障,没有异步消息的时候

MessageQueue唤醒的情况:

  • 设置阻塞延迟的时间到了,执行自动唤醒
  • 当队列里边插入新的消息
  • 队列中存在同步屏障,插入新的一步消息的时候

延迟阻塞消息执行的过程是什么样的?

如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量nextPollTimeoutMillis),然后在循环开始的时候判断如果这个Message有延迟,就调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞。

  1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
  2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
  3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
  4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;直到阻塞时间到或者下一次有Message进队。

在Meaasge入队的时候会对消息的时间进行排序计算,保证message是按照时间顺序插入的。

同步屏障 MessageQueue.postSyncBarrier()

view.post()和handler.post()有什么区别

view.post()添加一个runnable任务到ViewRootImpl中进行缓存,在屏幕刷新Chorographer.postCallback添加一个mTraversalRunnable异步消息任务到handler消息队列中,执行异步消息进入到view的绘制流程,在performTraversals中会调用之前缓存在ViewRootImpl中的runnable添加到handler消息队列执行,当前view的绘制执行完成之后才会执行view.post的runnable,所以view.post runnable的中回调中能够拿到view的测量尺寸。

7.Activity的启动流程

image.png

Launcher进程通过ActivityManagerProxy跨进程发送startActivity的请求到ActivityManagerService,AMS通过socket跨进程请求Zygote fork处目标应用进程(client进程),应用进程通过ActivityManagerProxy发送attachApplication到ActivityManagerService,AMS通过ApplicationThreadProxy发送scheduleLaunchActivity到App应用进程,ApplicationThread通过Handler将launchActivity切换到主线程委托给Instrumentaion执行,后续Activity的生命周期调用类似的重复以上跨进程通信交互。

Activity启动

8.Android屏幕刷新机制

利用Choreographer监控应用的帧率。

Android屏幕刷新机制详解,Systrace

9.View的绘制流程

代码的关键调用:

--> WindowManagerImpl.addView(decorView, layoutParams) 
--> WindowManagerGlobal.addView()
WindowManagerGlobal.addView()负责创建ViewRootImpl,同时执行root.setView(view, wparams, panelParentView);

ViewRootImpl.setView(decorView, layoutParams, parentView)
-->ViewRootImpl.requestLayout()
-->scheduleTraversals()
-->TraversalRunnable.run()
-->doTraversal()
-->performTraversals()(performMeasure、performLayout、performDraw)

View的绘制从ActivityThread的Handler处理RESUME_ACTIVITY事件之后,在执行performResumeActivity之后,创建Window和DecorView并调用WindowManager的addView方法,在addView方法中会调用ViewRootImpl的setView方法,最终执行performTraversals方法,依次执行performMeasure、performLayout、performDraw。

分别对应View绘制的三大流程。在performMeasure中会依次调用measure、onMeasure,测量view的大小,最后调用setMeasuredDimension设置测量的结果。如果是ViewGroup,会遍历子View测量自身大小并设置结果。

在performLayout中会依次调用layout、onLayout,根据measure阶段测量的大小确定view在屏幕中的位置。该阶段只在ViewGroup中执行。

在performDraw中,会依次调用draw、onDraw,在onDraw()方法中首先会绘制背景,然后绘制view的内容,再然后调用dispatchDraw()调用子view的draw方法,最后绘制滚动条。ViewGroup默认不会执行onDraw方法,如果复写了onDraw(Canvas)方法,需要调用 setWillNotDraw(false);清除不需要绘制的标记。

resuestLayout、invalidate和postInvalidate有什么区别?

  • requestLayout: 该方法会递归调用父窗口的requestLayout,最终触发ViewRootImpl的performTraversals方法,此时mLayoutRequested为true,会触发onMeasure和onLayout,但不一定会触发onDraw。

  • invalidate: 该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会触发onMesaure()与onLayout()方法,但是会触发onDraw()方法。

  • postInvalidate: 该方法跟invalidate流程一样,只不过可以在子线程中调用。

布局的加载解析过程 (插件化换肤) 前提:待换肤的apk资源id与换肤包apk中的资源id要相等。

原理:利用apk包解析、资源加载和xml布局解析创建view并设置资源,来完成插件式换肤。

apk包资源的解析: 通过AssetManager.addAssetPath()加载apk包,在jni层去解析apk包,构造处Resource对象,通过名称、类型、包名获取资源。

// 获取要换肤的资源id:
try {
    int attributeCount = attrs.getAttributeCount();
    for (int i = 0; i < attributeCount; i++) {
        String attributeValue = attrs.getAttributeValue(i);
        if (attributeValue.startsWith("@")) {
            String attrValue = attributeValue.substring(1);
            int resId = Integer.parseInt(attrValue);
            // 资源名称
            String resourceName = context.getResources().getResourceEntryName(resId);
            // 资源类型
            String resourceType = context.getResources().getResourceTypeName(resId);
            // 获取资源皮肤包中对应的资源id
           int skinResId = skinResources.getIdentifier(resourceName, resourceType, "换肤apk的包名");
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

xml加载解析: setContentView,调用phoneWindow.setContentView(),新建DecorView,然后加载系统R.id.content这个布局作为DecorView的根布局,然后将setContentView的布局添加到contentView中。在LayoutInflater加载xml布局,使用XmlPullParser解析xml布局,最终经过LayoutInflater.Factory的实现类通过反射创建view。

try {
            Resources superRes = getResources();
            // 创建AssetManager,但是不能直接new所以只能通过反射
            AssetManager assetManager = AssetManager.class.newInstance();
            // 反射获取addAssetPath方法
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            // 皮肤包的路径:  本地sdcard/plugin.skin
            String skinPath = Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator+"plugin.skin";
            // 反射调用addAssetPath方法
            addAssetPathMethod.invoke(assetManager, skinPath);
            // 创建皮肤的Resources对象
            Resources skinResources = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
            // 通过资源名称,类型,包名获取Id
            int bgId = skinResources.getIdentifier("main_bg","drawable","com.hc.skin");
            Drawable bgDrawable = skinResources.getDrawable(bgId);
            // 设置背景
            findViewById(R.id.activity_main).setBackgroundDrawable(bgDrawable);
        } catch (Exception e) {
            e.printStackTrace();
        }
  1. 资源的获取:反射获取AssetManager对象,通过addAssetPath方法传入资源包路径加载皮肤资源,创建获取皮肤包中资源的Resources对象,通过资源名称、类型、包名获取资源。
  2. 获取待换肤的View:自定义LayoutInflater.Factory,通过反射覆盖系统的Factory,在onCreateView回调方法中拿到新建的view和view对应的AttributeSet属性,遍历AttributeSet找到要换肤的属性名称和属性值,连同view一起缓存到集合中。
  3. 执行换肤:遍历要换肤的view集合,根据view要换肤的属性名和属性值,通过第一步自定义皮肤Resources对象获取到资源包中对应的皮肤属性资源,然后应用到view的相应属性上。

概括:自定义LayoutInflater.Factory通过反射覆盖系统自身加载解析XML的LayoutInflater.Factory,目的是在解析创建view的时候能在LayoutInflater.Factory的onCreateView回调方法中通过参数view和AttributeSet拿到需要换肤的view和属性,然后包装对象缓存起来。触发换肤的时候,通过反射拿到AssetManager和Resource对象,根据资源名称、类型和包名加载apk皮肤包中的资源,遍历缓存的view和待换肤属性,重新赋值换肤的资源,调用invalidate刷新view。

插件式换肤原理 插件式换肤实践

10.Android事件分发

用户对屏幕的操作的事件可以划分为3种最基础的事件:
1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件

Android 的事件分发机制大体可以分为三部分 事件生产 事件分发 事件消费

事件分发的大概流程可以这样来描述:Activity -> PhoneWindow ->DecorView(DecorView其实就是一种ViewGroup) ->View,分发传递。

事件分发需要的三个重要方法来共同完成:

public boolean dispatchTouchEvent(event):用于进行点击事件的分发 public boolean onInterceptTouchEvent(event):用于进行点击事件的拦截 public boolean onTouchEvent(event):用于处理点击事件

image.png

事件继续分发需要调用super对应的方法。

事件分发详解

11.RecyclerView的缓存原理

RecyclerView的四级缓存:

  • 屏幕内部缓存:缓存没有与RecyclerView分离的ViewHolder和数据已经发生改变的ViewHolder(这是一级缓存)
// 一级缓存中用来存储屏幕中显示的ViewHolder
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// 数据发生改变的缓存viewHolder      
ArrayList<ViewHolder> mChangedScrap = null;
  • 二级缓存:用来缓存最近回收的ViewHolder,缓存复用时必须匹配position,这个集合里存的 ViewHolder 的原本数据信息都在,所以可以直接添加到 RecyclerView 中显示,不需要再次重新onBindViewHolder()。默认大小是2,最大的缓存大小默认为2个ViewHolder,可以通过Recycleview的setViewCacheSize方法进行设置缓存大小。
// 二级缓存中用来存储屏幕外的缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
  • 三级缓存:ViewCacheExtension给用户自定义实现的缓存,需要实现方法:
 public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
  • 四级缓存:RecyclerViewPool缓存已经跟RecyclerView分离的ViewHolder,获取的时候判断viewType,不做校验但是要重新设置数据调用onBindViewHolder方法。

12.Cache缓存队列的实现

LruCache与DisLruCache
Lru(Least Recently Used) 即最近最少使用算法,当缓存满时,会优先淘汰那些近期最少使用的缓存对象。(基于LinkedHashMap实现,对头放置最久未被使用的数据,队尾放置最近使用的数据,当缓存满时,从队头开始删除数据)
采用Lru算法的缓存有两种:LruCache 和 DiskLruCache,LruCache用于实现内存缓存,DiskLruCache用于实现存储设备缓存。

项目中的缓存使用: 在一些项目中通常会结合使用内存缓存和存储设备缓存,比如第一次从网络中获取到图片,将图片缓存到存储设备缓存和内存缓存,当再一次加载图片时,会首先从内存缓存获取查找需要的图片是否存在(从内存缓存获取图片的速度最快),如果没有,会到存储设备缓存获取查找需要的图片是否存在,如果没有,最后才再一次从网络获取图片。

LruCache: 内部采用 LinkedHashMap 以强引用的方式存储外界的缓存对象,提供 get 和 put 方法完成缓存的获取和添加。

  • 强引用:直接的对象引用。
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象才会被gc回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

使用LruCache实现内存缓存

private LruCache<String, Bitmap> mMemoryCache;

//设置缓存大小并计算bitmap大小
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
    }
};

//获取缓存对象
mMemoryCache.get(key);

//添加缓存对象
mMemoryCache.put(key, bitmap);
DisLruCache

DiskLruCache 的创建通过 open(File directory, int appVersion, int valueCount, long maxSize) 方法获取对象。

  • directory: 缓存目录路径。如果希望应用卸载后删除缓存文件,就选择sd卡上的缓存目录;如果要保留则选择sd卡上的其他特定目录。
  • appVersion: 设置成1即可。当版本号改变时,之前的缓存文件会被清空,但大多数时候版本号更改缓存文件仍然有效。
  • valueCount: 表示单个节点所对应的数据的个数,一般设为1即可。
  • maxSize: 缓存的总容量。
private DiskLruCache mDiskLruCache;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB

File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists) {
    diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
DiskLruCache的缓存添加

详细操作步骤:

  1. 通过图片的url将其转换成key(该key一般为md5,转为md5是防止url有特殊符号),通过key获取到Editor对象
  2. 通过Editor对象获取文件输出流,将图片写入缓存文件中
  3. 缓存完成后调用editor.commit()方法提交才正式写入为缓存,如果写入期间出现异常,调用editor.abort()回滚操作。
private static final int DISK_CACHE_INDEX = 0; //因为在DiskLruCache创建时open()方法中的valueCount设置为1,这里需要设置为0

String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
    if (downloadUrlToStream(url, outputStream)) {
        editor.commit();
    } else {
        editor.abort();
    }
    mDiskLruCache.flush();
}

//将网络下载的图片写入到缓存文件中
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {}

private String hashKeyFromUrl(String url) {
    String cacheKey;
    try {
        final MessageDigest mDigist = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch(Exception e) {
        cacheKey = String.valueOf(url.hashCode());
    }
    return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(OxFF & bytes[i]);
        if (hex.length == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

架构

MVVM

LifeCycler

LifeCycler讲解

订阅者模式: LifeCyclerOnwer、LifeCycler、LifeCyclerRegistry、LifeCyclerObserver

  • 在Activity 获取 Lifecycle,实际上是通过Activity的父类 ComponentActvitiy 获取,父类实现了 LifecycleOwner 接口,就能获取 Lifecycle ,最后注册 LifecycleObserver 就能拿到生命周期回调了。

LiveData

LiveData讲解 订阅者模式:

实时数据刷新:当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据;

不会发生内存泄漏:observer会在LifecycleOwner状态变为DESTROYED后自动remove;

不会因 Activity 处于STOP等状态而导致崩溃:如果LifecycleOwner生命周期处于非活跃状态,则它不会接收任何 LiveData事件;

不需要手动解除观察:开发者不需要在onPause或onDestroy方法中解除对LiveData的观察,因为LiveData能感知生命周期状态变化,所以会自动管理所有这些操作;

数据始终保持最新状态:数据更新时,若LifecycleOwner为非活跃状态,那么会在变为活跃时接收最新数据。例如,曾经在后台的 Activity 会在返回前台后,observer立即接收最新的数据等;

Android开源框架原理

ARouter

ARouter中主要的角色
  • Warehouse: 存储路由表信息,例如:path、path所对应的Class、拦截器、服务的实例

  • LogisticCenter: 负责任务调度处理,往Warehouse条虫路由信息

ARouter一次api调用的大致流程

路由api调用链路的大致流程: 获取ARouter单例对象传入path参数调用navigation,在navigation()中调用LogisticCenter.complete()对Postcord信息进行完善(因为在build的时候只是对PostCard赋值了context和path,complete这步是对Postcard携带的终点类class、拦截器等信息作完善),判断是否需要需要经过拦截器,最终根据PostCard携带的type信息做相应的处理:如果是Activity类型,则构建Intent进行跳转;如果是Provider类型,则获取Provider实例进行返回;如果是Fragment/Brocast类型,则通过反射构造实例进行返回。

如何给仓库Warehouse中的路由映射表填充信息的?
    1. 在ARouter初始化的时候,init方法中根据特定的包名遍历dex中的arouter注解生成的类,通过反射得到实例调用loadInto()方法将路由信息加载到Warehouse的路由映射表中。
    1. 使用插件的方式,在编译的过程中根据报名扫描dex得到特定的全路径包名,在LogisticCenter的loadMapInfo()方法中进行注册。在ARouter.init()初始化的时候调用loadMapInfo()通过反射得到实例调用loadInto()将路由信息加载到WareHouse的路由映射表中。
AutoWire是如何给变量赋值的?

AutoWire是用来给修饰的变量赋值的,在编译期间通过APT注解处理器生成AutoWire的辅助类,辅助类里边通过获取intent中的值对Autowire修饰的变量进行赋值。(前提是要在对应的类里边调用inject方法)

跨组件方法是如何调用的,原理
A module使用B module下的一个弹框组件,A和B均依赖arouter
A关注自己想要的,在下沉arouter组件中定义一个接口继承IProvider,让B去具体实现

用注解标识的IProvider实现类,在APT处理的时候生成辅助类构造成RouteMeta存放在Warehourse中,在使用的时候通过class获取到服务的实例,然后调用跨组件的方法。

protected <T> T navigation(Class<? extends T> service) {
    try {
        Postcard postcard = LogisticsCenter.buildProvider(service.getName());

        // Compatible 1.0.5 compiler sdk.
        // Earlier versions did not use the fully qualified name to get the service
        if (null == postcard) {
            // No service, or this service in old version.
            postcard = LogisticsCenter.buildProvider(service.getSimpleName());
        }

        if (null == postcard) {
            return null;
        }

        LogisticsCenter.completion(postcard);
        return (T) postcard.getProvider();
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());
        return null;
    }
}

OkHttp

OkHttp详解

  • 一 请求与响应流程

    • 1.1 请求的封装
    • 1.2 请求的发送
    • 1.3 请求的调度
  • 二 拦截器

    • 2.1 RetryAndFollowUpInterceptor
    • 2.2 BridgeInterceptor
    • 2.3 CacheInterceptor
    • 2.4 ConnectInterceptor
    • 2.5 CallServerInterceptor
  • 三 连接机制

    • 3.1 建立连接
    • 3.2 连接池
  • 四 缓存机制

    • 4.1 缓存策略
    • 4.2 缓存管理

发起请求的示例:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .build();
Request request = new Request.Builder()
        .url(url)
        .build();
okHttpClient.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});

image.png

public final class Dispatcher {
    
      private int maxRequests = 64;
      private int maxRequestsPerHost = 5;
    
      // 准备异步执行的任务队列
      private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
    
      // 正在异步执行的任务队列
      private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
    
      // 正在同步执行的任务队列
      private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
      
      // 同步执行的方法
      synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
      }

      // 异步任务执行调度
      synchronized void enqueue(AsyncCall call) {
      //正在运行的异步请求不得超过64,同一个host下的异步请求不得超过5个
      if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
      } else {
        readyAsyncCalls.add(call);
      }
    }
}

Dispatcher是一个任务调度器,它内部维护了三个双端队列:

  • readyAsyncCalls:准备运行的异步请求
  • runningAsyncCalls:正在运行的异步请求
  • runningSyncCalls:正在运行的同步请求

记得异步请求与同步骑牛,并利用ExecutorService来调度执行任务。

OkHttp中最重要的是责任链模式

请求的处理

final class RealCall implements Call {
      Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        //这里可以看出,我们自定义的Interceptor会被优先执行
        interceptors.addAll(client.interceptors());
        //添加重试和重定向烂机器
        interceptors.add(retryAndFollowUpInterceptor);
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        interceptors.add(new CacheInterceptor(client.internalCache()));
        interceptors.add(new ConnectInterceptor(client));
        if (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));
    
        Interceptor.Chain chain = new RealInterceptorChain(
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);
      }
}
  • RetryAndFollowUpInterceptor:负责重定向。

  • BridgeInterceptor:负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。

  • CacheInterceptor:负责读取缓存以及更新缓存。

  • ConnectInterceptor:负责与服务器建立连接。

  • CallServerInterceptor:负责从服务器读取响应的数据。

缓存策略

image.png

HTTP的缓存可以分为两种:

  • 强制缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。
  • 对比缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存 标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。

EventBus

private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();
private static final Map<Class<?>, List<Class<?>>> eventTypesCache = new HashMap<>();

private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
private final Map<Object, List<Class<?>>> typesBySubscriber;
private final Map<Class<?>, Object> stickyEvents;

使用
EventBus.getDefault().register(this);
EventBus.getDefault().post("");

// 订阅的时候注册事件

// 当前的类与订阅方法的缓存集合
Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();

// 缓存订阅的事件类型和订阅的方法
Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;

// 缓存订阅类和订阅事件类型
Map<Object, List<Class<?>>> typesBySubscriber;

register()方法主要分为查找注册两部分,先从缓存METHOD_CACHE中查找,如果找到则直接返回,没有找到则进入findUsingInfo方法通过反射查找订阅的事件方法,返回并缓存。 注册:在订阅类中查找到对应的订阅类型,添加到缓存集合中。在发送订阅事件的时候,通过订阅事件类型获取到订阅方法集合,遍历调用订阅方法。 注销:通过订阅类从缓存中拿到订阅事件类型集合,遍历订阅事件集合,从缓存中拿到订阅方法集合,遍历移除缓存的订阅方法。

热修复Tinker

热修复

  • 代码修复
  • 资源修复
  • 动态库连接修复

代码修复有三种方案:

1、底层替换方案:阿里系为主。不会再次加载新类,而是直接native层修改原有类。 2、Instant Run方案:基于字节码操作框架AMS底层实现。为每个方法添加一些代码,来记录方法是否有变化,有变化时就生成替换类。 3、类加载方案:腾讯系为主。基于类加载机制与dex分包方案。

Android ClassLoader回顾

1、ClassLoader是一个抽象类,BaseDexClassLoader 继承自ClassLoader,是抽象类ClassLoader 的具体实现类,PathClassLoader、DexClassLoader都继承自BaseDexClassLoader 。 2、PathClassLoader 、DexClassLoader源码中无任何自己的实现,继承自BaseDexClassLoader,方法都在父类BaseDexClassLoader 中实现。 3、PathClassLoader 只能加载安装在 Android 系统内 APK 文件(/data/app 目录下),其他位置的文件加载时都会报 ClassNotFoundException。因为 PathClassLoader 会读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的,没有安装的时候,自然没有生成这个文件。 4、DexClassLoader 可从任意目录加载 jar、apk、dex。是实现代码热修复核心

Java层热修复的核心原理 类加载方案基于dex分包方案,我们将有bug的类A.class 进行修改,然后再将A.class打包成dex补丁包Patch.dex放到Element[] 的首个元素中。 在类加载时首先会找到Patch.dex这个补丁包中的A.class文件,然后加载。后续的有bug的dex文件中的A.class根据双亲委派机制不会被加载了。 这时候 带有 bug 的 class 就算被 “修复” 了。

Glide四级缓存

Glide四级缓存的讲解 Glide分为四级缓存,分别为activecache,memorycache,diskdatacache,diskresourcecache。

  • activecache 为活动缓存,缓存正在使用的图片;
  • memorycache 为内存缓存;
  • diskdatacache 为磁盘缓存的原图片缓存;
  • diskresourcecache 为磁盘缓存的处理后图片缓存。

其中前两者为内存中的缓存,后两者为磁盘中的缓存。

activecache为面向用户展示的缓存,该级缓存是通过弱引用维护,然后保存在hashmap中。

取:当APP需要请求图片时,首先从该缓存中根据key查询,如果查询得到,那么将该图片的引用次数acquire加1,并将查询得到的图片展示给用户;

清:当用户滑动页面该图片为不可见状态或者页面调用ondestroy方法销毁时,会调用onrelease()方法从activecache中释放该图片。此时会将图片的引用acquire减1,如果acquire<=0说明当前页面已经不引用该图片,那么该图片会从activecache中删除,并存入memorycache中。

存:当用户请求某图片时,如果四级缓存中都未获取到,那么会进行网络请求,请求成功后,会将图片缓存至activecache中。

memorycache:如果用户请求图片在activecache中没找到,那么会继续往memorycache中查找。如果找到该图片,那么返回该图片,并在memrorycache中删除该图片,但在返回之前会给该图片引用acquire加1记录图片引用次数,并且将该图片放进activecache中。该级内存缓存运用了lrucache的原理。如果缓存满了,用最少使用算法清除最不常用的图片(根据图片被引用次数acquire清除)。

对于磁盘缓存,比较灵活可以根据需求自己设置:调用diskCacheStrategy(DiskCacheStrategy.NONE)其中参数有四种:all,none,data,resource,autonatic。 all:表示即缓存原始图片也缓存转换处理过后的图片; none:不进行磁盘缓存; data:只缓存原始图片; resource:只缓存转换处理过后的图片; automatic:默认策略,如果是远程图片,则只缓存原始图片;如果是本地图片,那么只缓存转换过后的图片。 根据以上总结,总的来说只有两种磁盘缓存方式,1.原始图片缓存,2.处理后的图片缓存。并且这两种可以混合使用。 底部原理是采用DiskLruCache做的。

MMKV

SharedPreference缺点

  1. 跨进程不安全
  2. 加载缓慢:异步加载,但是异步加载线程没有设置优先级,如果这时候主线程读取数据需要等待加载线程执行完毕(也就是主线程等待低优先级线程锁的问题)
  3. 全量写入:无论是commit还是apply,即使改动一个条目,也会把全部内容写到文件
  4. 卡顿:异步落盘机制在应用崩溃时会导致数据丢失

SP优化

可以在Application中重写getSharedPreference方法,返回自己实现的sp。我们可以自己将多次读写进行合并

MMKV 通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

数据组织

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double 等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。

写入优化

标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。

空间增长

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

数据有效性

考虑到文件系统、操作系统都有一定的不稳定性,我们另外增加了 crc 校验,对无效数据进行甄别。在 iOS 微信现网环境上,我们观察到有平均约 70万日次的数据校验不通过。

LuBan图片压缩

Android性能优化

启动优化

1.主线程优化:

  • MultiDex优化
  • ContentProvider优化: ContentProvider的onCreate方法在Application的onCreate方法之前执行,可将在应用启动时候需要同步初始化的任务提到ContentProvider的onCreate方法里边进行初始化。(如果当前应用中有多个自己的ContentProvider,可引入JetPack组件StartUp针对ContentProvider进行合并优化)
  • 任务调度分类: 懒加载的任务可以放到使用的时候再去初始化。例如:短信SDK的初始化
  • Activity优化: 在Activity中不要做同步阻塞任务,将任务可以放到异步执行;必要的同步阻塞任务可以放到Handler的idelCallback中执行。
  • UI优化:
  1. 首页布局通过代码组合View的方式构建布局,添加到DecorView节省XML布局加载解析的时间。
  2. 异步预加载方案:在后台线程提前将这些 view 加载到内存,在 measure 阶段再直接从内存中进行读取。
  • 主线程消息调度: 提高启动场景页面相关消息的优先级。

2.后台任务优化:

减少后台线程任务:

  • 减少后台线程不必要的任务的执行,特别是一些重 CPU、IO 的任务;
  • 对启动阶段线程数进行收敛,防止过多的并发任务抢占主线程资源,同时也可以避免频繁的线程间调度降低并发效率(减少线程数,使用线程池)。

GC抑制:

  • 启动阶段如果频繁的GC会严重影响启动速度,减少启动阶段代码的执行和内存的申请。

双亲委派机制:

  1. 首先从已加载类中查找,如果能够找到则直接返回,找不到则调用 parent classloader 的 loadClass 进行查找;
  2. 如果 parent clasloader 能找到相关类则直接返回,否则调用 findClass 去进行类加载;

ByteX 抖音的字节码开源框架

H5加载优化

image.png

优化由客户端和H5端协同完成。

  • 在空闲时,提前初始化好WebView,并使用webView复用池进行缓存。
  • HTML文档进行预加载,给HTML更新的时间做一个约定,当本地缓存的HTML过期时候去加载新的HTML文件缓存到本地,在页面关闭的时候去开启加载HTML任务。 分场景,在页面空闲的时候去做HTML的预加载,在HTML页面关闭的时候去校验HTML文档的过期时间是不是小于设定的阈值,如果是,则去预加载更新HTML文档。
  • 预请求HTML:配合路由组件使用,在路由跳转的过程中拦截判断目标页面,提前开启请求加载HTML的任务。
  • 将webView.loadUrl()以及接口数据请求进行前置,在原生view Inflate的时候并行执行。
  • 在加载图片的时候,将图片交给图片加载框架执行,转成字节流缓存,然后交给WebView渲染。

apk瘦身优化

首先就是要分析Apk的组成,通过Analyze app分析出来的图片(打开方式:Android Studio下 ——> Build——> Analyze app): image.png

文件/目录描述
lib/存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可,如果非必需,可以考虑拿掉x86的部分
res/存放编译后的资源文件,例如:drawable、layout等等
assets/应用程序的资源,应用程序可以使用AssetManager来检索该资源
META-INF/APK中所有文件的签名摘要等信息
classes.dexclasses文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc编译后的二进制资源文件
AndroidManifest.xml清单文件
优化方案

动态库优化: 在Android系统中,每一种CPU架构对应一种ABI,主要有:arm64-v8a(最新),armeabi,armeabi-v7a,x86,x86_64,mips,mips64。现在我们只需要配置armeabi-v7a即可:

android{
    defaultConfig{
        ndk{
            abiFilters "armeabi-v7a"
        }
    }
}
国际化资源优化
android{
    defaultConfig{
        // 只适配需要的区域
        resConfigs 'zh'
    }
}
代码压缩/代码混淆

配置代码压缩和混淆,在主module的build.gradle里配置minifyEnabled 为true,即压缩了代码,也混淆了代码:

buildTypes{
    release{
        // 1、是否进行混淆
        minifyEnabled true
        // 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
        zipAlignEnabled true
        // 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
        // 这些代码所引用的资源也会被标记为无用资源,然后
        // 系统通过资源压缩功能将它们移除。
        // 需要注意的是目前资源压缩器目前不会移除values/文件夹中
        // 定义的资源(例如字符串、尺寸、样式和颜色)
        // 开启后,Android构建工具会通过ResourceUsageAnalyzer来检查
        // 哪些资源是无用的,当检查到无用的资源时会把该资源替换
        // 成预定义的版本。主要是针对.png、.9.png、.xml提供了
        // TINY_PNG、TINY_9PNG、TINY_XML这3个byte数组的预定义版本。
        // 资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
图片压缩与更换格式

可以更换图片格式,比如webp,svg可以更小,android studio也提供了对应的支持,但是没有最好的格式,只是适用场景不同。图片压缩网站推荐squoosh.app/。1.12M png图片,选择webp格式后,优化后大小39.6 kB

网络图片优化

很多入口较深的高清大图,或者需要经常更新的图片,也许用户根本不看,就没有必要内置在apk中,看时加载即可,如果需要提前占位置,可以用缩略图代替,至于哪些图网络化,需要根据业务与用户体验来权衡。

无用资源优化

通过Android Studio提供的工具检索出项目未被使用的代码、资源。但删除的时候不能一键清除,因为java类有可能是通过反射来调用的,比如项目里有跟JS交互的相关java类,IDE提示未被使用,但实际有被用。还有,资源也可以动态获取来使用,比如getResources().getIdentifier(“name”,“defType”,getPackageName()),如果某个资源仅存在动态获取资源id的方式,那么这个资源也会被检测为未被使用。所以,AS提供的一键清除所有无用资源要慎重:

5ba3b04b7a60485cbf904703da897f0f.png

image.png 在使用AS的工具检测出无用资源的时候,最好手动一个个排查资源是否有使用,避免误删。

可以使用Lint来剔除无用资源和无用代码:

image.png

image.png

image.png

image.png

三方库处理

选择第三方 SDK 或三方库的时候,可以将包大小作为选择的指标之一, 或只引入部分需要的代码针对库做业务裁剪,而不是将整个包的代码都引入进来。

混合开发,动态化方案

比如一些模块可以使用H5的方式实现,H5的资源通过网络动态下载。

内存优化

内存优化

image.png

  • 善用线程池。
  • 善用对象池复用对象。
  • Bitmap图片资源压缩,用完及时回收释放内存。
  • 注册器、数据库游标记得及时回收。

内存优化的好处:

  • 1.防止应用发生OOM。
  • 2.降低应用由于内存过大被LMK机制杀死的概率。
  • 3.避免不合理使用内存导致GC次数增多,从而导致应用发生卡顿。

内存的问题

    1. 内存抖动

频繁发生GC的时候内存波动图形呈 锯齿张GC导致卡顿

这个问题在 Dalvik虚拟机 上会 更加明显,而 ART虚拟机 在 内存管理跟回收策略 上都做了 大量优化内存分配和GC效率相比提升了5~10倍,所以 出现内存抖动的概率会小很多

  • 2. 内存泄漏

Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。简言之,就是 对象被持有导致无法释放或不能按照对象正常的生命周期进行释放。一般来说,可用内存减少、频繁GC,容易导致内存泄漏。(生命周期短的对象被生命周期长的对象持有,导致无法被释放)

内存泄漏的场景的避免的方式

1. handler造成的内存泄漏: 使用静态内部类+WeakReference包装强引用类型,在生命周期销毁的方法里边清空handler消息回调,并且置为null。

单例模式造成的内存泄漏(单例的静态特性导致其生命周期同应用一样长)

  1. 原因:单例持有Activity、Service的Context,持有View

解决方案:如果传入Context,使用ApplicationContext。使用弱引用的方式持有View

内部类或者匿名内部类造成的内存泄漏:

解决方案:

  1. 将内部类编程静态内部类

  2. 如果有强引用Activity中的属性,即将该属性的引用方式改为弱引用

  3. 在业务允许的情况下,当Activity执行onDestroy()方法的时候,结束耗时任务(如匿名的Thread)

Activity Context的不正确使用

静态变量持有Activity的引用,静态变量生命周期大于Activity的生命周期导致Activity无法被回收,而造成内存泄漏。

解决方案:

  1. 使用ApplicationContext代替ActivityContext,因为ApplicationContext生命周期与应用相同,不依赖Activity的生命周期

  2. 对Context的引用不要超过它本身的生命周期,慎重的对Context使用static关键字。Context里如果有线程,一定要在onDestroy()里及时停掉

注册监听器的泄漏:

系统服务可以通过Context.getSystemService获取,他们负责执行某些后台任务,或者为硬件访问提供接口。如果Context想要在服务内部发生事件是被通知,呢就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity的引用,如果在Activity onDestroy()时没有释放掉引用就会造成内存泄漏。

解决方案:1. 使用ApplicationContext代替ActivityContext

  1. 在Activity执行onDestroy()时,调用反注册

Cursor、Streame没有close,View没有recyle

集合中对象没有清理造成的内存泄漏。

WebView造成的泄漏。

解决方案:不用时候销毁

避免内存抖动

原因:大量的对象被创建又在短时间内马上被释放(频繁GC)

避免在循环中创建临时对象

避免在onDrow中创建Paint、bitmap对象

使用优化过的数据结构

HashMap,ArrayMap,SparseArray源码分析及性能对比

编程语言提供的某些类未针对在移动设备上使用进行优化。例如,通用 HashMap实现的内存效率可能非常低,因为每个映射都需要单独的入口对象。

Android框架包括若干优化的数据的容器,其中包括 SparseArray,SparseBooleanArray,和LongSparseArray。例如,这些SparseArray类更高效,因为它们避免了系统需要自动 复制 密钥并且有时需要值(这会为每个条目创建另一个对象或两个对象)。

    1. 内存溢出

像内存申请一块内存空间,内存不足以给到的时候就会发生内存泄漏。 除了因内存泄漏累积到一定程度导致OOM的情况以外,也有一次性申请很多内存,比如说 一次创建大的数组或者是载入大的文件如图片的时候会导致OOM。而且,实际情况下 很多OOM就是因图片处理不当 而产生的。

内存分析工具

Memory Profiler

  • 1)、实时图表展示应用内存使用量
  • 2)、用于识别内存泄漏、抖动等
  • 3)、提供捕获堆转储、强制GC以及根据内存分配的能力

优点

  • 1)、方便直观
  • 2)、线下使用

Memory Analyzer

强大的 Java Heap 分析工具,查找 内存泄漏及内存占用, 生成 整体报告分析内存问题 等等。建议 线下深入使用

LeakCanary

自动化 内存泄漏检测神器。建议仅用于线下集成

它的 缺点 比较明显,具体有如下两点:

  • 1)、虽然使用了 idleHandler与多进程,但是 dumphprof 的 SuspendAll Thread 的特性依然会导致应用卡顿
  • 2)、在三星等手机,系统会缓存最后一个Activity,此时应该采用更严格的检测模式
Android内存管理机制回顾

1、Java 内存分配

Java的 内存分配区域 分为如下 五部分

  • 1)、方法区:主要存放静态常量
  • 2)、虚拟机栈:Java变量引用
  • 3)、本地方法栈:native变量引用
  • 4)、堆:对象
  • 5)、程序计数器:计算当前线程的当前方法执行到多少行

内存都懂常见案例:

下面列举一些导致内存抖动的常见案例,如下所示:

1、字符串使用加号拼接

  • 1)、使用StringBuilder替代
  • 2)、初始化时设置容量,减少StringBuilder的扩容

2、资源复用

  • 1)、使用 全局缓存池,以 重用频繁申请和释放的对象
  • 2)、注意 结束 使用后,需要 手动释放对象池中的对象

3、减少不合理的对象创建

  • 1)、ondraw、getView 中创建的对象尽量进行复用
  • 2)、避免在循环中不断创建局部变量

4、使用合理的数据结构

使用 SparseArray类族、ArrayMap 来替代 HashMap

而对于 内存泄漏的分析 一般可简述为如下 两步

  • 1)、使用 Memory Profiler 初步观察
  • 2)、通过 Memory Analyzer 结合代码确认
搭建系统化的图片优化和监控机制(这个很有用)

Lint代码静态检测

性能优化工具

Profile

Systrace

常用数据结构

多线程编程

Flutter

简历中的案例

App强更方案

得物App的Crash率

得物App的内存泄漏率

得物app代码优化MVP改成MVVP

玩物得志社区首页Feed流场景

作者:看书的小蜗牛

Android性能监控调试开源示例

Matrix

最终目的是为了自研APM