接触Flutter开发一段时间后发现自己对Flutter渲染流程重要的一环Layer的认知比较少,虽然Flutter对Widget的封装非常全面了开发者基本上只要面向Widget编程就可以完成绝大部分的功能,但是它作为一个UI框架我们还是需要尽可能的掌握它渲染体系的来龙去脉,因此借此篇文章简单介绍笔者对Layer的探索。
参与UI的构建和显示涉及到两个线程分别是界面线程(UI Thread)和光栅线程(GPU Thread),UI线程做构建流水线工作(开发者编写的代码), 光栅线程做UI绘制工作(图形库 Skia 在此线程上运行)。
1,GPU每隔一定的时间发出一个Vsycn信号这个时间由屏幕的刷新率决定,以60HZ的刷新率为例那么它的时间间隔就是1000/60 = 16.7 ms一次。
2,UI线程收到Vsycn信号后就会做UI的构建工作(需要在16.7ms内完成否则出现丢帧),然后发送到光栅线程GPU线程。
3,GPU Thread收到UI Thread发来的UI数据后就会通过Skia去上屏渲染。
上图摘自Flutter官网介绍,从上图可以看到有个Layer Tree这也是本文探索的目标。
Flutter的开发更像是面向Widget编程,Widget内部又封装了Element以及RenderObject 那么我们先从Flutter中的三棵树说起:
main(){
runApp(MaterialApp(
home: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyWidget(),
MyWidget()
],
),
));
}
class MyWidget extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 200,
child: Text('MyWidget',style: TextStyle(fontSize: 25),),);
}
}
上述代码中Widget , Element , RenderObject三棵树的对应关系:
从上图可以看出Widget和Element的数量是一一对应的,而RenderObject不是。查看framework.dart源码后可以发现只有RenderObjectWidget的派生类才会有RenderObject,其他的Widget都不具备渲染能力。
当Flutter收到Vsycn的时候就会做UI的构建工作,最终会调用RendererBinding的drawFrame()
@protected
void drawFrame() {
assert(renderView != null);
//1,布局逻辑,确定大小
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
//2,绘制逻辑,拿到SkCanvas绘制到layer上。具体逻辑见RenderObject中的paint方法
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
renderView.compositeFrame()最终生成UI数据发送到GPU实现渲染,RenderView是Flutter中根部的RenderObject。compositeFrame的核心代码如下:
void compositeFrame() {
//创建SceneBuilder,获取到引擎层的句柄
final ui.SceneBuilder builder = ui.SceneBuilder();
//Scene 最最终是通过SceneBuilder生成的,也是引擎层的句柄,此处的layer就是根部的layer,它会合成所有的layer
final ui.Scene scene = layer!.buildScene(builder);
//发送Scene到引擎
_window.render(scene);
scene.dispose();
}
在上述代码中可以找到layer的身影, layer!.buildScene(builder)就是做Layer Tree的合成。此处的layer是一个TransformLayer是ContainerLayer的子类。
在绘制过程中渲染树RenderObject Tree将生成一个图层树Layer Tree,Layer Tree合成后发送到引擎渲染上屏。大多数Layer的特性都可以更改,并且可以将图层移动到不同的父层,且Layer树不会保持其自身的脏状态。要合成树先要在根部的Layer创建SceneBuilder对象,并调用Layer中的addToScene方法添加到SceneBuilder上(Flutter中默认根部的layer是一个TransformLayer)。
layer分为五大类:
接下来用几个示例来了解Flutter常见的Layer
PictureLayer是Flutter中最常使用到的layer,先看看它的类结构
class PictureLayer extends Layer {
//省略无关代码
ui.Picture? _picture;
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(picture != null);
builder.addPicture(layerOffset, picture!, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
}
}
PictureLayer中有一个成员属性Picture, Picture是Engine层绘制图像重要的一个环节,可以参考Flutter官方的示例:
[https://github.com/flutter/flutter/blob/449f4a6673f6d89609b078eb2b595dee62fd1c79/examples/layers/raw/canvas.dart]
按照官方的示例我们精简一下代码流程:
final recorder = ui.PictureRecorder();
///基于画板创建的画布
final canvas = Canvas(recorder, cullRect);
///缩放因子
final ratio = ui.window.devicePixelRatio;
///设置缩放比
canvas.scale(ratio, ratio);
canvas.drawRect(Rect.fromLTRB(0, 0, 200, 200), Paint()..color = Colors.blue);
///录制结束,生成一个Picture
Picture picture = recorder.endRecording();
SceneBuilder sceneBuilder = ui.SceneBuilder();
//对应PictureLayer中的addToScene方法
sceneBuilder.addPicture(Offset(0, 0), picture);
sceneBuilder.pop();
///生成scene
final scene = sceneBuilder.build();
//通知引擎在合适的时机渲染
ui.window.render(scene);
scene.dispose();
上面的代码就可以渲染出一个图层:
小结: 脱离Widget后我们也可以直接使用framework的api渲染出图像。
在了解到Flutter的绘制流程后我们再来看RenderObject的绘制流程:
void paint(PaintingContext context, Offset offset) { //通过重写paint决定绘制逻辑}
PaintingContext的构造方法:
PaintingContext(this._containerLayer, this.estimatedBounds)
void _startRecording() {
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
_containerLayer.append(_currentLayer!);
}
上述流程可以用如下代码简单替换,下面的代码可以绘制出一个PictureLayer的图像:
PaintingContext context = PaintingContext(rootLayer,Rect.fromLTRB(0, 0, 1000, 1000));
//模拟paint方法
context.canvas.drawRect(Rect.fromLTRB(200, 200, 800, 800), Paint()..color = Colors.blue);
context.stopRecordingIfNeeded();
final SceneBuilder builder = ui.SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
ui.window.render(scene);
scene.dispose();
接下来绘制多个PictureLayer的图像,每一帧只绘制了一个颜色的Rect在PictureLayer上,通过合成Layer达到一帧显示多个Rect。
main() async {
ui.window.onBeginFrame = beginFrame;
ui.window.onDrawFrame = draw1stFrame;
///画第一帧
ui.window.scheduleFrame();
///画第二帧,
await Future.delayed(Duration(milliseconds: 500),(){
ui.window.onDrawFrame = draw2ndFrame;
ui.window.scheduleFrame();
});
///画第三帧
await Future.delayed(Duration(milliseconds: 500),(){
ui.window.onDrawFrame = draw3rdFrame;
ui.window.scheduleFrame();
});
///画第四帧
await Future.delayed(Duration(milliseconds: 500),(){
ui.window.onDrawFrame = draw4thFrame;
ui.window.scheduleFrame();
});
}
void beginFrame(Duration duration) {
}
OffsetLayer rootLayer = OffsetLayer();
void draw1stFrame(){
print('draw1stFrame');
PaintingContext context = PaintingContext(rootLayer,Rect.fromLTRB(0, 0, 1000, 1000));
context.canvas.drawRect(Rect.fromLTRB(200, 200, 800, 800), Paint()..color = Colors.blue);
context.stopRecordingIfNeeded();
final SceneBuilder builder = ui.SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
ui.window.render(scene);
scene.dispose();
}
void draw2ndFrame(){
print('draw2ndFrame');
PaintingContext context = PaintingContext(rootLayer,Rect.fromLTRB(0, 0, 1000, 1000));
context.canvas.drawRect(Rect.fromLTRB(400, 400, 1000, 1000), Paint()..color = Colors.red);
context.stopRecordingIfNeeded();
final SceneBuilder builder = ui.SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
ui.window.render(scene);
scene.dispose();
}
void draw3rdFrame(){
print('draw3rdFrame');
PaintingContext context = PaintingContext(rootLayer,Rect.fromLTRB(0, 0, 1200, 1200));
context.canvas.drawRect(Rect.fromLTRB(600, 600, 1200, 1200), Paint()..color = Colors.yellow);
context.stopRecordingIfNeeded();
final SceneBuilder builder = ui.SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
ui.window.render(scene);
scene.dispose();
}
void draw4thFrame(){
print('draw4thFrame');
PaintingContext context = PaintingContext(rootLayer,Rect.fromLTRB(0, 0, 1000, 2000));
context.canvas.drawRect(Rect.fromLTRB(200, 800, 800, 1400), Paint()..color = Colors.deepPurpleAccent);
context.stopRecordingIfNeeded();
final SceneBuilder builder = ui.SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
ui.window.render(scene);
scene.dispose();
}
多个PictureLayer效果图:
以上四个色块代表都有自己的PictureLayer,然后append到根部的rootLayer上合成一帧数据。
每一个Layer都对应着SceneBuilder一个api操作,PictureLayer对应的是SceneBuilder.addPicture方法(可以查看具体Layer中addToScene方法),除了PictureLayer还有类型的Layer,下面就简单介绍几种:
1,TextureLayer 外接纹理图层
SceneBuilder.addTexture
2,ClipPathLayer 剪裁图层 ---> 剪裁子图层
SceneBuilder.pushClipPath
注意:图层的剪裁是比较消耗性能的,尽可能避免使用。
3,ColorFilterLayer 滤色器图层 ---> 滤色子图层
SceneBuilder.pushColorFilter
其中pushColorFilter和pushClipPath这类的方法会得到一个EngineLayer,EngineLayer是dart层持有Engine层的一个引用,其他还有很多图层操作的API这里就不一一举例了。
通过上面的示例,我们了解到RenderObject最终的绘制都是在Layer上的,它是通过PaintingContext和Layer关联上的
在Renderobject中有个isRepaintBoundary的方法,默认返回值是false,当它的返回值是true的时候就不会使用父节点的PaintingContext,而是重新创建一个PaintingContext来绘制。PaintingContext中会创建一个新的Picturelayer。
在RenderObject中有一个isRepaintBoundary的方法,通过重写isRepaintBoundary方法的返回值为true时可以做指定当前RenderObject节点使用独立的PictureLayer进行渲染。
@override
bool get isRepaintBoundary => super.isRepaintBoundary;
代码逻辑如下:
PaintingContext.paintChild
void paintChild(RenderObject child, Offset offset) {
//child isRepaintBoundary = true 就会
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
//合成
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
assert(() {
if (debugProfilePaintsEnabled)
Timeline.finishSync();
return true;
}());
}
PaintingContext._compositeChild
void _compositeChild(RenderObject child, Offset offset) {
// Create a layer for our child, and paint the child into it.
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
}
final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(child._layer!);
}
PaintingContext.repaintCompositedChild ---> PaintingContext._repaintCompositedChild
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
child._layer = childLayer = OffsetLayer();
} else {
childLayer.removeAllChildren();
}
//创建新的PaintingContext
childContext ??= PaintingContext(child._layer!, child.paintBounds);
//绘制child
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();
}
通过自定义一个RandomColorRenderObject,重写isRepaintBoundary的返回值,分别返回true和false。点击文字会发现返回false的时候RandomColorRenderObject的piant会被调用,而返回true的时候RandomColorRenderObject的piant不会会被调用。
void main() {
runApp(MaterialApp(
home: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(child:RandomColorWidget(),),
MyText(),
],
),
));
}
class MyText extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MyTextState();
}
}
class MyTextState extends State {
String text = _text();
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 300,
child: GestureDetector(
child: Text(text),
onTap: () {
setState(() {
text = _text();
});
},
),
);
}
}
String _text() {
return "12345678${Random().nextInt(10)}";
}
class RandomColorWidget extends RenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) {
return RandomColorRenderObject(context);
}
@override
RandomColorElement createElement() {
return RandomColorElement(this);
}
}
class RandomColorElement extends RenderObjectElement {
RandomColorElement(RenderObjectWidget widget) : super(widget);
}
class RandomColorRenderObject extends RenderBox {
RandomColorRenderObject(BuildContext context);
ViewConfiguration createViewConfiguration() {
final double devicePixelRatio = window.devicePixelRatio;
return ViewConfiguration(
size: window.physicalSize / devicePixelRatio,
devicePixelRatio: devicePixelRatio,
);
}
@override
Rect get paintBounds {
return Rect.fromLTRB(
0,
0,
200 ,
200 );
}
@override
void performLayout() {
size = paintBounds.size;
// print('RandomColorRenderObject performLayout');
}
@override
bool get isRepaintBoundary => true;
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
context.canvas.save();
///画Rect
context.canvas.drawRect(
Rect.fromLTWH(0, 0, 200, 200), Paint()..color = _randomColor());
context.canvas.restore();
}
@override
Rect get semanticBounds => paintBounds;
}
Color _randomColor(){
return Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255));
}
优势 : 当某个Layer的绘制很消耗性能又不会频繁的刷新,在不影响其他Layer的前提下可以通过复用提升性能。这样其它的RenderObject在刷新重绘的时候这个Layer不会被重绘。
通过此次探索希望能帮助大家加深对Layer的认知, 简而言之 RenderObject只负责绘制逻辑而 Layer才是最终输出到Skia的产物。不同的Layer对应着SceneBuilder中图层操作不同的Api, 因篇幅有限此次就不表述其它Layer的效果及作用了, 有兴趣的同学可以自行参照SceneBuilder的源码去研究。
参考Flutter官方文档【https://docs.flutter.dev/perf/rendering】
篇文章我们完成了一条信息的测量和绘制,本篇我们来实现消息的平移动画
效果图如下:
在自定义View中,通常我比较喜欢额外创建一个Bitmap和一个Canvas来绘制动画效果。大家可以根据自己喜好修改,实现的方式有很多。
首先在首次测量的时候我们创建Canvas、Matrix、Bitmap,如果你的实际使用场景中,View的大小可能会更改,这里也可以每次测量都重新创建。
首先声明3个变量:
```Kotlin
private lateinit var mBufferBitmap: Bitmap
private lateinit var mBufferCanvas: Canvas
private lateinit var mBufferMatrix: Matrix
```
在`onMeasure`中创建:
```Kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 测量代码...
if (!this::mBufferBitmap.isInitialized) {
mBufferBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
mBufferCanvas = Canvas(mBufferBitmap)
mBufferMatrix = Matrix()
}
}
```
接下来我们修改一下消息数据模型,将每条消息的*动画进度*以及其他一些属性存储起来,以便后续拓展和修改。
```Kotlin
data class Message(
val avatar: String,// 头像地址
val nickname: String,// 昵称
val joinRoom: Int,// 1=加入,否则均为退出,改为Boolean也可
var info: NicknameInfo,// 存储本条消息的宽高等信息
var shader: BitmapShader? = null,// 图片加载相关
var bitmap: Bitmap? = null,
var life: Int = 5,// 消息存活时间
val timing: Long = System.currentTimeMillis(),// 已消耗时间
var xProgress: Float = 1f,// x轴平移比例,取值1.0f~0.0f
var yProgress: Float = 0f,// y轴平移比例,同上
)
data class NicknameInfo(
val nickname: String,// 修改后的nickname(超过5个字符,后面变为省略号
val nicknameWidth: Float,// 昵称宽度
val messageWidth: Float,// 消息总宽
val statusTextWidth: Float// "加入房间"、"退出房间",这几个字的宽度。根据实际需求这里也可以改为全局变量,因为字宽基本上可以说是固定的
)
```
数据的命名和定义可以完全按照自己的喜好来,存储需要存储的数据即可。无论是自定义View还是其他,最终总是要落实到数据上的,定义好存储数据的结构和算法即可。
根据我们上面定义的数据,我们用mBufferCanvas和mBufferMatrix进行一个渲的染:p
```Kotlin
private fun drawMessage() {
mBufferMatrix.reset()// 绘制前reset一下matrix,清空一下buffer bitmap。
// 这里使用 mBufferBitmap.eraseColor(Color.TRANSPARENT)也是可以的,至于两者有什么不同,我也不清楚(; ̄ェ ̄),欢迎留言补充
mBufferCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
val msg = messageList[0]// 上一篇文章中,我们使用的是一个全局变量,这里我偷偷换成了List,用其他形式也可以,因为需求中是显示两条消息,用数组也行,两个全局变量也可以,根据自己喜好即可
val info = msg.info
val xOffset = msg.xProgress * info.messageWidth
val yOffset = msg.yProgress * messageHeight + msg.yProgress * messagePadding
mBufferMatrix.setTranslate(xOffset, yOffset)// 平移效果通过matrix来实现,也可以直接在绘制的时候加上偏移量,我本来是这么做的,但创建的mBufferMatrix就显得很多余,我就改成用matrix了(´・ω・`)
mBufferCanvas.setMatrix(mBufferMatrix)// 记得设置上
drawMsg(// 绘制消息的代码我丢在一个方法里了,这样我绘制两条再调用一次drawMsg就行了,我可不想复制两遍代码(´・_・`)
msg,
info.messageWidth,
info.nicknameWidth,
info.nickname
)
post { invalidate() }// 防止子线程调用,进行一个post
}
/**
* DO NOT CALL THIS FUNCTION
*/
private fun drawMsg(
msg: Message,
messageWidth: Float,
nicknameWidth: Float,
nickname: String
) {
path.reset()// 用前先reset,好习惯
paint.color = Color.parseColor("#F3F3F3")
val statusText = if (msg.joinRoom == 1) "进入直播间" else "退出直播间"//状态直接传进来也可,个人喜好
// 接下来你看到的所有xOffset、yOffset,都是我改用matrix之前的逻辑,把0f删掉、把x/yOffset取消注释,就变成不用matrix的版本了(´・ω・`)
val messageLeft = measuredWidth - messageWidth// + xOffset
path.addArc(// 给path添加一个半圆
messageLeft,
//yOffset,
0f,
messageLeft + avatarPadding + avatarHeight.toFloat(),
/*yOffset*/0f + messageHeight.toFloat(),
90f,
180f
)
// 给path添加矩形
path.moveTo(messageLeft + avatarHeight.shr(1).toFloat(), /*yOffset*/0f)
path.lineTo(measuredWidth.toFloat(), /*yOffset*/0f)
path.lineTo(measuredWidth.toFloat(), /*yOffset*/0f + messageHeight.toFloat())
path.lineTo(
messageLeft + avatarHeight.shr(1).toFloat(), /*yOffset*/
0f + messageHeight.toFloat()
)
paint.color = Color.parseColor("#434343")// 背景色
mBufferCanvas.drawPath(path, paint)// 填充
// 绘制文本
paint.color = Color.WHITE
mBufferCanvas.drawText(
statusText,
messageLeft + avatarHeight + avatarPadding.shl(1) + nicknameWidth + messagePadding,
//(measuredWidth - statusTextWidth - statusTextPadding) + /*xOffset*/上面提到放开offset的算法,这里的注视就没删除,留下做参考0f,
messageHeight.shr(1) + fontCenterOffset + /*yOffset*/0f,
paint
)
paint.color = Color.parseColor("#BCBCBC")// 绘制昵称
mBufferCanvas.drawText(
nickname,
messageLeft + avatarPadding.shl(1) + avatarHeight,
//(messageWidth - statusTextWidth - statusTextPadding.shl(1) - nicknameWidth) + /*xOffset*/0f,
messageHeight.shr(1) + fontCenterOffset + /*yOffset*/0f,
paint
)
msg.bitmap?.let {// 图片加载完成的话,绘制头像
mBufferCanvas.save()
paint.shader = msg.shader
val translateOffset = (messageHeight - it.width).shr(1)
mBufferCanvas.translate(
messageLeft + translateOffset,
/*yOffset*/0f + translateOffset.toFloat()
)
mBufferCanvas.drawCircle(
it.width.shr(1).toFloat(),
it.width.shr(1).toFloat()/*messageHeight.shr(1).toFloat()*/,
avatarHeight.shr(1).toFloat(),
paint
)
paint.shader = null
mBufferCanvas.restore()
}
}
```
完成上面的代码后只需要修改x轴和y轴的变量,即可实现"动画"了。动手试试吧(´・ω・`)
下篇文章我们来实现添加消息、计时、生命结束后删除消息等功能,还有真正的动画效。
ndroid中的四大组件以及应用场景
1、Activity的生命周期
生命周期:对象什么时候生,什么时候死,怎么写代码,代码往那里写。
注意:
Main1Activity: onPause Main2Activity: onCreate Main2Activity: onStart Main2Activity: onResume MainA1ctivity: onStop
异常状态下的生命周期:
资源相关的系统配置发生改变或者资源不足:例如屏幕旋转,当前Activity会销毁,并且在onStop之前回调onSaveInstanceState保存数据,在重新创建Activity的时候在onStart之后回调onRestoreInstanceState。其中Bundle数据会传到onCreate(不一定有数据)和onRestoreInstanceState(一定有数据)。
防止屏幕旋转的时候重建,在清单文件中添加配置: android:configChanges="orientation"
2、Fragment的生命周期
正常启动
Activity: onCreate Fragment: onAttach Fragment: onCreate Fragment: onCreateView Fragment: onActivityCreated Activity: onStart Activity: onResume
正常退出
Activity: onPause Activity: onStop Fragment: onDestroyView Fragment: onDestroy Fragment: onDetach Activity: onDestroy
3、Activity的启动模式
4、Activity与Fragment之间的传值
//Activity中对fragment设置一些参数 fragment.setArguments(bundle); //fragment中通过getArguments获得Activity中的方法 Bundle arguments = getArguments();
//Activity中的代码 EventBus.getDefault().post("消息"); //Fragment中的代码 EventBus.getDefault().register(this); @Subscribe public void test(String text) { tv_test.setText(text); }
5、Service
Service分为两种:
对应的生命周期如下:
context.startService() ->onCreate()- >onStartCommand()->Service running--调用context.stopService() ->onDestroy() context.bindService()->onCreate()->onBind()->Service running--调用>onUnbind() -> onDestroy()
注意
Service默认是运行在main线程的,因此Service中如果需要执行耗时操作(大文件的操作,数据库的拷贝,网络请求,文件下载等)的话应该在子线程中完成。
特殊情况是!:Service在清单文件中指定了在其他进程中运行。
6、Android中的消息传递机制
为什么要使用Handler?
因为屏幕的刷新频率是60Hz,大概16毫秒会刷新一次,所以为了保证UI的流畅性,耗时操作需要在子线程中处理,子线程不能直接对UI进行更新操作。因此需要Handler在子线程发消息给主线程来更新UI。
这里再深入一点,Android中的UI控件不是线程安全的,因此在多线程并发访问UI的时候会导致UI控件处于不可预期的状态。Google不通过锁的机制来处理这个问题是因为:
因此,Google的工程师最后是通过单线程的模型来操作UI,开发者只需要通过Handler在不同线程之间切花就可以了。
概述一下Android中的消息机制?
Android中的消息机制主要是指Handler的运行机制。Handler是进行线程切换的关键,在主线程和子线程之间切换只是一种比较特殊的使用情景而已。其中消息传递机制需要了解的东西有Message、Handler、Looper、Looper里面的MessageQueue对象。
如上图所示,我们可以把整个消息机制看作是一条流水线。其中:
为什么在子线程中创建Handler会抛异常?
Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。因此Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。
正确的使用方法是:
handler = null; new Thread(new Runnable() { private Looper mLooper; @Override public void run() { //必须调用Looper的prepare方法为当前线程创建一个Looper对象,然后启动循环 //prepare方法中实质是给ThreadLocal对象创建了一个Looper对象 //如果当前线程已经创建过Looper对象了,那么会报错 Looper.prepare(); handler = new Handler(); //获取Looper对象 mLooper = Looper.myLooper(); //启动消息循环 Looper.loop(); //在适当的时候退出Looper的消息循环,防止内存泄漏 mLooper.quit(); } }).start();
主线程中默认是创建了Looper并且启动了消息的循环的,因此不会报错:
应用程序的入口是ActivityThread的main方法,在这个方法里面会创建Looper,并且执行Looper的loop方法来启动消息的循环,使得应用程序一直运行。
子线程中可以通过Handler发送消息给主线程吗?
可以。有时候出于业务需要,主线程可以向子线程发送消息。子线程的Handler必须按照上述方法创建,并且关联Looper。
7、事件传递机制以及自定义View相关
Android的视图树
Android中View的机制主要是Activity的显示,每个Activity都有一个Window(具体在手机中的实现类是PhoneWindow),Window以下有DecorView,DecorView下面有TitleVie以及ContentView,而ContentView就是我们在Activity中通过setContentView指定的。
事件传分发机制
ViewGroup有以下三个与事件分发的方法,而View只有dispatchTouchEvent和onTouchEvent。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); }
事件总是从上往下进行分发,即先到达Activity,再到达ViewGroup,再到达子View,如果没有任何视图消耗事件的话,事件会顺着路径往回传递。其中:
注意
自定义View的分类
View的测量-onMeasure
View的测量最终是在onMeasure方法中通过setMeasuredDimension把代表宽高两个MeasureSpec设置给View,因此需要掌握MeasureSpec。MeasureSpec包括大小信息以及模式信息。
MeasureSpec的三种模式:
下面给出模板代码:
public class MeasureUtils { /** * 用于View的测量 * * @param measureSpec * @param defaultSize * @return */ public static int measureView(int measureSpec, int defaultSize) { int measureSize; //获取用户指定的大小以及模式 int mode = View.MeasureSpec.getMode(measureSpec); int size = View.MeasureSpec.getSize(measureSpec); //根据模式去返回大小 if (mode == View.MeasureSpec.EXACTLY) { //精确模式(指定大小以及match_parent)直接返回指定的大小 measureSize = size; } else { //UNSPECIFIED模式、AT_MOST模式(wrap_content)的话需要提供默认的大小 measureSize = defaultSize; if (mode == View.MeasureSpec.AT_MOST) { //AT_MOST(wrap_content)模式下,需要取测量值与默认值的最小值 measureSize = Math.min(measureSize, defaultSize); } } return measureSize; } }
最后,复写onMeasure方法,把super方法去掉:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(MeasureUtils.measureView(widthMeasureSpec, 200), MeasureUtils.measureView(heightMeasureSpec, 200) ); }
View的绘制-onDraw
View绘制,需要掌握Android中View的坐标体系:
View的坐标体系是以左上角为坐标原点,向右为X轴正方向,向下为Y轴正方向。
View绘制,主要是通过Android的2D绘图机制来完成,时机是onDraw方法中,其中包括画布Canvas,画笔Paint。下面给出示例代码。相关API不是介绍的重点,重点是Canvas的save和restore方法,通过save以后可以对画布进行一些放大缩小旋转倾斜等操作,这两个方法一般配套使用,其中save的调用次数可以多于restore。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Bitmap bitmap = ImageUtils.drawable2Bitmap(mDrawable); canvas.drawBitmap(bitmap, getLeft(), getTop(), mPaint); canvas.save(); //注意,这里的旋转是指画布的旋转 canvas.rotate(90); mPaint.setColor(Color.parseColor("#FF4081")); mPaint.setTextSize(30); canvas.drawText("测试", 100, -100, mPaint); canvas.restore(); }
View的位置-onLayout
与布局位置相关的是onLayout方法的复写,一般我们自定义View的时候,只需要完成测量,绘制即可。如果是自定义ViewGroup的话,需要做的就是在onLayout中测量自身以及控制子控件的布局位置,onLayout是自定义ViewGroup必须实现的方法。
8、性能优化
布局优化
<include android:id="@+id/v_test" layout="@layout/include_view" />
<ViewStub android:id="@+id/v_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/view_stub" /> //需要手动调用inflate方法,布局才会显示出来。 stub.inflate(); //其中setVisibility在底层也是会调用inflate方法 //stub.setVisibility(View.VISIBLE); //之后,如果要使用ViewStub标签里面的View,只需要按照平常来即可。 TextView tv_1 = (TextView) findViewById(R.id.tv_1);
内存优化
APP设计以及代码编写阶段都应该考虑内存优化:
IntentService在内部其实是通过线程以及Handler实现的,当有新的Intent到来的时候,会创建线程并且处理这个Intent,处理完毕以后就自动销毁自身。因此使用IntentService能够节省系统资源。
@Override public void onLowMemory() { super.onLowMemory(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); switch (level) { case TRIM_MEMORY_COMPLETE: //... break; case 其他: } }
android:largeHeap="true"
分析方法 1\. 使用Android Studio提供的Android Monitors中Memory工具查看内存的使用以及没使用的情况。 2\. 使用DDMS提供的Heap工具查看内存使用情况,也可以手动触发GC。 3\. 使用性能分析的依赖库,例如Square的LeakCanary,这个库会在内存泄漏的前后通过Notification通知你。
什么情况会导致内存泄漏
解决方案 1\. 应该尽量避免static 成员变量引用资源耗费过多的实例,比如Context。 2\. Context 尽量使用ApplicationContext,因为Application 的Context 的生命周期比较长,引用它不会出现内存泄露的问题。 3\. 使用WeakReference 代替强引用。比如可以使用WeakReference<Context> mContextRef
解决方案 1\. 将线程的内部类,改为静态内部类(因为非静态内部类拥有外部类对象的强引用,而静态类则不拥有)。 2\. 在线程内部采用弱引用保存Context 引用。
查看内存泄漏的方法、工具
性能优化
//开启数据采集 Debug.startMethodTracing("test.trace"); //关闭 Debug.stopMethodTracing();
OOM
避免OOM的一些常见方法:
BitmapFactory.Options options = new BitmapFactory.Option(); options.inSampleSize = 2; //Options 只保存图片尺寸大小,不保存图片到内存 BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 2; Bitmap bmp = null; bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts); //回收 bmp.recycle();
ANR
不同的组件发生ANR 的时间不一样,主线程(Activity、Service)是5 秒,BroadCastReceiver 是10 秒。
ANR一般有三种类型:
解决方案: 1\. UI线程只进行UI相关的操作。所有耗时操作,比如访问网络,Socket 通信,查询大量SQL 语句,复杂逻辑计算等都放在子线程中去,然后通过handler.sendMessage、runonUITread、AsyncTask 等方式更新UI。 2\. 无论如何都要确保用户界面操作的流畅度。如果耗时操作需要让用户等待,那么可以在界面上显示进度条。 3\. BroadCastReceiver要进行复杂操作的的时候,可以在onReceive()方法中启动一个Service来处理。
9、九切图(.9图)、SVG图片
九切图
点九图,是Android开发中用到的一种特殊格式的图片,文件名以”.9.png“结尾。这种图片能告诉程序,图像哪一部分可以被拉升,哪一部分不能被拉升需要保持原有比列。运用点九图可以保证图片在不模糊变形的前提下做到自适应。点九图常用于对话框背景图片中。
android5.0的SCG矢量动画机制
10、Android中数据常见存储方式
11、进程间通信
操作系统进程间通信的方法,android中有哪些?
操作系统:
Android中的进程通信方式并不是完全继承于Linux:
12、常见的网络框架
常用的http框架以及他们的特点
13、常用的图片加载框架以及特点、源码
Fresco是把图片缓存放在了Ashmem(系统匿名内存共享区)
不管发生什么,垃圾回收器都不会自动回收这些 Bitmap。当 Android 绘制系统在渲染这些图片,Android 的系统库就会把这些 Bitmap 从 Ashmem 堆中抽取出来,而当渲染结束后,这些 Bitmap 又会被放回到原来的位置。如果一个被抽取的图片需要再绘制一次,系统仅仅需要把它再解码一次,这个操作非常迅速。
14、在Android开发里用什么做线程间的通讯工具?
传统点的方法就是往同步代码块里些数据,然后使用回调让另外一条线程去读。在Android里我一般会创建Looper线程,然后Hanlder传递消息。
15、Android新特性相关
16、网络请求优化
网络请求优化
网络请求的安全性
这块了解的不多。我给你说说我的思路吧,利用哈希算法,比如MD5,服务器给我们的数据可以通过时间戳和其他参数做个加密,得到一个key,在客户端取出数据后根据数据和时间戳再去生成key与服务端给的做个对比。
17、新技术相关
RXJava:一个异步请求库,核心就是异步。利用的是一种扩展的观察模式,被观察者发生某种变化的时候,可以通过事件(onNext、onError、onComplete)等方式通过观察者。RXJava同时支持线程的调度和切换,用户可以指定订阅发生的线程以及观察者触发的线程。
Retrofit:通过注解的方式来指定URL、请求方法,实质上底层是通过OKHttp来实现的。
文末
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,在这里[免费分享]给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
image.png
资料免费领取方式:转发后关注我后台私信关键词【资料】获取!
转发+点赞+关注,第一时间获取最新知识点
Android架构师之路很漫长,一起共勉吧!
*请认真填写需求信息,我们会在24小时内与您取得联系。