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无法拦截
如何解决滑动冲突
-
外部拦截法:
重写父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; } 复制代码
-
内部拦截法:
在子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的加载流程
- 通过Activity的setContentView方法间接调用Phonewindow的setContentView(),在PhoneWindow中通过getLayoutInflate()得到LayoutInflate对象
- 通过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()中的具体流程是什么?
- 绘制背景:drawBackground(canvas)
- 绘制自己的内容:onDraw(canvas)
- 绘制Children:dispatchDraw(canvas)
- 绘制装饰: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布局
对比
- 同层级的布局,LinearLayout<RelatvieLayout=ConstraintLayout,因为LinearLayout执行onMeasure一遍,RelativeLayout执行两遍
- LinearLayout会增加层级深度,RelativeLayout减少层级,所以通常下使用RelativeLayout,如果层级简单则使用LinearLayout
RelativeLayout的子View如果高度和RelativeLayout不同,会引发效率问题
setContentView的执行过程
- 初始化windows
- 绑定ui布局
什么时候可以获得view的宽高
因为onMeasure和生命周期不同步,所以不能在onCreate,onStart,onResume中进行获取操作,
- 在view.post方法中进行获取,内部实现是handler机制,回调的时候已经执行完了
- 在onWindowFocusChanged获取焦点后,view的绘制完成,可以在这里拿到view的宽高
- 使用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));
}
});
复制代码
- 手动调用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执行的具体过程
- Activity实例化,执行attach方法,在attach中创建PhoneWindow
- 执行onCreate方法,执行setContentView,先调用phoneWindow.setContentView(),然后开始根据不同的主题创建DecorView的结构,传入我们的xml文件,生成一个多结构的View
- Activity调用onResume方法,调用WindowManager.addView()方法,随后在addView()方法中创建ViewRootImpl
- 接着调用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中有三个重要方:
- 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中进行查找
如图所示,如果只是删除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相同的就取出来绑定并复用。
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的优化
放大缓存大小和缓存池大小
- 再滑动过程中,不论上滑还是下滑都会从mCachedViews中查找缓存,如果滑动频繁,可以通过
RecyclerView.setItemViewCacheSize(...)
方法增大mCachedViews的大小,减少onBindViewHolder()和onCreateViewHolder()调用 - 放大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的优势是什么?
- 屏幕外缓存可以直接在mCacheView()中复用,不需要重新BindView
- recyclerPool可以提供给多个RecyclerView使用,在特定场景下,如viewpaper+多个列表页下有优势.
- 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.被同时标记为remove
和invalid
;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方法,如果该方法超过一个任务的常规耗时,就会导致主线程卡顿
解决方法:
-
放大mCacheView和RecyclerPool的大小,提高复用率,减少渲染
-
图片在滑动结束后再进行加载,避免在滑动的时候向主线程做更新
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(); } } }); 复制代码
在滑动过程中停止加载,在滑动结束后恢复加载
-
使用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); 复制代码
-
减少布局的嵌套和层级,减少过度绘制,尽量自定义view
-
如果Item高度固定,调用
RecyclerView.setHasFixedSize(true);
来避免requestLayout
浪费资源 -
可以关闭动画,减少RecyclerView的渲染次数
RecyclerView的自适应高度
- 使用瀑布流布局StaggeredGridLayoutManager
- 重写LinearLayoutManager,onMeasure中重新测量子布局的大小
RecyclerView嵌套RecyclerView滑动冲突,NestedScrollView嵌套RecyclerView
- 同方向的情况下会造成滑动冲突,默认外层的RecyclerView可滑动
一般有两种处理方案:内部拦截法和外部拦截法
这里推荐内部拦截法,通过设置requestDisallowInterceptTouchEvent(true)时,不让父RecyclerView拦截子类的事件 - ScrollView嵌套RecyclerView同样可以使用这个方法解决。也可以使用NestedScrollView,该类就是为了解决滑动冲突问题,可以保证两View类都可以滑动,但是需要设置RecyclerView.setNestedScrollingEnabled(false),取消RecyclerView本身的滑动效果。解决滑动的卡顿感
动画
简述
帧动画:一连串的图片进行连贯的播放,形成动画。
补间动画:通过xml文件实现,实现 alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转),通过不断的绘制view,看起来移动了效果,实际上view没有变化,还在原地
属性动画:对于对象属性的动画,也可以使用xml配置,但是推荐代码配置,比xml更加方便。通过不断改变自己view的属性值,真正的改变view
所有的补间动画都可以用属性动画实现
属性动画和补间动画的区别
- 补间动画虽然移动了,但是点击的还是原来的位置,点击事件允许触发。而属性动画不是,所以我们可以确认,属性动画才是真正实现了View的移动,补间动画的view其实只是在其他地方绘制了一个影子
- Activity退出时,没有关闭动画,属性动画会导致Activity无法释放的内存泄漏,而补间动画不会发生这样的情况
- xml的补间动画复用率极高,在页面切换过程中都有很好的效果
帧动画避免大图,否则会带来oom
属性动画中的差值器和估值器是什么?
差值器:定义动画随时间流逝的变化规律。通俗点就是动画的执行速度的变化,可以是由缓即快,由快即缓,也可以是匀速,也可以是弹性动画效果 ,LinearInterpolator(匀速差值器)
估值器:定义从初始值过渡到结束值的规则定义,TypeEvaluator,可以通俗的理解为位置的移动
android系统启动流程
android系统架构
简述系统启动流程
从系统层看:
- linux 系统层
- Android系统服务层
- Zygote
从开机启动到Home Launcher:
- 启动bootloader (小程序;初始化硬件)
- 加载系统内核 (先进入实模式代码在进入保护模式代码)
- 启动init进程(用户级进程 ,进程号为1)
- 启动Zygote进程(初始化Dalvik VM等)
- 启动Runtime进程
- 启动本地服务(system service)
- 启动 HomeLauncher
第一个启动的进程是什么?
init进程,其他进程都是fork这个进程的
init进程孵化出了什么进程?
- 守护进程
- Zygote进程,负责孵化应用进程
- MediaServer进程
Zygote进程做了什么?
- 创建Socket服务端
- 加载虚拟机
- SystemServer进程
- fork第一个应用进程—Launcher
为什么要创建Socket服务端?
- ServiceManager不能保证在孵化Zygote进程时就初始化好了,所以无法使用Binder
- Binder属于多线程操作,fork不允许多线程操作,容易发生死锁,所以使用Socket
app启动流程
- 用户点击 icon
- 系统开始加载和启动应用
- 应用启动:开启空白(黑色)窗口
- 创建应用进程
- 初始化Application
- 启动 UI 线程
- 创建第一个 Activity
- 解析(Inflater)和加载内容视图
- 布局(Layout)
- 绘制(Draw)
源码分析
- LauncherActivity.startActivitySafely(intent):使用intent启动
- Activity.startActivity(intent):
- Activity.startActivityForResult(intent):获取ApplicationThread成员变量,是一个Binder对象
- Instrumentation.execStartActivity:ActivityManagerService的远程接口
- ActivityManagerProxy.startActivity:通过Binder进入AMS
- ActivityManagerService.startActivity
- ActivityStack.startActivityMayWait:解析MainActivity的信息
- ActivityStack.startActivityLocked:创建即将要启动的Activity的相关信息
- ActivityStack.startActivityUncheckedLocked:获取intent标志位,新建Task栈,添加到AMS中
- Activity.resumeTopActivityLocked:查看LauncherActivity状态,新建Activity的状态
- ActivityStack.startPausingLocked:停止LauncherActivity,onPause
- ApplicationThreadProxy.schedulePauseActivity
- ApplicationThread.schedulePauseActivity
- ActivityThread.queueOrSendMessage:在主线程通过Handler发送消息
- H.handleMessage:Handler的回调
- ActivityThread.handlePauseActivity:pause LauncherActivity
- ActivityManagerProxy.activityPaused:进入AMS中的onPause事件
- ActivityManagerService.activityPaused
- ActivityStack.activityPaused
- ActivityStack.completePauseLocked
- ActivityStack.resumeTopActivityLokced:LauncherActivity已经onPause了
- ActivityStack.startSpecificActivityLocked
- ActivityManagerService.startProcessLocked:创建新进程
- ActivityThread.main:app入口,添加looper循环
- ActivityManagerProxy.attachApplication:通过Binder进入AMS中
- ActivityManagerService.attachApplication
- ActivityManagerService.attachApplicationLocked
- ActivityStack.realStartActivityLocked
- ApplicationThreadProxy.scheduleLaunchActivity:进入ApplicationThread
- ApplicationThread.scheduleLaunchActivity
- ActivityThread.queueOrSendMessage
- H.handleMessage
- ActivityThread.handleLaunchActivity
- ActivityThread.performLaunchActivity:进入onCreat方法
- 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,你可以启动
这里以启动微信为例子说明
- Launcher通知AMS 要启动微信了,并且告诉AMS要启动的是哪个页面也就是首页是哪个页面
- AMS收到消息告诉Launcher知道了,并且把要启动的页面记下来
- Launcher进入Paused状态,告诉AMS,你去找微信吧
上述就是Launcher和AMS的交互过程
- AMS检查微信是否已经启动了也就是是否在后台运行,如果是在后台运行就直接启动,如果不是,AMS会在新的进程中创建一个ActivityThread对象,并启动其中的main函数。
- 微信启动后告诉AMS,启动好了
- AMS通过之前的记录找出微信的首页,告诉微信应该启动哪个页面
- 微信按照AMS通知的页面去启动就启动成功了。
Activity启动流程
参照app的启动流程
- ApplicationThread:ActivityThread的内部类,负责和AMS进行Binder通信
- ActivityManagerService:服务端对象,负责管理系统中所有的Activity
Activity 启动过程是由 ActivityMangerService(AMS) 来启动的,底层 原理是 Binder实现的 最终交给 ActivityThread 的 performActivity 方法来启动她
ActivityThread大概可以分为以下五个步骤
- 通过ActivityClientRecord对象获取Activity的组件信息
- 通过Instrument的newActivity使用类加载器创建Activity对象
- 检验Application是否存在,不存在的话,创建一个,保证 只有一个Application
- 通过ContextImpl和Activity的attach方法来完成一些初始化操作
- 调用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打包流程
- aapt阶段,打包res目录,生成R.java
- AIDL阶段,生成java文件
- java编译器。将java文件通过javac编译生成
.class
文件 - dex阶段,生成
.dex
文件 - apk打包阶段,将文件打包成为apk文件
- 签名阶段,对apk进行签名
- 整理apk文件
aapt和aapt2的区别?
aapt是全量编译,打包res目录,生成R文件
aapt2是差量编译,将变化的res目录进行重新打包,修改R文件
aapt2中存在两部分,编译和链接
编译:将资源文件编译为二进制文件
链接:将编译后二进制文件进行合并,生成独立的文件
在需要差量的时候,只需要重新编译二进制文件,再将这些二进制文件生成新的文件即可
apk的组成
- AndroidManifest.xml
- assets(项目中assets目录)
- classes.dex
- lib库
- META-INF(校验文件)
- res(资源文件)
- 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文件大小的四分之一)
安装过程
- 将apk文件复制到/data/app目录
- 解析apk信息
- dexopt操作(将dex文件优化为odex文件)
- 更新权限信息
- 发送安装完成广播
Android虚拟机发展史
- android初期,Dalvik负责加载dex/odex文件
- 2.2版本,JIT(即时编译)初次加入,每次启动的时候编译,耗时,耗电
- 4.4版本引入ART(Android RunTime)和AOT(Ahead-of-time)(运行前编译成机器码),与Dalvik共存
- 5.0版本全部采用ART编译器,不耗时,不耗电,在安装期间比较慢而已,而且会占用额外的控件存储机器码
- 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文件进行了优化后得到的文件格式
- .dex去除了.class中冗余的信息,更加轻量
- .class内存占用大,不适合移动端,堆栈的加栈模式,加载速度慢,文件IO操作多,类查找慢
.dex文件在虚拟机进行加载时,会预加载成.odex文件,.odex文件对.dex文件进行了优化,避免了重复验证和优化处理,启动时,可直接接在odex文件,提升app启动的速度
简述安装流程
-
使用installPackageAsUser判断安装来源
-
校验后(权限,存储空间,安全)将apk文件copy至data/app目录
-
解析apk信息,覆盖安装或者安装新应用
-
Dalvik中将dex优化为odex文件
ART将dex翻译为oat文件(机器码)预编译过程 复制代码
-
创建/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
- 主线程频繁进行IO操作,比如读写文件或者数据库;
- 硬件操作如进行调用照相机或者录音等操作;
- 多线程操作的死锁,导致主线程等待超时;
- 主线程操作调用join()方法、sleep()方法或者wait()方法;
- 耗时动画/耗资源行为导致CPU负载过重
- system server中发生WatchDog ANR;
- service binder的数量达到上限
在应用程序运行过程中,通过send一个延迟的handler,延迟时间为设置的anr时间,如果到时间,没有执行完任务/没有移除handler任务,就会调用appNotResponding方法,触发anr
主要在AMS和WMS中进行控制,通过获取/data/anr/trace.txt进行分析
什么情况下会导致oom?
- 大图片存储导致oom,内存溢出
- 使用软弱引用,当内存不足时,删除Bitmap缓存
- 调用Bitmap.recycle()快速回收,但是慎用,容易报错
- 除了程序计数器之外的内存模型都会发生oom
java.lang.StackOverflowError:死循环/递归调用产生的
复制代码
- 关闭流文件、数据库cursor等对象关闭
- 创建很多线程会导致oom,因为开辟线程需要对虚拟机栈,本地方法栈,程序计数器,开辟内存,线程数量过多,会导致OOM
如何将应用设置为Launcher?
设置HOME,DEFAULT。
MVC,MVP,MVVM
MVC
- View:对应于布局文件
- Model:业务逻辑和实体模型
- Controller:对应于Activity
缺点:
- Controller(Activity)中处理的逻辑过于繁重,原因是在Activity有太多操作View的代码,View和Controller绑定太过紧密
android中算不上mvc模式,Activity可以叫View层,也可以叫Controller层,所有代码基本都在Activity中
MVP
- View 对应于Activity,负责View的绘制以及与用户交互
- Model 依然是业务逻辑和实体模型
- Presenter 负责完成View于Model间的交互
因为Activity任务过于繁重,所以在Activity中提炼出一个Presenter层,该层主要通过接口和View层交互,同时获得View层的反馈
优点
- 大大减轻了Activity的逻辑,将View和Presenter做分离,让项目更加简单明确
缺点
- 每个功能需要添加一个Presenter类,添加各种借口,增加开发量
- Presenter层持有Activity层的引用,需要注意内存泄漏或空指针的问题
MVVM
- View:View层
- ViewModel层:JetPack中的ViewModel组件,配合LiveData+DataBinding,保证View和ViewModel之间的交互,双向绑定,数据的更新可以实时绑定到界面中。
- Model层:数据层
ViewModel层中代替了Presenter的作用,里边做具体的逻辑,ViewModel与Activity的绑定通过反射构建,通过LiveData达到响应式,在Activity中调用ViewModel的逻辑,并实时更新到界面。
优点
- ViewModel+LiveData同Activity的生命周期绑定,当Avtivity不存在后,会销毁ViewModel,减少内存泄漏
- 提供Activity中多个Fragment的数据共享和逻辑调用
- 提供响应式编程,提供解决问题新方向
- 优秀的架构思想+官方支持=强大
- 代码量少,双向绑定减少UI的更新代码
缺点
- 降低了View的复用性,因为添加了很多DataBinding的代码,绑定到Activity中
- 难以定位bug,流程许多地方都是自动化更新,执行,无法确定当中哪一个环节出现问题(数据逻辑问题还是界面显示问题)
SharedPreferences commit apply使⽤区别
-
commit具有回调
-
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加载优化?不改变图片质量的情况下怎么优化?
- 修改Bitmap.Config,降低bitmap每个像素所占用的字节大小,替换格式为RGB_565,这样,内存直接缩小1倍
- 修改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)
- 2.3版本之前:存储在本地内存中,不及时回收(recycler()方法),会触发OOM
- 2.3版本到7.0版本:像素数据和对象数据都存储在堆中
- 8.0以后:将对象存储在本地内存中(非java内存),通过NativeAllocationRegistry对bitmap进行回收
Fresco 对这个有详细的描述
深拷贝和浅拷贝
深拷贝:拷贝堆区中值
浅拷贝:拷贝堆区中的引用地址
创建一个对象的方式?
- 使用new关键字创建
- Class.newInstance反射创建
- Constructor.newInstance反射创建
- 利用clone方法实现(浅拷贝)
- 通过反序列化实现(深拷贝)
界面卡顿的原因
- UI线程存在耗时操作
- 视图渲染时间过长,导致卡顿
- 频繁gc,内存抖动
冷启动、温启动、热启动
冷启动:app首次启动,或者上次正常关闭后的启动,需要创建app的进程
- 启动系统进程。加载启动app进程,创建app进程
- 启动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
双亲委派
当一个类需要被初始化加载时,总会先把加载请求传递给父加载器,最终会传递到最高层加载器进行加载。父类加载器会检查是否加载过该类,如果没有加载过,则加载,若无法加载,会传递给子类加载器加载。
为何要使用双亲委派
- 首先明确,jvm认为不同加载器加载的类为两个不同的对象,所以为了系统安全性,需要保证相同的类要被同一个类加载器加载
- 避免了重复加载,如果父类加载过,直接使用父类加载过的类。
能不能自己写个类叫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的目的。
优点是兼容性高,但是会增加应用体积
- startActivity 的时候最终会走到 AMS 的 startActivity 方法
- 系统会检查一堆的信息验证这个 Activity 是否合法。
- 然后会回调 ActivityThread 的 Handler 里的 handleLaunchActivity
- 在这里走到了 performLaunchActivity 方法去创建 Activity 并回调一系列生命周期的方法
- 创建 Activity 的时候会创建一个 LoaderApk对象,然后使用这个对象的 getClassLoader 来创建 Activity
- 我们查看 getClassLoader() 方法发现返回的是 PathClassLoader,然后他继承自 BaseDexClassLoader
- 然后我们查看 BaseDexClassLoader 发现他创建时创建了一个 DexPathList 类型的 pathList对象,然后在 findClass 时调用了 pathList.findClass 的方法
- 然后我们查看 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,文件路径))
- 直接传输Bitmap,Bitmap实现Parcelable序列化,所以可以直接在内存中传输,所以可以直接通过Bundle传输过去,但是限制大小为1M。
- 可以存储在文件中,传输一个文件路径过去
- 使用Bundle的putBinder方法,通过Binder发送,其实putBinder传输过去的只是一个文件描述符fd,获取到fd后,从共享内存中获取到Bitmap
而用Intent/bundle直接传输的时候,会禁用文件描述符fd,只能在parcel的缓存区中分配空间来保存数据,所以无法突破1M的大小限制
webview
android调用js代码
- 通过loadUrl的方法直接调用js方法,会刷新页面,没有返回值
- evaluateJavascript()方法,android4.4以后使用,不会刷新页面,有返回值
js调用android代码
-
addJavascriptInterface()方法进行对象映射,
存在漏洞
4.2以下创建一个类,使用@JavascriptInterface注解标识方法,使用addJavascriptInterface()为js创建对象
漏洞:
- 通过反射获取到这个类的所有方法和系统类,进行获取信息泄漏
- 4.2后添加注解避免漏洞攻击
-
webViewClient.shouldOverrideUrlLoading()拦截url
不存在漏洞
在js中传入url,携带参数,拼接到url中,在shouldOverrideUrlLoading获取
-
触发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做异步处理
- 按钮短300ms内不允许重复点击
RxView.clicks(button).debounce(300, TimeUnit.MILLISECONDS).subscribe(new Action1<Void>() {
@Override
public void call(Void aVoid) {
Log.i("test", "clicked");
}
});
复制代码
- 轮询,定时执行
//每隔两秒执行一次
Observable.interval(2, 2, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
@Override
public void call(Long aLong) {
//TODO WHAT YOU WANT
}
});
复制代码
- 消息传递,可取代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进行绑定,一方变化会导致另一方变化。
缺点:
- 难以排查bug,不知道是view的bug还是model的bug,bug会转移
- 不能复用view,因为绑定不同的model