阅读 538

Android面试指南(二)————Android基础篇(二)

View

触摸事件传递

DecorView为最顶层的view,DecorView、TitleView和ContentView都为FrameLayout

如果一个view处理了down事件,那么后续的move,up都会交给他处理

点击事件的传递流程

onTouchListener(onTouch)>onTouchEvent()>onClickListener(onClick)

onTouchListener的onTouch返回为false,则onTouchEvent被调用

简述view的事件传递

事件传递从父类向子类传递,其中包含3个方法,在一个类中顺序执行,

  • dispatchEvent:事件的分发,true---->分发给自己
  • onIntercepterEvent:事件拦截,true---->拦截后交给自己的onTouchEvent处理,false ---->传递给子View
  • onTouchEvent:事件的执行。

如果View没有对ACTION_DOWN进行消费,之后的事件也不会传递过来。

事件的传递是从Activity开始的,Activity -->PhoneWindow–>DectorView–>ViewGroup–>View;主要操作在ViewGroup和View中;

ViewGroup类主要调用:dispatchTouchEvent()–>onInterceptTouchEnent()–>dispatchTransformedTouchEvent();ViewGroup不直接调用onTouchEvent()方法;

相关子类方法
Activity类Activity……dispatchTouchEvent(); onTouchEvent();
View容器(ViewGroup的子类)FrameLayout、LinearLayout、ListView、ScrollVIew……dispatchTouchEvent(); onInterceptTouchEvent(); onTouchEvent();
View控件(非ViewGroup子类)Button、TextView、EditText……dispatchTouchEvent(); onTouchEvent();

onIntercepterTouchEvent()方法之只存在ViewGroup中,Activity为最顶层,不需要拦截,直接分发,view为最底层,不需要拦截,直接分发

  • 以ACTION_DOWN为开始,UP或者CANCEL为结束
  • 如果dispatch不处理ACTION_DOWN事件,那么就不会继续接收到后续的ACTION_xxxx事件
如何让只执行onTouch事件,不执行onClick事件?

将onTouch方法的返回值改为true,就会只执行onTouch事件,不执行onClick事件。

如果截取了事件,还会往下传吗?那会走到哪里?

如果截取了事件就不会往下传递了,只会执行本Viewgroup的onTouchEvent。

如果截取了事件并处理了事件还会返回父级吗?

会返回父类,因为父类需要确认子级是否已经处理了事件

requestDisallowInterceptTouchEvent

子view让其父view不做事件拦截, 在子view的onTouchEvent方法中调用parent.requestDisallowInterceptTouchEvent(true)方法,

如果父view拦截事件,是怎么通知到子view的onInterceptTouchEvent中调用disallowIntercepter?

在ScrollView中进行源码分析:
在onIntercepterTouchEvent中返回true,则进行拦截,在按下滑动一小部分距离后设置为false(ACTION_MOVE),可以进行事件传递,当然就可以调用disallowIntercepter方法进行处理,后续的值触发父view的机制,直接过滤掉了onIntercepterTouchEvent

所以在ScrollView中默认的onClickListener是不生效的

onIntercepterTouchEvent不执行,直接返回false,然后向下dispatch到子类

该方法生效的前提是父view不拦截ACTION_DOWN事件,第一次的ACTION_DOWN事件可以传递到子view中,则后续的ACTION事件父view无法拦截

如何解决滑动冲突
  1. 外部拦截法:

    重写父view的onIntercepterTouchEvent,在其中对触摸的坐标进行控制,在父view要拦截的时候拦截,在子view想要调用的时候不进行拦截

    public boolean onInterceptTouchEvent(MotionEvent event) {
            boolean intercepted = false;
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    intercepted = false;
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    if (满足父容器的拦截要求) {
                        intercepted = true;
                    } else {
                        intercepted = false;
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    intercepted = false;
                    break;
                }
                default:
                    break;
            }
            mLastXIntercept = x;
            mLastYIntercept = y;
            return intercepted;
        }
    复制代码
  2. 内部拦截法:

    在子view的dispatchTouchEvent中在ACTION_DOWN事件下调用parent.requestDisallowInterceptTouchEvent(true);,设置不允许父view的拦截

    public boolean dispatchTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    parent.requestDisallowInterceptTouchEvent(true);
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    if (父容器需要此类点击事件) {
                        parent.requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    break;
                }
                default:
                    break;
            }
    
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(event);
        }
    复制代码

    该条件需要在父view的ACTION_DOWN事件可以传递到子view中才可以实现,所以需要在父view的onInterceptTouchEvent中不拦截父View的ACTION_DOWN事件

    public boolean onInterceptTouchEvent(MotionEvent event) {
    
            int action = event.getAction();
            if (action == MotionEvent.ACTION_DOWN) {
                return false;
            } else {
                return true;
            }
        }
    复制代码
ACTION_CANCEL怎么理解?
  • 在划出子view的布局后,onIntercepterTouchEvent进行拦截ACTION_MOVE事件,并将其转化为ACTION_CANCEL交给子view的处理,表示手指划出view所在区域
  • 在父view进行拦截的时候,子view有可能接收到ACTION_CANCEL事件

触摸事件的结束有两种状态,一种时ACTION_UP事件,另一种就是ACTION_CANCEL事件,正常在view的事件传递中,抬起手指的ACTION_UP事件会被监听,当父view认为不需要将后续的ACTION_MOVE事件传递给子View的时候,就会将ACTION_MOVE事件转化为ACTION_CANCEL事件,子View就会认为事件结束

主要是父view在拦截中做了处理影响子view的触摸,不需要触摸就直接传ACTION_CANCEL。

使用TouchTarget(具体实现时mFirstTouchTarget)单链表存储触摸事件的,当置为CANCLE时,将触摸view在mFirstTouchEvent删除

事件到底是先到DecorView还是先到Window的?

ViewRootImpl——>DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup

为什么绕来绕去的呢,光DecorView就走了两遍。

  • ViewRootImpl并不知道有Activity这种东西存在,它只是持有了DecorView。所以先传给了DecorView,而DecorView知道有Activity,所以传给了Activity。
  • Activity也不知道有DecorView,它只是持有PhoneWindow,所以这么一段调用链就形成了。

多点触控(非重点)

使用TouchTarge(mFirstTouchTarget)管理

private static final class TouchTarget {
        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;
}
复制代码
  • view:触摸目标view
  • pointerIdBits:位运算(与、或)
  • next:链表指针

第一个触摸目标,在ACTION_DOWN、ACTION_POINTER_DOWN时会触发寻找触摸目标过程(事件分发),所以DOWN事件会重置mFirstTouchTarget。

  • 单点触控,mFirstTouchEvent为单个对象
  • 多点触控,在一个view上,也是单个对象
  • 多点触控,在多个view上,会成为一个链表

传入的view消耗了事件,则构建一个TouchTarget,并发至在mFirstTouchTarget的头部。多个view目标会头插在链表中。

即便是多指触控,也都是使用ACTION_MOVE,不做区分,可以使用index获取

如果ViewGroup是横向滑动的,RecyclerView是纵向滑动的,当调用RecyclerView进行纵向滑动时,在横向滑动会怎么样?

当使用纵向滑动,默认事件传递是viewPager到RecyclerView,即后续的所有事件都由RecyclerView进行处理,那么RecycleView没有横向事件,所以不会做处理,所以不会出现横向的滑动。

View的加载流程

简述View的加载流程
  1. 通过Activity的setContentView方法间接调用Phonewindow的setContentView(),在PhoneWindow中通过getLayoutInflate()得到LayoutInflate对象
  2. 通过LayoutInflate对象去加载View,主要步骤是

(1)通过xml的Pull方式去解析xml布局文件,获取xml信息,并保存缓存信息,因为这些数据是静态不变的

(2)根据xml的tag标签通过反射创建View逐层构建View

(3)递归构建其中的子View,并将子View添加到父ViewGroup中

加载结束后就开始绘制view了

View的绘制机制

DecorView为最顶层的view,DecorView、TitleView和ContentView都为FrameLayout,

当Activity对象被创建完毕后,会将DecorView添加到PhoneWindow中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,view的绘制过程是由ViewRootImpl完成的。

所有的view都是依附在window上的,比如PopupWindow、菜单。

Window是个概念性的东西,你看不到他,如果你能感知它的存在,那么就是通过View,所以View是Window的存在形式,有了View,你才感知到View外层有一个皇帝的新衣——window

有视图的地方就有window

简述View的绘制流程

深度便利
主要分为3个方法,顺序执行:

  • measure():测量视图的大小,根据MeasureSpec进行计算大小
  • layout():确定view的位置
  • draw():绘制view。创建Canvas对象。六个步骤:①、绘制视图的背景;②、保存画布的图层(Layer);③、绘制View的内容;④、绘制View子视图,如果没有就不用;⑤、还原图层(Layer);⑥、绘制滚动条。

draw()中的具体流程是什么?

  1. 绘制背景:drawBackground(canvas)
  2. 绘制自己的内容:onDraw(canvas)
  3. 绘制Children:dispatchDraw(canvas)
  4. 绘制装饰:onDrawForeground(canvas)
MeasureSpec分析

MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求,子view依据该值进行大小的绘制

MeasureSpec是个大小和模式的组合值。是一个32位的整型,将size(大小)和mode(模式)打包成一个int,其中高两位是mode,其余30位存储size(大小)

  // 获取测量模式
  int specMode = MeasureSpec.getMode(measureSpec)

  // 获取测量大小
  int specSize = MeasureSpec.getSize(measureSpec)

  // 通过Mode 和 Size 生成新的SpecMode
  int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
复制代码

测量模式有三种:

  • EXACTLY: 相等于MATCH_CONTENT
  • AT_MOST: 相等于WRAP_CONTENT
  • UNSPECIFIED: 相等于具体的值

RelativeLayout、LinearLayout和ConstraintLayout

LinearLayout:

  • weight设置导致二次测量,首先测量一遍大小onMeasure(非weight),然后根据weight在次测量,调整大小

RelativeLayout:

  • onMeasure执行两遍,对横向和纵向分别测量,所以是2遍

ConstraintLayout:

  • 可以不使用嵌套,提供相对布局,并且支持权重布局,尽可能减少层级,提高性能,类似于flex布局

对比

  1. 同层级的布局,LinearLayout<RelatvieLayout=ConstraintLayout,因为LinearLayout执行onMeasure一遍,RelativeLayout执行两遍
  2. LinearLayout会增加层级深度,RelativeLayout减少层级,所以通常下使用RelativeLayout,如果层级简单则使用LinearLayout

RelativeLayout的子View如果高度和RelativeLayout不同,会引发效率问题

setContentView的执行过程

  1. 初始化windows
  2. 绑定ui布局

什么时候可以获得view的宽高

因为onMeasure和生命周期不同步,所以不能在onCreate,onStart,onResume中进行获取操作,

  1. 在view.post方法中进行获取,内部实现是handler机制,回调的时候已经执行完了
  2. 在onWindowFocusChanged获取焦点后,view的绘制完成,可以在这里拿到view的宽高
  3. 使用ViewTreeObserver的回调也可以解决这个问题。
ViewTreeObserver observer = tv1.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    tv1.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
                int width = tv1.getMeasuredWidth();
                int height = tv1.getMeasuredHeight();
                Log.d("tv1Width", String.valueOf(width));
                Log.d("tv1Height", String.valueOf(height));
            }
        });
复制代码
  1. 手动调用measure方法后,获取宽高

什么时候开始绘制Activity的view的?

在DecorView添加(addView)到phoneWindows中时,触发measure,layout,draw方法

PhoneWindow是在什么时候创建的?

在Activity的attch方法时,创建了PhoneWindow

View的刷新机制

requestLayout和invalidate区别是什么

requestLayout:触发onMeasure,onLayout方法,大小和位置变化,不一定触发onDraw

invalidate:触发performTraversals机制,导致view重绘,调用onDraw方法,主要是内容发生变化

postInvalidate:异步调用invalidate方法

invalidate如果是个view,那就只有自己本身会draw,如果是ViewGroup就是对子view进行重绘

简析Activity、Window、DecorView以及ViewRoot之间的错综关系

  • Activity是控制器

  • windows装载DecorView,并将DecorView交给ViewRoot进行绘制和交互,其唯一实现子类就是PhoneWindow,在attach中创建,是Activity和View交互的中间层,帮助Activity管理View。

  • DecorView是FrameLayout的子类,是视图的顶级view

  • viewRoot负责view的绘制和交互,实际的viewRoot就是ViewRootImpl类,是连接WMS和DecorView的纽带

setContentView执行的具体过程

  1. Activity实例化,执行attach方法,在attach中创建PhoneWindow
  2. 执行onCreate方法,执行setContentView,先调用phoneWindow.setContentView(),然后开始根据不同的主题创建DecorView的结构,传入我们的xml文件,生成一个多结构的View
  3. Activity调用onResume方法,调用WindowManager.addView()方法,随后在addView()方法中创建ViewRootImpl
  4. 接着调用ViewRootImpl的setView方法,最终触发meaure,layout,draw方法进行渲染绘制,其中和WMS通过Binder交互,最终显示在界面上

四者的创建时机?

  • Activity:startActivity后,performLaunchActivity方法中创建
  • PhoneWindow:Activity的attach方法
  • DecorView:setConentView中创建
  • ViewRootImpl:onResume中调用WM.addView方法创建

dialog为什么不能用application创建?

Android-Window机制原理之Token验证(为什么Application的Context不能show dialog)

token是WMS唯一用来标识系统中的一个窗口

Dialog有一个PhoneWindow实例,属于应用窗口。Dialog最终也是通过系统的WindowManager把自己的Window添加到WMS上。Dialog是一个子Window,需要依附一个父window。

Dialog创建PhoneWindow时,token是null。只有传入Activity中的Context对象,Activity才会将自己的token给Dialog,这样,才会被WMS所识别,如果使用的不是Activit的token,就会报错BadTokenException

在application的情况下,将Dialog的window升级为系统window即可显示

RecyclerView和ListView

Android—RecyclerView进阶(4)—复用机制及性能优化

RecyclerView问题汇总

老大爷都能看懂的RecyclerView动画原理

RecyclerView性能优化及高级使用

简述RecyclerView的刷新和缓存机制

img

recyclerView中有三个重要方:

  • Adapter:负责与数据集交互
  • LayoutManager:负责ItemView的布局,接管Measure,Layout,Draw过程
  • Recycler:负责管理ViewHolder
  • ViewHolder:视图的创建和显示在Recycler中有多个缓存池,

mAttachedScrap被称为一级缓存,在重新layout时使用,主要是数据集发生变化的场景

//屏幕内缓存scrap
// mAttachedScrap在重新layout时使用,表示未与RecyclerView分离的ViewHolder
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// mChangedScrap用于数据变化等
ArrayList<ViewHolder> mChangedScrap = null;
//屏幕外缓存cache
// mCachedViews和RecycledViewPool用于滑动时的缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
// 用户自定义缓存,一般不用
private ViewCacheExtension mViewCacheExtension;
//屏幕外缓存pool,数据会被重置,虚之行onBindViewHolder
RecycledViewPool mRecyclerPool;


复制代码
  • mAttachedScrap:mAttachedScrap用于屏幕中可见表项的回收和复用,没有大小限制 mAttachedScrap生命周期起始于RecyclerView布局开始,终止于RecyclerView布局结束,无论mAttachedScrap中是否存在数据,都会清空,存储到mCacheView或者mRecyclerPool

插入或是删除itemView时,先把屏幕内的ViewHolder保存至AttachedScrap中

mAttachView和mCacheView都是通过比对position或者id(setStableIds(true)+getItemId复写)来确定是否复用的

缓存存储结构区别

  • mAttachedScrap:ArrayList
  • mCachedView:ArrayList
  • mRecyclerPool:SparseArray,ScrapData中包含ArrayList和其他标记位。
数据集发生变化

当数据集发生变化后,我们会调用notifyDataSetChanged()方法进行刷新布局操作,这时LayouManager通过调用detachAndScrapAttachedViews方法,将布局中正在显示的ItemView缓存到mAttachScrap中,重新构建ItemView时,LayoutManager会首先到mAttachScrap中进行查找 img 如图所示,如果只是删除Data1数据,执行NotifyDataSetChanged()方法时,layoutManager将Data0到Data4缓存到mAttachScrap中,重新渲染布局时,会直接复用mAttachScrap中的四个布局,而得不到复用的布局会被放置在mRecyclerPool中。

通过比较Position确定mAttachScrap中ItemView的复用,因为2的位置从2变为1,位置发生变化,但是还是通过比对position进行复用,那是因为在recyclerView重新渲染时,执行dispatchLayoutStep1()对position进行了校正。

滑动类型

在滑出可视区域后,会将ViewHolder存储在mCachedView中,当超出大小(默认大小为2+预加载item)后会将最先放进来的放在RecyclerViewPool中,根据viewType进行缓存,每个viewType缓存最多5个,从RecyclerViewPool中取出的数据,最终会调用onBindViewHolder()方法重新绑定

当发现有新的构建时,会去缓存找,找不到就去mRecyclerPool中寻找,如果有viewType相同的就取出来绑定并复用。

img

RecyclerView滑动时,刚开始的时候回收了Position0和Position1,它们被添加到了mCachedViews中。随后回收Position2时,达到数量上限,最先进入mCachedViews的Position0被放进了mRecyclerPool中。 再看下方进入可视区域的3个Item,最初的Position6和Position7找不到对应的缓存,只能新建ViewHolder并绑定。当Position8滑入可视区域时,发现mRecyclerPool中有一个ViewType相等的缓存,则将其取出并绑定数据进行复用。

当有数据进行变动时,数据的position会发生变化。

stableId

mChangedScrap----->mAttachedScrap----->mCachedViews----->ViewCacheExtension----->RecycledViewPool-------->onCreatViewHolder

如果是单个viewType的RecyclerView,在滑动过程中,RecyclerPool最多可能存在一个数据

假设一屏幕显示7个,向上滑动10个,总共bindView10个,又下滑10个(滑回去),总共8个(cacheView复用两个),一共18个

在RecyclerView的v25版本中,引入预取机制,在初始化时,初始化8个,提前缓存一个数据

RecyclerView的优化

放大缓存大小和缓存池大小
  1. 再滑动过程中,不论上滑还是下滑都会从mCachedViews中查找缓存,如果滑动频繁,可以通过RecyclerView.setItemViewCacheSize(...)方法增大mCachedViews的大小,减少onBindViewHolder()和onCreateViewHolder()调用
  2. 放大RecyclerViewPool的默认大小,现在是每个viewType中默认大小为5,如果显示数据过多,可放大默认大小
//设置viewType类型的默认存储大小为10
recyclerview.getRecycledViewPool().setMaxRecycledViews(viewType,10);
复制代码

如果多个RecyclerView中存在相同ViewType的ItemView,那么这些RecyclerView可以公用一个mRecyclerPool。

优化onBindViewHolder()耗时

尽量少的在onBindViewHolder中执行操作,减少新建对象对内消耗

布局优化

多使用include,merage,viewStub,LinearLayout,FrameLayout

measure()优化和减少requestLayout()调用

当RecyclerView宽高的测量模式都是EXACTLY(精确数据)时,onMeasure()方法不需要执行dispatchLayoutStep1()等方法来进行测量。而当RecyclerView的宽高不确定并且至少一个child的宽高不确定时,要measure两遍。 因此将RecyclerView的宽高模式都设置为EXACTLY有助于优化性能。

如果RecyclerView的宽高都不会变,大小不变,方法RecyclerView.setHasFixedSize(true)可以避免数据改变时重新计算RecyclerView的大小,优化性能

notifyDataSetChanged 与 notifyItemRangeChanged 的区别?

当notifyItemRangeChanged的区间在mRecyclerpool的大小的间隔内,则会通过mRecyclerpool复用viewholder,响应快速。

notifyItemInsert()和notifyItemRemove()方法,会通过RecyclerView的预加载流程,会将ViewHolder缓存到mAttachView中,避免重新create和bind。

notifyItemChanged(int)方法更新固定item

notifyDataSetChanged 会将所有viewholder放置在pool中,但是只能放置5个,其他就回收了,再构建时,需要重新绘制测量,界面会导致闪烁等

如果使用SetHasStableIds(true),会将数据缓存到scrap中,复用时直接使用

调用 notifyDataSetChanged 时闪烁的原因?

itemView重新测量和布局导致的(bindViewHolder),并非createViewHolder。数据存储在RecyclerViewPool中,拿出需要重新BindView,itemView重新进行测量和布局,导致出现UI线程耗时,出现闪烁

如果使用SetHasStableIds(true),会将数据缓存到scrap中,复用时直接使用

如果你的列表能够容纳很多行,而且使用 notifyDataSetChanged 方法比较频繁,那么你应该考虑设置一下容量大小。

RecyclerView相对于ListView的优势是什么?

  1. 屏幕外缓存可以直接在mCacheView()中复用,不需要重新BindView
  2. recyclerPool可以提供给多个RecyclerView使用,在特定场景下,如viewpaper+多个列表页下有优势.
  3. ListView缓存View,RecyclerView缓存ViewHolder

adapter,viewHolder的作用?adapter中常用方法的作用是什么?

  • Adapter:负责与数据集交互
  • ViewHolder:视图的创建和显示,持有所有的用于绑定数据或者需要操作的View
//创建Item视图,并返回相应的ViewHolder
public VH onCreateViewHolder(ViewGroup parent, int viewType)
//绑定数据到正确的Item视图上。
public void onBindViewHolder(VH holder, int position)
//返回该Adapter所持有的Item数量
public int getItemCount()
//用来获取当前项Item(position参数)是哪种类型的布局
public int getItemViewType(int position)

复制代码

RecyclerPool为何使用SparseArray?

在RecyclerView中,第四级缓存,mRecyclerPool中存储是通过SparseArray存储ViewHolder,根据不同的ViewType的int值为键,ScrapData为值,ScrapData也是ArrayList及其标志位组成的,在进行put和get方法时,都是通过ViewType值获取。
不使用HashMap的原因是:

  • 我们定义了viewType为int值,则不用HashMap中较为繁重的类型,减少装箱问题耗时
  • 量级较小,不需要HashMap的大量级处理
  • 节省内存

使用SparseArray存储空间id和空间对象关系。

HashMap更加复杂,SparseArray减少开销

LayoutManager样式有哪些?setLayoutManager源码里做了什么?

  • LinearLayoutManager 水平或者垂直的Item视图。
  • GridLayoutManager 网格Item视图。
  • StaggeredGridLayoutManager 交错的网格Item视图。

当之前设置过 LayoutManager 时,移除之前的视图,并缓存视图在 Recycler 中,将新的 mLayout 对象与 RecyclerView 绑定,更新缓存 View 的数量。最后去调用 requestLayout ,重新请求 measure、layout、draw。

ItemDecoration的用途是什么?自定义ItemDecoration有哪些重写方法?分析一下addItemDecoration()源码?

用途:来改变Item之间的偏移量或者对Item进行装饰

//装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡
public void onDraw(Canvas c, RecyclerView parent)
//装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上
public void onDrawOver(Canvas c, RecyclerView parent)
//与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量。
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
复制代码

当通过这个方法添加分割线后,会指定添加分割线在集合中的索引,然后再重新请求 View 的测量、布局、(绘制)

mChangedScrap和mAttachedScrap的区别是什么?

因为mChangedScrap表示item变化了,有可能是数据变化,有可能是类型变化,所以它的viewHolder无法重用,只能去RecycledViewPool中重新取对应的,然后再重新绑定。

mChangedScrap与mAttachedScrap,作用差不多。

mChangedScrap更多的用于pre-layout的动画处理。

然后一点需要注意:mChangedScrap只能在pre-layout中使用,mAttachedScrap可以在pre-layout与post-layout中使用。

mChangedScrap:ViewHolder.isUpdated() == true

mAttachedScrap:1.被同时标记为removeinvalid;2.完全没有改变的ViewHolder

在notifyItemRangeChanged,将数据变化的放置在mChangedScrap,没有变化的存储在mAttachScrap中,然后再取出来,mChangedScrap的数据会被移动到RecyclerPool中,进行重新绑定后再放回mChangedScrap中

mAttachScrap中得不到复用的会放置在recyclerpool中

onMeasure过程

过程中包含mAttachedScrap的使用

dispatchLayoutStep1:预布局

dispatchLayoutStep2:实际布局

dispatchLayoutStep3:执行动画信息

如何解决Glide错乱问题

因为存在复用机制,8可能会复用1,在网络不好或者图片过大的情况下,8的图片加载缓慢,会先显示1的图片,加载后才会刷新掉。

方案:imageView设置tag,判断是否复用,如果是复用,就清除该控件上Glide的缓存

RecyclerView卡顿优化

通过BlockCanary进行主线程卡顿检测,打印出任务耗时,在卡顿时,打印出栈堆信息

原理是在looper.loop()死循环中,执行任务都是dispatchMessage方法,如果该方法超过一个任务的常规耗时,就会导致主线程卡顿

解决方法:

  1. 放大mCacheView和RecyclerPool的大小,提高复用率,减少渲染

  2. 图片在滑动结束后再进行加载,避免在滑动的时候向主线程做更新

    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                Glide.with(mContext).resumeRequests();
            }else {
                Glide.with(mContext).pauseRequests();
            }
        }
    });
    复制代码

    在滑动过程中停止加载,在滑动结束后恢复加载

  3. 使用DiffUtil进行局部刷新优化

    //DiffUtil会自动计算新老数据的差异,自动调用notifyxxx方法,将无脑的notifyDataSetChanged()进行优化
    //并且伴随动画
    adapter.notifyItemRangeInserted(position, count);
    adapter.notifyItemRangeRemoved(position, count);
    adapter.notifyItemMoved(fromPosition, toPosition);
    adapter.notifyItemRangeChanged(position, count, payload);
    复制代码
    //文艺青年新宠
    //利用DiffUtil.calculateDiff()方法,传入一个规则DiffUtil.Callback对象,和是否检测移动item的 boolean变量,得到DiffUtil.DiffResult 的对象
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
    //利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年
    diffResult.dispatchUpdatesTo(mAdapter);
    //别忘了将新数据给Adapter
    mDatas = newDatas;
    mAdapter.setDatas(mDatas);
    复制代码
  4. 减少布局的嵌套和层级,减少过度绘制,尽量自定义view

  5. 如果Item高度固定,调用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源

  6. 可以关闭动画,减少RecyclerView的渲染次数

RecyclerView的自适应高度

  1. 使用瀑布流布局StaggeredGridLayoutManager
  2. 重写LinearLayoutManager,onMeasure中重新测量子布局的大小

RecyclerView嵌套RecyclerView滑动冲突,NestedScrollView嵌套RecyclerView

  1. 同方向的情况下会造成滑动冲突,默认外层的RecyclerView可滑动
    一般有两种处理方案:内部拦截法外部拦截法
    这里推荐内部拦截法,通过设置requestDisallowInterceptTouchEvent(true)时,不让父RecyclerView拦截子类的事件
  2. ScrollView嵌套RecyclerView同样可以使用这个方法解决。也可以使用NestedScrollView,该类就是为了解决滑动冲突问题,可以保证两View类都可以滑动,但是需要设置RecyclerView.setNestedScrollingEnabled(false),取消RecyclerView本身的滑动效果。解决滑动的卡顿感

动画

简述

帧动画:一连串的图片进行连贯的播放,形成动画。

补间动画:通过xml文件实现,实现 alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转),通过不断的绘制view,看起来移动了效果,实际上view没有变化,还在原地

属性动画:对于对象属性的动画,也可以使用xml配置,但是推荐代码配置,比xml更加方便。通过不断改变自己view的属性值,真正的改变view

所有的补间动画都可以用属性动画实现

属性动画和补间动画的区别

  1. 补间动画虽然移动了,但是点击的还是原来的位置,点击事件允许触发。而属性动画不是,所以我们可以确认,属性动画才是真正实现了View的移动,补间动画的view其实只是在其他地方绘制了一个影子
  2. Activity退出时,没有关闭动画,属性动画会导致Activity无法释放的内存泄漏,而补间动画不会发生这样的情况
  3. xml的补间动画复用率极高,在页面切换过程中都有很好的效果

帧动画避免大图,否则会带来oom

属性动画中的差值器和估值器是什么?

差值器:定义动画随时间流逝的变化规律。通俗点就是动画的执行速度的变化,可以是由缓即快,由快即缓,也可以是匀速,也可以是弹性动画效果 ,LinearInterpolator(匀速差值器)

估值器:定义从初始值过渡到结束值的规则定义,TypeEvaluator,可以通俗的理解为位置的移动

android系统启动流程

android系统架构

简述系统启动流程

从系统层看:

  1. linux 系统层
  2. Android系统服务层
  3. Zygote

从开机启动到Home Launcher:

  1. 启动bootloader (小程序;初始化硬件)
  2. 加载系统内核 (先进入实模式代码在进入保护模式代码)
  3. 启动init进程(用户级进程 ,进程号为1)
  4. 启动Zygote进程(初始化Dalvik VM等)
  5. 启动Runtime进程
  6. 启动本地服务(system service)
  7. 启动 HomeLauncher

第一个启动的进程是什么?

init进程,其他进程都是fork这个进程的

init进程孵化出了什么进程?

  • 守护进程
  • Zygote进程,负责孵化应用进程
  • MediaServer进程

Zygote进程做了什么?

  • 创建Socket服务端
  • 加载虚拟机
  • SystemServer进程
  • fork第一个应用进程---Launcher

为什么要创建Socket服务端?

  • ServiceManager不能保证在孵化Zygote进程时就初始化好了,所以无法使用Binder
  • Binder属于多线程操作,fork不允许多线程操作,容易发生死锁,所以使用Socket

app启动流程

  1. 用户点击 icon
  2. 系统开始加载和启动应用
  3. 应用启动:开启空白(黑色)窗口
  4. 创建应用进程
  5. 初始化Application
  6. 启动 UI 线程
  7. 创建第一个 Activity
  8. 解析(Inflater)和加载内容视图
  9. 布局(Layout)
  10. 绘制(Draw)

源码分析

  1. LauncherActivity.startActivitySafely(intent):使用intent启动
  2. Activity.startActivity(intent):
  3. Activity.startActivityForResult(intent):获取ApplicationThread成员变量,是一个Binder对象
  4. Instrumentation.execStartActivity:ActivityManagerService的远程接口
  5. ActivityManagerProxy.startActivity:通过Binder进入AMS
  6. ActivityManagerService.startActivity
  7. ActivityStack.startActivityMayWait:解析MainActivity的信息
  8. ActivityStack.startActivityLocked:创建即将要启动的Activity的相关信息
  9. ActivityStack.startActivityUncheckedLocked:获取intent标志位,新建Task栈,添加到AMS中
  10. Activity.resumeTopActivityLocked:查看LauncherActivity状态,新建Activity的状态
  11. ActivityStack.startPausingLocked:停止LauncherActivity,onPause
  12. ApplicationThreadProxy.schedulePauseActivity
  13. ApplicationThread.schedulePauseActivity
  14. ActivityThread.queueOrSendMessage:在主线程通过Handler发送消息
  15. H.handleMessage:Handler的回调
  16. ActivityThread.handlePauseActivity:pause LauncherActivity
  17. ActivityManagerProxy.activityPaused:进入AMS中的onPause事件
  18. ActivityManagerService.activityPaused
  19. ActivityStack.activityPaused
  20. ActivityStack.completePauseLocked
  21. ActivityStack.resumeTopActivityLokced:LauncherActivity已经onPause了
  22. ActivityStack.startSpecificActivityLocked
  23. ActivityManagerService.startProcessLocked:创建新进程
  24. ActivityThread.main:app入口,添加looper循环
  25. ActivityManagerProxy.attachApplication:通过Binder进入AMS中
  26. ActivityManagerService.attachApplication
  27. ActivityManagerService.attachApplicationLocked
  28. ActivityStack.realStartActivityLocked
  29. ApplicationThreadProxy.scheduleLaunchActivity:进入ApplicationThread
  30. ApplicationThread.scheduleLaunchActivity
  31. ActivityThread.queueOrSendMessage
  32. H.handleMessage
  33. ActivityThread.handleLaunchActivity
  34. ActivityThread.performLaunchActivity:进入onCreat方法
  35. MainActivity.onCreate

总结:

1~11:Launcher通过Binder进程通知ActivityManagerService,他要启动一个Activity

12~16:ActivityManagerService通过Binder进程通知Launcher进入Pause阶段

17~24:Launcher告知我已进入pause阶段,ActivityManagerService创建新进程,用来启动ActivityThread。

25~27:ActivityThread通过Binder进程将ApplicationThread的Binder传递给ActivityManagerService,以便AMS可以直接用这个Binder通信

28~35:AMS通过Binder通知ActivityThread,你可以启动

这里以启动微信为例子说明

  1. Launcher通知AMS 要启动微信了,并且告诉AMS要启动的是哪个页面也就是首页是哪个页面
  2. AMS收到消息告诉Launcher知道了,并且把要启动的页面记下来
  3. Launcher进入Paused状态,告诉AMS,你去找微信吧

上述就是Launcher和AMS的交互过程

  1. AMS检查微信是否已经启动了也就是是否在后台运行,如果是在后台运行就直接启动,如果不是,AMS会在新的进程中创建一个ActivityThread对象,并启动其中的main函数。
  2. 微信启动后告诉AMS,启动好了
  3. AMS通过之前的记录找出微信的首页,告诉微信应该启动哪个页面
  4. 微信按照AMS通知的页面去启动就启动成功了。

Activity启动流程

参照app的启动流程

  • ApplicationThread:ActivityThread的内部类,负责和AMS进行Binder通信
  • ActivityManagerService:服务端对象,负责管理系统中所有的Activity

Activity 启动过程是由 ActivityMangerService(AMS) 来启动的,底层 原理是 Binder实现的 最终交给 ActivityThread 的 performActivity 方法来启动她

ActivityThread大概可以分为以下五个步骤

  1. 通过ActivityClientRecord对象获取Activity的组件信息
  2. 通过Instrument的newActivity使用类加载器创建Activity对象
  3. 检验Application是否存在,不存在的话,创建一个,保证 只有一个Application
  4. 通过ContextImpl和Activity的attach方法来完成一些初始化操作
  5. 调用oncreat方法

Android开启新进程的方式是通过复制第一个zygote(受精卵)进程实现,所以像受精卵一样快速分裂

SystemServer是什么?有什么作用?他和zygote的关系是什么?

SystemServer也是一个进程,并且复制于zygote,系统中重要的服务都是在这个进程中开启的,如:AMS,PMS,WMS等

ActivityManagerService是什么?什么时候初始化的?有什么作用?

简称AMS,负责系统中所有Activity的生命周期,控制其开启、关闭、暂停等 是在SystemServer进程开启时进行初始化的

App 和 AMS(SystemServer 进程)还有 zygote 进程是如何通信的?

App 与 AMS 通过 Binder 进行 IPC 通信,AMS(SystemServer 进程)与 zygote 通过 Socket 进行 IPC 通信。

AMS/PMS/WMS运行在一个线程中还是进程中?

运行在System_server进程中的线程中

apk打包流程

  1. aapt阶段,打包res目录,生成R.java
  2. AIDL阶段,生成java文件
  3. java编译器。将java文件通过javac编译生成.class文件
  4. dex阶段,生成.dex文件
  5. apk打包阶段,将文件打包成为apk文件
  6. 签名阶段,对apk进行签名
  7. 整理apk文件

aapt和aapt2的区别?

aapt是全量编译,打包res目录,生成R文件

aapt2是差量编译,将变化的res目录进行重新打包,修改R文件

aapt2中存在两部分,编译和链接

编译:将资源文件编译为二进制文件

链接:将编译后二进制文件进行合并,生成独立的文件

在需要差量的时候,只需要重新编译二进制文件,再将这些二进制文件生成新的文件即可

apk的组成

  1. AndroidManifest.xml
  2. assets(项目中assets目录)
  3. classes.dex
  4. lib库
  5. META-INF(校验文件)
  6. res(资源文件)
  7. resources.arsc(资源文件映射,索引文件)

apk安装流程

存在多少种安装方式,分别是什么?

四种

  • 系统应用安装——————开机时完成安装,没有安装界面
  • 网络下载安装——————通过市场应用完成,没有安装界面
  • adb命令安装——————没有安装界面
  • 第三方应用安装——————sdk卡导入apk,点击安装,存在安装界面

安装过程中的重要路径

应用安装涉及到如下几个目录:

system/app ---------------系统自带的应用程序,获得adb root权限才能删除

data/app ---------------用户程序安装的目录。安装时把 apk文件复制到此目录

data/data ---------------存放应用程序的数据

data/dalvik-cache--------将apk中的dex文件安装到dalvik-cache目录下(dex文件是dalvik虚拟机的可执行文件,其大小约为原始apk文件大小的四分之一)

安装过程

  1. 将apk文件复制到/data/app目录
  2. 解析apk信息
  3. dexopt操作(将dex文件优化为odex文件)
  4. 更新权限信息
  5. 发送安装完成广播

Android虚拟机发展史

  1. android初期,Dalvik负责加载dex/odex文件
  2. 2.2版本,JIT(即时编译)初次加入,每次启动的时候编译,耗时,耗电
  3. 4.4版本引入ART(Android RunTime)和AOT(Ahead-of-time)(运行前编译成机器码),与Dalvik共存
  4. 5.0版本全部采用ART编译器,不耗时,不耗电,在安装期间比较慢而已,而且会占用额外的控件存储机器码
  5. 7.0版本JIT回归,再用JIT/AOT并用,即初次启动使用JIT,在手机空闲时,使用AOT生成机器码(只编译热点函数信息,用户操作次数越多,性能越高),这样保证了安装迅速,启动迅速,耗电少

Dalvik和ART是什么,有啥区别?

Dalvik

Dalvik是Google公司自己设计用于Android平台的虚拟机。支持已转换为.dex格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。 Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 很长时间以来,Dalvik虚拟机一直被用户指责为拖慢安卓系统运行速度不如IOS的根源。 2014年6月25日,Android L 正式亮相于召开的谷歌I/O大会,Android L 改动幅度较大,谷歌将直接删除Dalvik,代替它的是传闻已久的ART。

ART

即Android Runtime ART 的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。这个过程叫做预编译(AOT,Ahead-Of-Time)。这样的话,应用的启动(首次)和执行都会变得更加快速。

区别

Dalvik是基于寄存器的,而JVM是基于栈的。 Dalvik运行dex文件,而JVM运行java字节码 自Android 2.2开始,Dalvik支持JIT(just-in-time,即时编译技术)。 优化后的Dalvik较其他标准虚拟机存在一些不同特性:  1.占用更少空间  2.为简化翻译,常量池只使用32位索引   3.标准Java字节码实行8位堆栈指令,Dalvik使用16位指令集直接作用于局部变量。局部变量通常来自4位的“虚拟寄存器”区。这样减少了Dalvik的指令计数,提高了翻译速度。   当Android启动时,Dalvik VM 监视所有的程序(APK),并且创建依存关系树,为每个程序优化代码并存储在Dalvik缓存中。Dalvik第一次加载后会生成Cache文件,以提供下次快速加载,所以第一次会很慢。  Dalvik解释器采用预先算好的Goto地址,每个指令对内存的访问都在64字节边界上对齐。这样可以节省一个指令后进行查表的时间。为了强化功能, Dalvik还提供了快速翻译器(Fast Interpreter)。

对比

ART有什么优缺点呢?

优点: 1、系统性能的显著提升。 2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。 3、更长的电池续航能力。 4、支持更低的硬件。 缺点: 1.机器码占用的存储空间更大,字节码变为机器码之后,可能会增加10%-20% 2.应用的安装时间会变长

.dex .class .odex的区别

.dex是谷歌对.class文件进行了优化后得到的文件格式

  1. .dex去除了.class中冗余的信息,更加轻量
  2. .class内存占用大,不适合移动端,堆栈的加栈模式,加载速度慢,文件IO操作多,类查找慢

.dex文件在虚拟机进行加载时,会预加载成.odex文件,.odex文件对.dex文件进行了优化,避免了重复验证和优化处理,启动时,可直接接在odex文件,提升app启动的速度

简述安装流程

  1. 使用installPackageAsUser判断安装来源

  2. 校验后(权限,存储空间,安全)将apk文件copy至data/app目录

  3. 解析apk信息,覆盖安装或者安装新应用

  4. Dalvik中将dex优化为odex文件

    ART将dex翻译为oat文件(机器码)预编译过程
    复制代码
  5. 创建/data/data/包名 存放应用数据,发送广播结束安装

接口加密

项目中的接口加密技巧

在版本中写死一个密钥,首个接口请求后返回该app的密钥。

对上传的get,post请求的参数以ASCII码进行排序+密钥后生成md5值,添加到header中,传递给服务器

服务器端根据获取到的参数依据同样的规则生成md5后进行比较,如果相同,比较时间戳是否在5秒内,通过则成功

不使用token机制的原因是本产品不存在账号密码等机制,应用可能一直保持在线状态,不会下线,需要协调token的时效性,所以不使用该方案。

缺点:token机制一台机子只允许一个token进行访问,而上述方案没有该限制

常规token校验机制

适用于存在账户名密码的应用

小知识点

ANR条件?

Service执行的操作最多是20s,BroadcastReceiver是10s,Activity是5s,超过时间发生ANR

ANR原理解析

Application Not Responding

  1. 主线程频繁进行IO操作,比如读写文件或者数据库;
  2. 硬件操作如进行调用照相机或者录音等操作;
  3. 多线程操作的死锁,导致主线程等待超时;
  4. 主线程操作调用join()方法、sleep()方法或者wait()方法;
  5. 耗时动画/耗资源行为导致CPU负载过重
  6. system server中发生WatchDog ANR;
  7. service binder的数量达到上限

在应用程序运行过程中,通过send一个延迟的handler,延迟时间为设置的anr时间,如果到时间,没有执行完任务/没有移除handler任务,就会调用appNotResponding方法,触发anr

主要在AMS和WMS中进行控制,通过获取/data/anr/trace.txt进行分析

什么情况下会导致oom?

  1. 大图片存储导致oom,内存溢出
    • 使用软弱引用,当内存不足时,删除Bitmap缓存
    • 调用Bitmap.recycle()快速回收,但是慎用,容易报错
  2. 除了程序计数器之外的内存模型都会发生oom
java.lang.StackOverflowError:死循环/递归调用产生的
复制代码
  1. 关闭流文件、数据库cursor等对象关闭
  2. 创建很多线程会导致oom,因为开辟线程需要对虚拟机栈,本地方法栈,程序计数器,开辟内存,线程数量过多,会导致OOM

如何将应用设置为Launcher?

设置HOME,DEFAULT。

MVC,MVP,MVVM

浅谈MVP in Android

MVC
  • View:对应于布局文件
  • Model:业务逻辑和实体模型
  • Controller:对应于Activity

缺点

  1. Controller(Activity)中处理的逻辑过于繁重,原因是在Activity有太多操作View的代码,View和Controller绑定太过紧密

android中算不上mvc模式,Activity可以叫View层,也可以叫Controller层,所有代码基本都在Activity中

MVP
  • View 对应于Activity,负责View的绘制以及与用户交互
  • Model 依然是业务逻辑和实体模型
  • Presenter 负责完成View于Model间的交互

img

因为Activity任务过于繁重,所以在Activity中提炼出一个Presenter层,该层主要通过接口和View层交互,同时获得View层的反馈

优点

  1. 大大减轻了Activity的逻辑,将View和Presenter做分离,让项目更加简单明确

缺点

  1. 每个功能需要添加一个Presenter类,添加各种借口,增加开发量
  2. Presenter层持有Activity层的引用,需要注意内存泄漏或空指针的问题
MVVM
  1. View:View层
  2. ViewModel层:JetPack中的ViewModel组件,配合LiveData+DataBinding,保证View和ViewModel之间的交互,双向绑定,数据的更新可以实时绑定到界面中。
  3. Model层:数据层

ViewModel层中代替了Presenter的作用,里边做具体的逻辑,ViewModel与Activity的绑定通过反射构建,通过LiveData达到响应式,在Activity中调用ViewModel的逻辑,并实时更新到界面。

优点

  1. ViewModel+LiveData同Activity的生命周期绑定,当Avtivity不存在后,会销毁ViewModel,减少内存泄漏
  2. 提供Activity中多个Fragment的数据共享和逻辑调用
  3. 提供响应式编程,提供解决问题新方向
  4. 优秀的架构思想+官方支持=强大
  5. 代码量少,双向绑定减少UI的更新代码

缺点

  1. 降低了View的复用性,因为添加了很多DataBinding的代码,绑定到Activity中
  2. 难以定位bug,流程许多地方都是自动化更新,执行,无法确定当中哪一个环节出现问题(数据逻辑问题还是界面显示问题)

SharedPreferences commit apply使⽤区别

  1. commit具有回调

  2. apply将信息推送到主存,异步提交到文件,commit同步提交到文件

Bitmap解析

Bitmap是怎么存储图片的?

Bitmap是图片在内存中的表达形式,存储的是有限个像素点,每个像素点存储着ARGB值,代表每个像素所代表的颜色(RGB)和透明度(A)

Bitmap图片的内存是怎么计算的?

图片内存 = 宽 * 高 * 每个像素所占字节
每个像素所占字节和Bitmap.Config有关:

  • ARGB_8888:常用类型,总共32位,4个字节,分别表示透明度和RGB通道。
  • ARGB_4444:2个字节
  • RGB_565:16位,2个字节,只能描述RGB通道。
  • ALPHA_8:1个字节
Bitmap加载优化?不改变图片质量的情况下怎么优化?
  1. 修改Bitmap.Config,降低bitmap每个像素所占用的字节大小,替换格式为RGB_565,这样,内存直接缩小1倍
  2. 修改inSampleSize采样率,降低图片的大小,不影响图片的质量,控制每隔inSampleSize个像素进行一次采集

inSampleSize为1时,为原图大小。大于1时,比如2时,宽高就会缩小为原来的1/2

inSampleSize进行2的幂取整操作,1,2,4,8等

Bitmap内存复用怎么实现?

如果在一个imageView中加载多种不同的Bitmap图片,如果频繁的去创建bitmap,获取内存,释放内存,从而导致大量GC,内存抖动。
在使用Bitmap时,使用inBitmap配合inMutable参数,复用Bitmap内存。在4.4之前,只能复用内存大小相同的Bitmap,4.4之后,新Bitmap内存大小小于或等于复用Bitmap空间的,可以复用

高清大图如何加载?

使用BitmapRegionDecoder属性进行部分加载,根据界面滑动,不断更新部分图片的位置

intent可以传递bitmap吗?

可以,bitmap是parcelable序列化过的,也可以转化成byte[]进行传递

大小受限1M,因为binder的大小是1M,binder的线程数不大于16

Bitmap内存在各个android版本的存储?

Android Bitmap变迁与原理解析(4.x-8.x)

  1. 2.3版本之前:存储在本地内存中,不及时回收(recycler()方法),会触发OOM
  2. 2.3版本到7.0版本:像素数据和对象数据都存储在堆中
  3. 8.0以后:将对象存储在本地内存中(非java内存),通过NativeAllocationRegistry对bitmap进行回收

Fresco 对这个有详细的描述

深拷贝和浅拷贝

深拷贝:拷贝堆区中值
浅拷贝:拷贝堆区中的引用地址

创建一个对象的方式?

  1. 使用new关键字创建
  2. Class.newInstance反射创建
  3. Constructor.newInstance反射创建
  4. 利用clone方法实现(浅拷贝)
  5. 通过反序列化实现(深拷贝)

界面卡顿的原因

  1. UI线程存在耗时操作
  2. 视图渲染时间过长,导致卡顿
  3. 频繁gc,内存抖动

冷启动、温启动、热启动

冷启动:app首次启动,或者上次正常关闭后的启动,需要创建app的进程

  1. 启动系统进程。加载启动app进程,创建app进程
  2. 启动app进程任务。渲染屏幕,加载布局等

温启动:系统进程存在,app非正常关闭,只需要执行第二步,需要创建Activity或者重新布局等

热启动:热启动就是App进程存在,并且Activity对象仍然存在内存中没有被回收。所以热启动的开销最少,这个过程只会把Activity从后台展示到前台,无需初始化,布局绘制等工作

冷启动可以认为是android标准启动流程

Android类加载器

Android从ClassLoader中派生出两个类加载器:PathClassLoader和DexClassLoader

DexClassLoader:是一个可以从包含classes.dex实体的.jar或.apk文件中加载classes的类加载器。可以用于实现dex的动态加载、代码热更新等等。

PathClassLoader:可以操作在本地文件系统的文件列表或目录中的classes

DexClassLoader:能够加载未安装的jar/apk/dex
PathClassLoader:只能加载系统中已经安装过的apk

双亲委派

当一个类需要被初始化加载时,总会先把加载请求传递给父加载器,最终会传递到最高层加载器进行加载。父类加载器会检查是否加载过该类,如果没有加载过,则加载,若无法加载,会传递给子类加载器加载。

为何要使用双亲委派
  1. 首先明确,jvm认为不同加载器加载的类为两个不同的对象,所以为了系统安全性,需要保证相同的类要被同一个类加载器加载
  2. 避免了重复加载,如果父类加载过,直接使用父类加载过的类。
能不能自己写个类叫java.lang.System?

不可以,通过双亲委派该类名被加载为系统类,不会加载自己写的类。
如果非要实现这个效果,需要绕过双亲委派机制,实现自己的类加载器进行加载

插件化

PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。

DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。

阿里系:DeXposed、andfix:从底层二进制入手(c语言)。阿里andFix hook 方法在native的具体字段。 art虚拟机上是一个叫ArtMethod的结构体。通过修改该结构体上有bug的字段来达到修复bug方法的目的, 但这个artMethod是根据安卓原生的结构写死的,国内很多第三方厂家会改写ArtMethod结构,导致替换失效。
腾讯系:tinker:从java加载机制入手。qq的dex插装就类似上面分析的那种。通过将修复的dex文件插入到app的dexFileList的前面,达到更新bug的效果,但是不能及时生效,需要重启。 但虚拟机在安装期间会为类打上CLASS_ISPREVERIFIED标志,是为了提高性能的,我们强制防止类被打上标志是否会有些影响性能
美团robust:是在编译器为每个方法插入了一段逻辑代码,并为每个类创建了一个ChangeQuickRedirect静态成员变量,当它不为空会转入新的代码逻辑达到修复bug的目的。 优点是兼容性高,但是会增加应用体积

  1. startActivity 的时候最终会走到 AMS 的 startActivity 方法
  2. 系统会检查一堆的信息验证这个 Activity 是否合法。
  3. 然后会回调 ActivityThread 的 Handler 里的 handleLaunchActivity
  4. 在这里走到了 performLaunchActivity 方法去创建 Activity 并回调一系列生命周期的方法
  5. 创建 Activity 的时候会创建一个 LoaderApk对象,然后使用这个对象的 getClassLoader 来创建 Activity
  6. 我们查看 getClassLoader() 方法发现返回的是 PathClassLoader,然后他继承自 BaseDexClassLoader
  7. 然后我们查看 BaseDexClassLoader 发现他创建时创建了一个 DexPathList 类型的 pathList对象,然后在 findClass 时调用了 pathList.findClass 的方法
  8. 然后我们查看 DexPathList类 中的 findClass 发现他内部维护了一个 Element[] dexElements的dex 数组,findClass 时是从数组中遍历查找的

sqlite怎么保证数据可见性和线程安全性?

sqlite不支持多个数据库连接进行写操作,但是使用同一个SQLiteHelper连接,可以进行多线程读和写,同一个连接下,sqlite内部有锁机制,不会出现异常,由于有锁的机制,所以是阻塞的,并不是真正的并发

延伸:SharedPreference是线程安全的,内部使用sychronized的

bundle的数据结构,为什么intent要使用bundle?

内部存储ArrayMap,key是int数组,value是object数组,使用Bundle传递对象和对象数组的时候会默认使用序列化,不用我们做处理。

key是hash值,value[]是存储的数据key值,和value值,采用二分法排序,使用二分法查找

优势:省内存,小数据上占优势。

大图传输

文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。第一个打开的文件是0,第二个是1,依此类推。
socket:如果是网络中,会使用ip号+port号方式为套接字地址,但是如果同一台主机上两个进程间通信用套接字,还需要指定ip地址,有点过于繁琐. 这个时候就需要用到UNIX Domain Socket, 简称UDS,UDS不需要IP和Port, 而是通过一个文件名来表示 (int, (AF_UNIX,文件路径))

  1. 直接传输Bitmap,Bitmap实现Parcelable序列化,所以可以直接在内存中传输,所以可以直接通过Bundle传输过去,但是限制大小为1M。
  2. 可以存储在文件中,传输一个文件路径过去
  3. 使用Bundle的putBinder方法,通过Binder发送,其实putBinder传输过去的只是一个文件描述符fd,获取到fd后,从共享内存中获取到Bitmap

而用Intent/bundle直接传输的时候,会禁用文件描述符fd,只能在parcel的缓存区中分配空间来保存数据,所以无法突破1M的大小限制

webview

android调用js代码
  1. 通过loadUrl的方法直接调用js方法,会刷新页面,没有返回值
  2. evaluateJavascript()方法,android4.4以后使用,不会刷新页面,有返回值
js调用android代码
  1. addJavascriptInterface()方法进行对象映射,存在漏洞 4.2以下

    创建一个类,使用@JavascriptInterface注解标识方法,使用addJavascriptInterface()为js创建对象

    漏洞:

    • 通过反射获取到这个类的所有方法和系统类,进行获取信息泄漏
    • 4.2后添加注解避免漏洞攻击
  2. webViewClient.shouldOverrideUrlLoading()拦截url 不存在漏洞

    在js中传入url,携带参数,拼接到url中,在shouldOverrideUrlLoading获取

  3. 触发js弹窗向android发消息。之后再回调中通过2方式的url传输消息

内存泄漏:加弱引用即可

要实现可以拖动的View该怎么做?

使用windowManager的updateViewLayout方法吗,实时传入手指的坐标就可以移动window

btn.setOnTouchListener { v, event ->
    val index = event.findPointerIndex(0)
    when (event.action) {
        ACTION_MOVE -> {
            windowParams.x = event.getRawX(index).toInt()
            windowParams.y = event.getRawY(index).toInt()
            windowManager.updateViewLayout(btn, windowParams)
        }
        else -> {
        }
    }
    false

}
复制代码

Android新知识

RxJava

响应式编程:根据响应去触发动作

使用观察者模式调用,使用于逻辑复杂的操作可以使用Rxjava做异步处理

  1. 按钮短300ms内不允许重复点击
RxView.clicks(button).debounce(300, TimeUnit.MILLISECONDS).subscribe(new Action1<Void>() {
            @Override
            public void call(Void aVoid) {
                Log.i("test", "clicked");
            }
        });
复制代码
  1. 轮询,定时执行
//每隔两秒执行一次
   Observable.interval(2, 2, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                //TODO WHAT YOU WANT
            }
        });
复制代码
  1. 消息传递,可取代EventBus
//发布消息
RxBus.getInstance().post("SomeChange");

//接收消息并处理
Subscription mSubscription = RxBus.getInstance().toObserverable(String.class).subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                handleRxMsg(s);
            }
});

//取消订阅
mSubscription.unsubscribe();
复制代码

Jetpack

一系列辅助android开发者的使用工具,统称Jetpack

提供新组件,比如导航组件,分页组件,切片组件等,例如mvvm中的LiveData,viewmodel都属于Jetpack组件

paging,room,livedata,viewmodel,lifecycler,compose,databinding,viewbinding

Jetpack在androidx中进行发布,androidx也属于Jetpack

AndroidX

androidx空间中包含Jetpack库,

之前使用android-support-v4(最低支持1.6) 和 android-support-v7(最低支持2.1)库做支持,androidx提出后,对support-v4 和 support-v7库不再做维护

MVVM

LiveData使用观察者模式观察生命周期,在onStart和onResume时回调onChanged,确保liveData对象内存泄漏。

DataBind 双向绑定,将view和model进行绑定,一方变化会导致另一方变化。

缺点:

  1. 难以排查bug,不知道是view的bug还是model的bug,bug会转移
  2. 不能复用view,因为绑定不同的model
文章分类
Android
文章标签