Android 高级开发进阶图谱

Posted xhmj12

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 高级开发进阶图谱相关的知识,希望对你有一定的参考价值。

被毁约+幸运避开裁员后成功上岸!

上一篇:聊一聊阿里P8、P9及以上人的水平

/   概述   /

随着android系统的迭代更新和开源api的强大,相信大部分开发者技术的瓶颈很难突破。开源的力量已经把技术深入到api中,所以我想跟大家分享一下万物之本和其演变的过程。

首先来讲一下思路,设计的领域要广,然后求精(深入这个东西学习的是思路,要恰到好处的学到精髓)。下面来陈列一下大纲:

  • 引用模式

  • 数据结构

  • 数学之Math

  • 进程

  • 线程

  • 高并发

  • 冷启动流程

  • activity视图

  • 自定义view

  • kotlin协程

  • 图片内存管理

  • 注解

  • oom

  • anr

/   引用模式   /

引导:在很早之前引用模式只有引用和未引用两个,jvm会帮助我们回收垃圾,后面为了更好的垃圾回收确认对象,衍生了以下四个等级。

Object obj = new Object();

强引用:情愿抛终止程序异常也不会回收此对象;会把强引用放到堆内存;

new Handler(new WeakReference<>(activity));

弱引用:jvm会不定时gc,当gc则会回收;

new Handler(new SoftReference<>(activity));

软引用:当内存不足时才会回收;

new Handler(new PhantomReference<>(activity));

虚引用:不确定回收时间,不常用。

问题1:android给每个app分配的默认内存是18m,我们项目代码大部分都是用强引用,为什么我们的app没有闪退?

答案:在没有使用的时候和不存在相互绑定的对象则同样会触发gc回收。

拓展:jvm内部有一个区域单独累加对象使用次数,常用的handler因为持有activity故无法回收activity导致内存泄露进而引起内存溢出。

  • 当activity被销毁时隔一段时间会触发gc,如果是强引用的话activity和handler相互引用则会导致jvm回收垃圾认为他还有用,则不会回收。

  • 静态类:如果是非静态类,默认间接引用外部类activity;静态类和外部类无联系,执行顺序最早;gc会随着外部类销毁而释放静态类。

以下是解决方法:

private MyHandler handler = new MyHandler(new WeakReference<>(this));
  private static class MyHandler extends Handler 
        private final WeakReference<Test> activity;
        private MyHandler(WeakReference<Test> activity) 
            this.activity = activity;
        
        @Override
        public void handleMessage(@NonNull Message msg) 
            Test page = activity.get();
            if (page != null) 
                switch (msg.what) 
                    case 1:
                        page.refresh();
                        break;
                    default:
                        break;
                
            
        
    

这时候你就觉得对引用模式的认识就结束了吗?不,并没有!

问题2:我以前在做一个需求的时候发现共享元素动画在来回点击的时候竟然偶尔不执行动画,这时候无意间打印了onDestory的调用竟然延迟了5s!

解答:主线程是单线程队列的,当任务过多可能会延迟执行;由此推出mvp会内存泄露,是不是很惊讶!

下面是解绑view层的mvp代码:

override fun onDestroy() 
        super.onDestroy()
        mPresenter?.let 
            lifecycle.removeObserver(it)
            it.detachView()

        
    
  override fun detachView() 
        mView = null
    

问题3:内存泄露一直是个开发中经常遇到的问题,如何检测内存泄露,大家可能会说使用bugly或者LeakCanary,那么其中的原理是什么?

引导:application注册registerActivityLifecycleCallbacks会监听所有activity的生命周期,你可能会觉得这跟引用模式和内存泄露有毛关系?

当activity走onDestory时候添加到一个弱引用activity队列中,过一段时间触发gc,如果弱引用不为空,则这个activity未销毁,则判定为内存泄露,这时候新开一个进程对泄露的activity进行判空,即可定位对应位置;

/   数据结构   /

数组

  • size固定

  • 只能使用基本类型

  • 速度快,占用内存小

集合

ArrayList 底层由数组构成,当需要扩容的时候,动态设置size,并使用Arrays.copy(list,size);同时ArrayList不是线程安全的。

SparseArray是android特有的一个集合类,由数组+数组构成,采用二分查找法查询,当数据量小的使用建议使用SparseArray,数据量大的时候建议ArrayList。

HashMap 由数组+链条组成,参数有长度16和扩容临界率0.75f,首先长度只能是2的n次方,如果参数传进去的是5那么会变成8。下面两种遍历方法最快也是常用的。

Map<Integer,String> map = new HashMap<Integer,String>(16,0.75f);

 for (Map.Entry<String, Integer> entry : map.entrySet()) 

            System.out.print("key: " + entry.getKey() + " value: " + entry.getValue() + " \\n");

        



 Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();

 while (iterator.hasNext()) 

            Map.Entry<String, Integer> item = iterator.next();

            System.out.print("key: " + item.getKey() + " value: " + item.getValue() + " \\n");

        

哈希碰撞:key的最终都会转换成long类型,(long=33)/16=2…1 ,那么这个值会放到第一个链条中,链条中是一个数组,依次类推,16*0.75f=12,当链条12个都存在非空数组,则会扩容。实际上(n-1)&hash,偶数和奇数,使用奇数通过二进制计算并让哈希碰撞均匀分配到每个链表上。

LinkedHashMap 存的时候慢,读取的时候快。

Map<Integer,String> map= new LinkedHashMap<>(16,0.75f,true);

LinkedHashMap采用双向链表+二分查找实现,第三个参数和hashmap不同,false读的时候和写的时候顺序相同,true则会采用使用后移,那么不经常使用的就移到了最前面,最常用的LRUCache继承的就是LinkedHashMap。

二叉树 :抽象类思想,当层级关系达到8就会变成红黑树;可理解链条和二三叉树去联想;

链条:抽象思想,数组+Object

队列:后进后出

Stack<Integer> sList = new Stack<>();

栈:先进先出,类似于activity栈原理。

Queue<Integer> list = new LinkedList<>()

/   数学之Math   /

  • 绝对值 abs

  • 随机数 random [0,1)

(int) (Math.random() * 3);//从0到3的整数

 Math.random()+3;//从3到4的double值
  • 最大/小值 max/min

  • 圆周率 PI

  • sin/cos 直角三角sin a=a/c cos a=b/c

在java中sin a中的a是弧度,在数学中:1弧度=PI/180。

sin(30)=sin(30*PI/180)
  • n次方 pow(x,n次方)

  • 取整

java中的取整并不是四舍五入,分为以下两种:

  • Math.floor(5.6f)=5;

  • Math.ceil(5.4f)=6;

四舍五入:Math.floor(x+0.5)。

平方/立方根 sqrt/cbrt。

/   进程   /

概念:android系统会给每个app分配一个进程。

进程的优缺点

优点

进程间是相互隔离的,相当于沙盒,故闪退不会影响彼此,可以给自己app添加一个子进程守护容易闪退的页面,例如融云在app中就是一个单独的进程。

缺点

进程间通信困难,需要ibinder+aidl导致代码复杂度,两个进程要有交互方法,application多次创建。

拓展:ipc通信有共享内存,IBinder,Socket三种通信模式,共享内存最快,为什么使用IBinder?

解答:IBinder一次拷贝,Socket二次拷贝,共享内存当遇到多线程并发或并行修改和读会导致错乱,当然也可以使用线程安全,可是这样就极大的增加了难度;Socket性能低,IBinder还可以保证安全,同时接口一目了然;自然Ibinder获胜。

/   线程   /

首先先来看下面一张图:

  • handler:android ui主线程和子线程信使跑腿

  • looper :线程中间者,loop开始循环,会循环取messageQueue中的消息体给handler使用;不使用需要用quit退出循环;

看下面代码循环并没有堵塞线程,只是return挂起。

for (;;) 

            if (!loopOnce(me, ident, thresholdOverride)) 

                return;

            

        
  • messageQueue:是一个容器队列

  • handlerthread:把looper和Thread进行封装,则减少代码不规范和线程导致的阻塞

我们看出HandlerThread继承的就是Thread,得出新开了子线程;同时拓展了quitSafely安全退出循环;我们上面知道looper循环的时候return掉了,故mTid(线程id)不会赋值为-1。

public class HandlerThread extends Thread 
  @Override
    public void run() 
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) 
            mLooper = Looper.myLooper();
            notifyAll();
        
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    
    

handler延时

下图是MessageQueue.next代码,我们发现如果下个message如果不是延时,则会移动到第一个执行,从而避免了线程堵塞影响其他消息体。

问题:synchronized中的时间计算会不会导致多个延迟消息不会在指定时间执行?for循环会并发吗?

答案:会按指定时间执行,nextPollTimeoutMillis 和ptr会间隔设置成0,通过底层时间计算,synchronized会把队列消息重新排序,并赋值时间nextPollTimeoutMillis给nativePollOnce NDK方法进行指定时间回调,然后for循环会不会并发,后面再讲述。

android线程为什么是单线程?

多线程不方便管理,单线程方便统一管理。

当需要高并发处理ui该怎么做?

大家都知道哔哩哔哩的弹幕库吧,那么多弹幕也没有卡顿,同时显示了所有的弹幕。

SurfaceView是视频播放的显示控件,视频是一帧一帧的变化的很快,游戏也是画面改变很快的,同样没有卡顿。

大致了解了一下SurfaceView为了大量处理ui诞生的,他新开了一个线程,用来单独处理ui更新;通过结合弹幕库看出多个自定义view汇总绘制整块动画,点击的时候获取到对应的坐标。同时涉及到ndk层,这个自己能力有限无法详细讲述

线程池:ThreadPoolExecutors(核心线程数,最大线程数,临界处理) 这里临界处理默认是报异常的。

  • 正常:这里只开了两个窗口办理业务,当客户来办理时只能在这两个窗口办理业务,人越来越多,就会在候客区等待,依次办理业务。

  • 爆满:当客户很多时,那么银行就会开启其他几个窗口,当窗口人数已满,候客区也满了,这时候还有客户来办理业务,那么银行就会采取拒绝措施(这里只是虚拟,正常银行不会这么干,只是客户自己会选择离开)

线程的正确使用

  1. 引用模式避免泄露

  2. 合理分配使用线程池充分利用cpu

  3. 切换线程是非常浪费性能的

/   高并发   /

下面是一个单例模式,涉及到高并发,先来分析一下为什么这么写?

volatile 保证了可见和有序性,并禁止指令重排序(对象是

volatile修饰的参数),getInstance双重验证保证了方法原子性和指令重排序。

public class Singgle 
    private static volatile Singgle singgle; //volatile 可见性和有序性 可以防止指令重排序 同时有相互联系也不会重排序

    private Singgle() 

    

    /**
     * synchronized 原子性
     * @return
     */
    public static Singgle getInstance() 
        if (singgle == null) 
            synchronized (Singgle.class) 
                if (singgle == null) singgle = new Singgle();
            
        
        return singgle;
    

    public String getName() 
        return "flower";
    

这里是cpu双核+内存条,单核由高速缓存+处理器组成,可以来思考一下为什么和高并发有关?

引言:高并发常见的三个特征,希望结合上面两个实例和下面三个特征分析。

  • 原子性:只有一个操作

  • 有序性:按照顺序执行

  • 可见性:两个人在改一个东西 另外一个立马可见

  • 指令重排序:代码经过编译器和处理器分析后会对代码执行顺序进行排序,原则是没有关联,不影响正常运行

问题1:for循环内部是并发/行的吗?

解答:是并发/行的,以下代码会偶现出现日志乱序,你可能会觉得日志是不是打印错了,那你多打印几次试一下。

for (int i = 0; i < 10000; i++)  
 System.out.print(num++ + "\\n");

问题2:如何拓展解决并发?现在看以下示例

示例1:可寓意为服务器给前台发了1w条消息。ReentrantLock是可重入/递归锁,处理器识别为递归和开始/结束点即可正确执行。

private Lock lock = new ReentrantLock();
  private int num = 0;
 for (int i = 0; i < 10000; i++) 
            lock.lock();
            try 
                num++;
                System.out.print(num + "\\n");
             finally 
                lock.unlock();
            
        

newSingleThreadContext是作用域,即告诉处理器我这个方法体是不能线程并发的,repeat里也是一个作用域,repeat内部实现了有序性,从而保证了原子性和有序性;有序性则说明变量num是在修改完毕后才执行下一次循环,避免了可见性。

val countContext = newSingleThreadContext("countContext")
            GlobalScope.launch(countContext) 
                withTimeoutOrNull(100) 
                    repeat(1000) 
                        num++
                        Log.e(TAG, "onCreate: $num")
                        handler.sendMessageDelayed(Message.obtain().apply 
                            data.putString("num", num.toString())
                        , 100)
                    
                    countContext.close()
                
            

必要情况使用atomic高阶函数实现高并发。

/   冷启动流程   /

ActivityThread.main是程序的入口。

public static void main(String[] args)
    ...
    Looper.prepareMainLooper(); 
    //初始化Looper
    ...
    ActivityThread thread = new ActivityThread();
    //实例化一个ActivityThread
    thread.attach(false);
    //这个方法最后就是为了发送出创建Application的消息
    ... 
    Looper.loop();
    //主线程进入无限循环状态,等待接收消息
  • looper 死循环收消息

  • H extend Handler 重写拓展类

内存通知

绑定application

gc回收

启动第一个activity

解绑application

  • ApplicationThread

  • handleLaunchActivity

  • main流程

looper初始化

new activityThread

attach =>使用IBinder初始化ApplicationThread 发送消息到H=>反射生成application&绑定application=>发送消息到H 反射获取启动开启页=>aciitvity.attach

looper.loop开启循环

冷启动具体流程链接:

https://www.jianshu.com/p/9ecea420eb52

/   Activity视图   /

activity.dispatchTouchEvent=>ViewGroup.dispatchTouchEvent=>View.dispatchTouchEvent

问题:点击穿透如何形成的?

答案:分发事件是一层一层下发的,dispatchTouchEvent返回boolean,如果未被消费false则会下发到下一层级,需要检查触摸事件和点击焦点。

//Activity
 public boolean dispatchTouchEvent(MotionEvent ev) 
        if (ev.getAction() == MotionEvent.ACTION_DOWN) 
            onUserInteraction();
        
        if (getWindow().superDispatchTouchEvent(ev)) 
            return true;
        
        return onTouchEvent(ev);
    
     public void onUserInteraction() 
    
      public boolean onTouchEvent(MotionEvent event) 
        if (mWindow.shouldCloseOnTouch(this, event)) 
            finish();
            return true;
        

        return false;
    
//Window
 /** @hide */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) 
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) 
            return true;
        
        return false;
    

上面代码我们看到onUserInteraction是空的

所以每次按下事件activity都会通过onUserInteraction拓展给我们使用;根据第二个代码块看出同时activity是可以作为弹窗的,第一个由activity消费,后面viewgroup根据子view分配点击事件。

/   自定义View   /

下面是一个三阶贝塞尔曲线绘制的水波浪,采用属性动画平移+绘制实现:

public class WaterView extends View 
    private Path path;
    private Paint paint;
    private int width;
    private int height;
    private int number = 2;
    public float oneWidth = 600f;
    public int oneHeight = 100;
    private List<PointF> list;
    private ValueAnimator animator;
    private float mCurrentScale = 0f;
    private Path criclePath;
    private Rect rect;
    private RectF rectF;


    public WaterView(Context context) 
        this(context, null);
    

    public WaterView(Context context, AttributeSet attrs) 
        this(context, attrs, 0);
    

    public WaterView(Context context, AttributeSet attrs, int defStyleAttr) 
        this(context, attrs, defStyleAttr, 0);
    

    public WaterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) 
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    

    public void startAnim() 
        if (!animator.isRunning())
            animator.start();
    

    private void init() 
        list = new ArrayList<>();
        path = new Path();
        criclePath = new Path();
        paint = new Paint();
        rect = new Rect();
        rectF = new RectF(0, 0, 100, 100);

        paint.setColor(Color.BLUE);
//        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setAntiAlias(true);
        paint.setAlpha(50);

        animator = new ValueAnimator();
//        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setDuration(1700);
        animator.setFloatValues(0f, 1f);
        animator.addUpdateListener(valueAnimator -> 
            mCurrentScale = (float) valueAnimator.getAnimatedValue();
            postInvalidate();
            invalidate();
        );


    

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
        oneWidth = width >> (number - 1);
        addPath();
        criclePath.addCircle(0, 0, width, Path.Direction.CCW);
        rect.set(width, height, 0, 0);
        setMeasuredDimension(width, height);
    

    private void addPath() 
        list.add(new PointF((oneWidth / 2), oneHeight * 2));
        list.add(new PointF(oneWidth / 2 * 3, 0));
        list.add(new PointF(oneWidth * 2, oneHeight));

    

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) 
        super.onLayout(changed, left, top, right, bottom);
    

    @Override
    protected void onDetachedFromWindow() 
        animator.cancel();
        path = null;
        paint = null;
        list = null;
        animator = null;
        rect = null;
        super.onDetachedFromWindow();
    


    /**
     * 首先moveTo 到绘画的起点
     * 二阶 quadTo 第一参数 固定点 最后参数结束的点
     * 三阶 cubicTo 第一 固定点 第二固定点 最后参数结束的点
     *
     * @param canvas 这里由两个三阶贝塞尔曲线组成 另外一个在屏幕之外 相同尺寸 通过属性动画循环平移 在平移结束后会再从0开始
     */
    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);

//        paint.setColor(Color.GRAY);

        path.arcTo(rectF, 90, 90);
        path.lineTo(50, 50);
        path.lineTo(50, 100);
        path.close();
//        paint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(path, paint);
        paint.setColor(Color.RED);
        paint.setAlpha(50);
//        canvas.drawRect(rect, paint);
        path.reset();
        canvas.save();
        paint.setColor(Color.BLUE);
        paint.setAlpha(50);
        float moveDicance = oneWidth * 2 * mCurrentScale;
        path.moveTo(-(2 * oneWidth) + moveDicance, oneHeight);
        path.cubicTo(-(oneWidth / 2 * 3) + moveDicance, oneHeight * 2,
                -(oneWidth / 2) + moveDicance, 0,
                0 + moveDicance, oneHeight);


        path.cubicTo(list.get(0).x + moveDicance, list.get(0).y,
                list.get(0 + 1).x + moveDicance, list.get(0 + 1).y,
                list.get(2).x + moveDicance, list.get(2).y);

        /*canvas.clipPath(criclePath);绘制圆边界*/
        path.lineTo(width, height);
        path.lineTo(-(2 * oneWidth) + moveDicance, height);
        path.lineTo(-(2 * oneWidth) + moveDicance, oneHeight);
        path.close();


        canvas.drawPath(path, paint);


        canvas.restore();
        path.reset();
    

onMeasure

测量view宽高MeasureSpec.getSize/getMode(onMeasure.widthMeasureSpec);

  • EXACTLY 精准模式

  • AT_MOST 最大模式

  • UNSPECIFIED 宽高不确定

width = MeasureSpec.getSize(widthMeasureSpec);
  int a = MeasureSpec.getMode(widthMeasureSpec);

onLayout

  • getChildAt 测量子view在父布局中位置 

  • getMeasureWidth 获取测量宽度

  • getWidth 获取测量后宽度

onDraw

  • 和生活中绘画是一样的,需要准备笔+纸+绘画的轨迹;

  • 坐标轴原点在矩形左上方;

  • 刚开始笔位置是在原点的,根据需要moveto到指定位置;

  • 如果需要绘画实心,需要连接每个线,最后使用close;

  • save和restore是成对出现的,多次绘制需要restore恢复到上次保存的样式,save是保存当前样式;

二阶贝塞尔曲线

三阶贝塞尔曲线

问题:结合实际绘画水波浪的思路?

引导:看这张图会发现控制点一上一下类似于水波浪,如果是无线长,而且一直平移会达到波浪的效果,不过我们不可能画一个无限长的线,太浪费性能了,屏幕外画两个三阶贝塞尔曲线,保证平移后正确对接结束和开始点。

/   Kotlin协程   /

协程出现是为了解决更好管理线程问题的。

Volatile字段和Synchronized在kotlin是使用注解使用的

@Volatile
    var synclist = CopyOnWriteArrayList<String>()
    @Synchronized
    fun numAdd() 
        num += 1
        Log.e(TAG, "testSync: $num")

    

runBlocking 堵塞

GlobalScope.launch 
            var num = 0
            runBlocking 
                num++
            
            System.out.print(num)
        

job

下面来看一下job的简单使用和源码:

val job = GlobalScope.launch(Dispatchers.Main, CoroutineStart.LAZY) 
        
 job.start()
 public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job 
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine

job有两个参数和一个方法体

Context上下文 对应Dispatchers有四个参数 :

  • Main 主线程

  • IO io文件流

  • Default 处理大数据 json解析等 同样为默认参数

  • Unconfined 在调用的线程直接使用

Start 启动模式 对应CoroutineStart四个参数:

  • DEFAULT 立即执行协程

  • LAZY只有在start的时候才会执行

  • ATOMIC 立即执行 在执行前无法取消

  • UNDISPATCHED 立即执行线程直到第一个suspend执行

async await 并发和异步

asynchronous(GlobalScope.launch(Dispatchers.Default, CoroutineStart.LAZY) 
            val s = GlobalScope.async 
                getName()
            .await()
            val p = GlobalScope.async 
                getAge()
            .await()
            Log.e("tag", "job: $p")
            Log.e("tag", "job: $s $p")
        
 suspend fun getAge(): Int =1++

async是可以支持并行的,await等待方法执行完毕。有兴趣可以观察一下AtomicInteger的源代码。

/   图片内存管理   /

问题1:图片内存占用大小如何计算?

size = width * height * 字节

根据上面计算公式可了解处理的方向。

宽和高缩放

bitmap不加载到内存中如何获取尺寸?inJustDecodeBounds为true的时候就不会加载bitmap了, 同时bitamp=null,可是能获取对应的宽和高。inSampleSize是缩放比例,一般最佳是bitmap尺寸和view尺寸相同。

BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;//不加载图片到内存中
        BitmapFactory.decodeResource(Resources.getSystem(), android.R.mipmap.sym_def_app_icon, options);
        System.out.println(options.outWidth + " \\n" +
                options.outHeight + " \\n" +
                options.inBitmap);
        options.inJustDecodeBounds = false;
        options.inSampleSize = 2;
        Bitmap bitmap = BitmapFactory.decodeResource(Resources.getSystem(), android.R.mipmap.sym_def_app_icon, options);

1像素占的字节

public static enum Config 
        ALPHA_8,//1字节 较不清晰
        RGB_565,//2个字节 相对于ARGB_4444不清晰
        /** @deprecated */
        @Deprecated
        ARGB_4444,//2字节 性价比 glide采用的格式
        ARGB_8888,//4字节 android默认 最清晰
        RGBA_F16,
        HARDWARE;

        private Config() 
        
    

本地资源res可能不会根据宽高*字节的方式计算。

bitmap分为多个文件夹,一般是来放app图标的,建议使用xhdpi和xxhdpi;

现在市场上大部分手机都是xhpi以上,而且系统首先判断的是xhpi是否存在文件和手机密度匹配,然后再去匹配其他尺寸文件夹。

如果只有一个文件夹有则会根据文件夹尺寸和手机密度进行缩放放大尺寸,故需要计算出缩放比例:

w=w * 屏幕密度/图片hdpi密度 size=w * h * 字节

问题:图片占用内存减少了,那么图片很多的话同样会内存溢出?

上面讲到过LinkedHashMap会根据使用顺序进行移动到后方,那就衍生出了LruCache内存缓存,和银行排队是一样的,无法处理很多人同时办理业务,从而保证银行的正常运转。

private val lruCache = object : LruCache<String, Bitmap>(10) 
        override fun entryRemoved(
            evicted: Boolean,
            key: String?,
            oldValue: Bitmap?,
            newValue: Bitmap?
        ) 
            if (evicted)
                oldValue?.recycle()
        
    

可以看出来重写entryRemoved方法,这个就是银行人太多了,多出来的人只是去银行门口了,并没有走,为了更好的处理问题,多出来的人让我们自己处理,如果不处理那么人越来越多就会出现各种事故,导致内存泄露。

那我们处理方案是什么:门口的人一个一个的说,你们回家吧!evicted=true是门口的人,recycle是回收;glide帮助我们处理了这些事情,同样也有问题没处理到的,我也不知道你的需求,那么glide就加载的全尺寸图片;人很多,不一定所有人都听你的话,这时候有一部分人就造反了,那么就造成银行成了恐怖事件,这时候警察就来了,结束了。

/   注解   /

问题1:枚举为什么浪费内存?如何解决?

反编译后 = 常量参数+数组+类。

下面是解决方法,反编译后会发现只是常量参数,只是在ide编译器会限制种类,而不是在运行的时候去判断。

@Retention(RetentionPolicy.SOURCE)
@StringDef(MsgType.HELLO, MsgType.WORLD)
public @interface MsgType 
    String HELLO ="HELLO";
    String WORLD ="WORLD";

fun getMsgType(@MsgType s: String) 

    

问题2:Butterknife,Retrofit,Eventbus等注解怎么实现连接的?

下面是注解,细心的人会发现Butterknife生成了一个类。在编译的时候有时候会编译不过去,就是这个类不存在。Apt为我们实现了什么,获取到注解的类方法参数等,然后生成类和方法。同时大家注意到了需要在onCreate和onDestory的的时候绑定和解绑。

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.FIELD, ElementType.METHOD, ElementType.TYPE)

public @interface BindView 

    int value() default 0;

/   OOM   /

oom是内存溢出的意思,内存泄露了会导致内存溢出,文件过大也会导致内存溢出。

引用模式和图片内存的处理是大部分程序的内存溢出原因,静态类也是跟随系统的不会回收,我们在书写代码的时候无法完全避免内存不溢出,线上环境好多无法复现是每个人苦恼的问题。

leakCanary只能解决线下问题,那我们是否可以借鉴他的思路?

上面说到在每个activity销毁的时候虚引用类,gc是在activity销毁的时候不一定立即执行的,leakCanary内部可能手动触发gc的

类在触发gc的时候执行的方法。

@Override
    protected void finalize() throws Throwable 
        super.finalize();
    

获取手机内存。

Runtime.getRuntime().totalMemory();

系统发出通知内存不足。

在四大组件和application都会有这个方法的重写。

  • TRIM_MEMORY_RUNNING_MODERATE:表示App正常运行,并且不会被杀掉,但是目前手机内存已经有点低了,系统可能会根据LRU List来开始杀进程。

  • TRIM_MEMORY_COMPLETE :表示 App已经处于 LRU List比较考靠前的位置,并且手机内存已经极低,随时都有可能被系统杀掉。

override fun onTrimMemory(level: Int) 
        super.onTrimMemory(level)
    

/   ANR   /

app长时间无响应。

问题1:底层如何实现抛异常喃?

常见的service,activity,广播等都对生命周期进行了处理。例如activity的onCreate,在开始的时候发出一个事件埋炸弹,5秒后如果还没有人拆炸弹就会引爆,这时候就闪退了。

问题2:如何检测执行时间?

字节插桩在编译过程执行部分打印时间,结束打印时间。

问题3:即然ui操作都是在主线程单独处理的,那我们是否可以根据此逻辑进行优化?

  1. android-watchdog新开一个子线程,每隔5s向主线程发送事件,如果主线程在堵塞中就不会更新标记;

  2. android-ANRWatchdog是watchdog的优化,每1秒检测一次,避免了5秒anr后无法输出anr事件

  3. BlockCanary是借鉴leakCanary衍生出来的一个检测执行速度的第三方api,同时会对指定时间未完成的进行日志输出,同时线上线下都可以使用;因为新开了一个进程,线上因此crash也不会影响主进程

到现在告一段落了,欢迎大家补充和指出不足。

来源:Wang You Hu

https://blog.csdn.net/qq_41912447?type=blog

THE END

PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。

以上是关于Android 高级开发进阶图谱的主要内容,如果未能解决你的问题,请参考以下文章

Android高级UI开源框架进阶解密附Loading图表菜单日历图片文本弹窗悬浮窗状态栏导航布局等经典框架源码解析

Android高级进阶(源码剖析篇) 前言

干货Android中高级开发进阶必备资料(附:PDF+视频+源码笔记)

普通Android开发如何进阶为Android高级工程师?

字节大佬写给Android中高级开发的《Android 音视频开发进阶指南》,限时开源分享!!!

40岁开始学习Android开发的我成了一名技术主管