接觸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小時內與您取得聯系。