安卓面试基础3

View的绘制面试讲解

在Android开发中,视图(View)的绘制是一个非常重要的过程。理解视图的绘制机制不仅有助于创建自定义视图,还能优化应用的性能。

1. 基本概念

Android视图的绘制是一个复杂的过程,涉及到多次测量(measure)、布局(layout)和绘制(draw)。视图树(View Tree)是一个层次结构,根节点通常是一个ViewGroup,如ActivityWindow对象。视图树的绘制过程可以分为以下几个步骤:

  • 测量(Measure):确定每个视图的宽度和高度。
  • 布局(Layout):确定每个视图的位置。
  • 绘制(Draw):将视图的内容绘制到屏幕上。

2. 绘制流程

视图的绘制过程主要由三个步骤组成:measurelayoutdraw。这些步骤是通过递归调用来完成的,从视图树的根节点开始从上向下遍历整个视图树。DecorView–>ViewGroup–>View

2.1 Measure(测量)

measure 步骤的目的是确定视图的大小。视图通过 measure 方法开始测量过程,最终通过 onMeasure 方法计算出视图的宽高。

1
2
3
4
5
6
7
8
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 自定义视图的测量逻辑
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}

2.2 Layout(布局)

layout 步骤的目的是确定每个视图的位置。视图通过 layout 方法开始布局过程,最终通过 onLayout 方法确定子视图的位置。

1
2
3
4
5
6
7
8
9
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 自定义视图的布局逻辑
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(left, top, right, bottom); // 设置子视图的位置
}
}

2.3 Draw(绘制)

draw 步骤的目的是将视图的内容绘制到屏幕上。视图通过 draw 方法开始绘制过程,最终通过 onDraw 方法绘制视图的内容。

1
2
3
4
5
6
7
8
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 自定义视图的绘制逻辑
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint); // 绘制一个红色矩形
}

3. 常见方法和技巧

3.1 自定义视图

在创建自定义视图时,经常需要重写 onMeasureonLayoutonDraw 方法。例如,创建一个自定义圆形视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CircleView extends View {
private Paint paint;

public CircleView(Context context) {
super(context);
init();
}

private void init() {
paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size); // 设置视图为正方形
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int radius = getWidth() / 2;
canvas.drawCircle(radius, radius, radius, paint); // 绘制一个圆形
}
}

3.2 优化绘制性能

  • 减少过度绘制:避免不必要的绘制操作,通过 clipRect 方法限制绘制区域。
  • 使用硬件加速:确保视图开启了硬件加速,提高绘制性能。
  • 批量绘制:尽可能将多个绘制操作合并为一个,以减少绘制次数。

4. 常见面试问题

4.1 解释Android视图的绘制过程

这个问题旨在考察你对视图绘制流程的理解。你应该详细解释 measurelayoutdraw 三个步骤,并描述它们的作用和实现方式。

4.2 如何创建自定义视图?

重点描述如何重写 onMeasureonLayoutonDraw 方法,并举例说明一个简单的自定义视图。

4.3 如何优化自定义视图的绘制性能?

可以从减少过度绘制、使用硬件加速、批量绘制等方面来回答,展示你对性能优化的理解。

4.4 什么是 ViewGroupView 的区别?

解释 ViewGroup 是一种特殊的 View,它可以包含其他子视图,而 View 是一个单一的视图组件。

4.5 什么是 MeasureSpec

MeasureSpec 是一个由父视图传递给子视图的约束,用于确定子视图的大小。MeasureSpec 由模式(mode)和大小(size)组成,模式包括 UNSPECIFIED不确定、EXACTLY确定范围 和 AT_MOST最大尺寸范围。

View事件传递机制

层叠布局 Activity–>Phonewindow–>DecorView–>ViewGroup–>View

1. 基本概念

事件传递机制主要涉及三个方法:

  • **dispatchTouchEvent()**:负责事件的分发。
  • **onInterceptTouchEvent()**:ViewGroup 用于拦截事件。
  • **onTouchEvent()**:处理事件。

2. 事件传递流程

2.1 事件分发(dispatchTouchEvent)

dispatchTouchEvent(MotionEvent ev) 是事件传递的起点。每个ViewGroupView都有这个方法,用于分发事件。

  • ViewGroup

    • 调用onInterceptTouchEvent()决定是否拦截事件。
    • 若不拦截,则将事件传递给子View的dispatchTouchEvent()
    • 若没有子View处理,则调用自身的onTouchEvent()
  • View

    • 直接调用onTouchEvent()处理事件。

2.2 事件拦截(onInterceptTouchEvent)

ViewGroup特有的方法,用于决定是否拦截事件。

  • 返回true:拦截事件,执行自身的onTouchEvent()
  • 返回false:不拦截,事件传递给子View。

2.3 事件处理(onTouchEvent)

onTouchEvent(MotionEvent ev)用于处理事件。

  • 返回true:表示事件被处理。
  • 返回false:表示事件未被处理,向上传递到父View的onTouchEvent()

3. 事件传递示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CustomViewGroup extends ViewGroup {

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("CustomViewGroup", "dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("CustomViewGroup", "onInterceptTouchEvent");
return false; // 默认不拦截
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d("CustomViewGroup", "onTouchEvent");
return true; // 处理事件
}
}

public class CustomView extends View {

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("CustomView", "dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d("CustomView", "onTouchEvent");
return true; // 处理事件
}
}

4. 事件传递的常见策略

  • ViewGroup拦截事件:可用于处理滑动冲突,例如在ScrollView中嵌套ListView
  • 子View处理事件:如按钮的点击事件。

5. 常见面试问题

5.1 事件传递机制的完整流程?

  • 描述dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()的调用顺序及其作用。

5.2 如何解决事件冲突?

  • 方法1:在ViewGroup中通过onInterceptTouchEvent()拦截。
  • 方法2:在子View中通过requestDisallowInterceptTouchEvent()请求父View不拦截。

5.3 什么是requestDisallowInterceptTouchEvent()

  • 告诉父ViewGroup不要拦截当前事件序列。

5.4 如何实现一个点击事件?

  • 重写onTouchEvent(),在MotionEvent.ACTION_UP时返回true并处理点击逻辑。

ListView和RecyclerView

ListView 是一个用于展示大量数据的视图组件,支持垂直滚动。

RecyclerView 是一个更灵活、更强大的列表视图组件,支持垂直和水平滚动,以及网格、瀑布流等多种布局。

ProGuard混淆

ProGuard是一个用于Java和Android应用程序的开源工具,用于优化、混淆和缩减代码。在Android项目中,ProGuard通常用于减小APK文件的大小,提高代码的性能,并保护代码不被逆向工程。

1. 基本概念

1.1 优化(Optimization)

优化代码以提高性能,包括删除未使用的类、方法和字段,以及优化字节码。

1.2 混淆(Obfuscation)

通过重命名类、方法和字段,使代码难以被逆向工程。混淆后的代码看起来像是随机的字符序列。

1.3 缩减(Shrinking)

移除未使用的代码和资源,以减小APK文件的大小。

2. 配置ProGuard

ProGuard的配置通过proguard-rules.pro文件实现。Android Studio默认生成一个proguard-rules.pro文件,用于存放ProGuard的配置规则。

2.1 启用ProGuard

build.gradle文件中启用ProGuard:

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
minifyEnabled true // 启用代码混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

2.2 配置ProGuard规则

proguard-rules.pro文件中添加代码混淆规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 保留所有注释以避免混淆
-keepattributes *Annotation*

# 保留所有实现了android.os.Parcelable的类
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

# 保留主Activity
-keep class com.example.MyMainActivity {
public <init>();
}

# 保留所有被注解的类和方法
-keep @interface com.example.MyAnnotation
-keep class ** {
@com.example.MyAnnotation *;
}

3. ProGuard的常见问题

3.1 混淆后应用程序崩溃

混淆后应用程序崩溃通常是因为某些代码被误混淆或移除。可以通过添加保留规则来解决:

1
2
3
4
5
6
7
8
9
10
# 保留所有Activity、Service、BroadcastReceiver、ContentProvider
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
-keep class * extends android.content.ContentProvider

# 保留所有带特定注解的方法
-keepclassmembers class * {
@com.example.MyAnnotation <methods>;
}

3.2 日志信息被混淆

可以通过保留日志方法来避免日志信息被混淆:

1
2
3
4
5
6
7
8
# 保留所有日志方法
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** w(...);
public static *** v(...);
public static *** i(...);
public static *** e(...);
}

3.3 反射相关的问题

反射在混淆后可能无法正常工作,可以通过保留相关类和方法来解决:

1
2
3
4
# 保留所有使用反射的类和方法
-keep class * {
public *;
}

4. 常见面试问题

4.1 什么是ProGuard?

ProGuard是一个用于优化、混淆和缩减Java和Android代码的开源工具。它通过删除未使用的代码和资源、优化字节码、以及重命名类、方法和字段来减小APK文件的大小并保护代码不被逆向工程。

4.2 如何启用ProGuard?

build.gradle文件中启用ProGuard,通过设置minifyEnabled true并指定ProGuard配置文件:

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

4.3 如何配置ProGuard以保留特定的类或方法?

proguard-rules.pro文件中添加保留规则,例如保留所有Activity:

1
-keep class * extends android.app.Activity

4.4 如何解决混淆后应用程序崩溃的问题?

可以通过仔细检查混淆规则,确保所有必要的类和方法未被混淆或移除。例如,保留所有带特定注解的方法:

1
2
3
-keepclassmembers class * {
@com.example.MyAnnotation <methods>;
}

4.5 如何查看混淆后的代码?

ProGuard在混淆后会生成映射文件(mapping.txt),其中包含了原始类、方法和混淆后的名称之间的映射关系。这个文件通常位于build/outputs/mapping/release/目录下。

4.6 ProGuard与R8的区别?

R8是Google推出的用于替代ProGuard的新工具。R8在功能上与ProGuard相似,但具有更高的性能和更好的代码优化能力。在Android项目中,可以通过设置android.enableR8=true来启用R8。