Forwards

有空的博客


  • 首页

  • 归档

  • 关于

网络请求之“重试设计”:退避

发表于 2020-02-20

重试的场景

重试的定义:我们认为这个故障是暂时的,而不是永久的,所以我们会去重试。

要重试:调用超时,被调用端返回了某种可以重试的错误(如繁忙中,流程中,维护中,资源不足等)

不要重试:业务级别的错误(如没有权限,或者是非法数据等),技术上的错误(如http的500等),这类型的错误重试下去没有意义。

重试的策略:

总原则:有个重试次数的最大值,经过一段时间不断的重试之后,就没有必要再重试了,应该报故障了,这样可以避免因为重试过多而导致网络上的负担更重

  1. No BackOff 无退避算法策略,即当重试时是立即重试
  2. Fixed BackOff 固定时间的退避策略,需设置参数

    1
    2
    3. UniformRandom BackOff 随机时间退避策略,需设置```sleeper```、```minBackOffPeriod```和```maxBackOffPeriod```。该策略在[```minBackOffPeriod```,```maxBackOffPeriod```]之间取一个随机休眠时间,```minBackOffPeriod```默认为500毫秒,```maxBackOffPeriod```默认为1500毫秒
    4. Exponential Backoff 指数级退避策略,每一次重试所需要的休息时间都会翻倍增加。让被调用方能够有更多的时间来从容处理我们的请求,和TCP的拥塞机制有点像。需设置参数```sleeper```、```initialInterval```、```maxInterval```和```multiplier```。```initialInterval```指定初始休眠时间,默认为100毫秒。```maxInterval```指定最大休眠时间,默认为30秒。```multiplier```指定乘数,即下一次休眠时间为当前休眠时间 * ```multiplier

  3. ExponentialRandom BackOff 随机指数退避策略,引入随机乘数,固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。

example:

  1. 指数级退避策略

    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
    36
    37
    //最大延迟时间间隔,单位是毫秒
    private static int MAX_WAIT_INTERVAL = 100*1000;
    //最大重试次数
    private static int MAX_RETRIES = 3;
    //指数退避算法
    public static void doOperationAndWaitForResult(){
    try{
    int retries = 0;
    boolean retry = false;
    do {
    long waitTime = Math.min(getWaitTimeExp(retries), MAX_WAIT_INTERVAL);
    System.out.print("等待时间:" + waitTime + " ms \n");
    //Wait for the result
    Thread.sleep(waitTime);
    //Get the result of the asynchronous operation
    Results results = getAsyncOperationResult();
    if (Results.SUCCESS == results){
    retry = false;
    }else if (Results.NOT_READY == results || Results.THROTTLED == results
    || Results.SERVER_ERROR == results){
    retry = true;
    }else {
    retry = false;
    }
    }while (retry && (retries ++ < MAX_RETRIES));
    }catch (Exception e){
    }
    }
    /**
    * 根据重试的次数,返回2的指数的等待时间
    * @param retryCount
    * @return
    */
    public static long getWaitTimeExp(int retryCount){
    long waitTime = ((long) Math.pow(2, retryCount) * 100L);
    return waitTime;
    }
  2. 随机时间退避策略:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private void avoidTime(int retryTime) {
    int tryTime = RETRY_TIME - retryTime;
    Random random = new Random(System.currentTimeMillis());
    int time = 0;
    switch (tryTime) {
    case 1:
    case 2:
    time = random.nextInt(150) + 50;
    break;
    case 3:
    time = random.nextInt(300) + 200;
    break;
    default:
    time = random.nextInt(150);
    break;
    }
    try {
    Thread.sleep(time);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

Android埋点技术分析

发表于 2019-07-10

一、概念

1. 什么是埋点

埋点是数据采集的一种方法,将每个需要统计的点击事件、页面上装摄像头,采集相应的信息和行为。

通过埋点、可以收集用户行为数据,进而用于分析和优化产品体验,也可以为产品的运营提供数据支撑,通常可以采集到以下类型数据:

  1. 行为数据:时间、地点、人物、交互的内容等
  2. 质量数据:App运行情况、浏览器加载情况、错误异常等
  3. 环境数据:手机型号、操作系统版本、浏览器UA(User Agent)、地理、运营商、网络环境等
  4. 运营数据:PV(Page View)、UV(Unique Visitor)、点击量、日活、留存、渠道来源等

2. 埋点事件分类

  1. 点击事件

    点击事件,用户点击按钮即算点击事件,不管点击后有无结果,点击一次记一次

  2. 曝光事件

    成功打开一次页面记一次,刷新页面一次记一次,加载下一页新页,加载一次记一次

  3. 页面停留时间事件

    表示一个用户在某个页面的停留时长记为停留时长

3. 移动端的埋点需求表

字段 释义
event_id 埋点ID
event_key 埋点名
status 埋点状态
state 埋点所在页面
desc 埋点描述
event_value 事件属性
value_comments 事件属性描述
opportunity 埋点发送时机
remark 备注
event_table 埋点发送日志
req_release 需求提出版本
achieve_release 需求实现版本
priority 需求优先级
update_time 需求更新事件

二、埋点的技术方案

1. 代码埋点

代码埋点是指在某个事件发生时调用数据发送接口上报数据。基于预先编码实现的代码埋点,其优点是:控制精准、采集灵活性强,可以自由的选择什么时候发送什么样的数据;缺点:开发、测试成本高,对于客户端而言需要等待发版才能修改线上的埋点。

2. 无痕埋点

无痕埋点也可称为无埋点或者全埋点,它指的是将App内产生的所有的、满足某个条件的行为,全部上报到后台服务器,在计算时筛选出可用的数据。其优点是很大程度上减少开发、测试的重复劳动,数据可以回溯并且全面。缺点:采集信息不够灵活,并且数据量大,里面可能很多是没有价值的数据。

3. 可视化埋点

可视化埋点是指通过可视化工具选择需要收集的埋点数据,下发配置给客户端,从而解析配置采集相应的埋点的方式。其优点是在于很大程度上减少开发、测试的重复劳动,数据量可控,可以在线上动态的进行埋点配置,无需等待App发版;缺点:采集信息不够灵活,并且无法解决数据回溯的问题。

Android事件分发机制

发表于 2019-05-04

本文结构:

Android事件分发机制

1. 相关概念

1.1 事件

事件分发的对象为事件(MotionEvent):当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件)

Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象

典型的事件类型主要有:

  1. MotionEvent.ACTION_DOWN:手指刚触摸屏幕;
  2. MotionEvent.ACTION_MOVE:手指在屏幕上滑动;
  3. MotionEvent.ACTION_UP:手指从屏幕上松开的一瞬间;
  4. MotionEvent.ACTION_CANCEL:非人为原因结束本次事件;

事件列:从手指触摸屏幕到手指离开屏幕,这个过程产生的一系列事件。任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件。

1.2 事件分发的本质

所谓事件分发机制,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生以后,系统需要将这个事件传递给一个具体的View,而这个传递的过程就是分发过程。

1.3事件的传递

一个点击事件产生后,传递的顺序是Activity(Window)–> ViewGroup –> View。

2. 事件分发的流程

点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()

  1. 方法介绍具体如下表所示:

Android事件分发方法

  1. Android事件分发流程如下:

事件分发业务流程图

总结: 当一个点击事件发生时,事件传递的过程如下:

  1. 事件最先传递到Activity的dispatchTouchEvent()进行事件分发
  2. 然后调用Window类实现类PhoneWindow的SuperDispatchTouchEvent()
  3. 调用DecorView的SuperDispatchTouchEvent()
  4. 调用ViewGroup的dispatchTouchEvent()
  5. 如果ViewGroup的onInterceptTouchEvent()返回true,则表示它要拦截当前事件,事件就由ViewGroup自己处理,调用自身的onTouchEvent();如果返回false,表示它不拦截当前事件,这时事件会传递到子元素,子元素的dispatchTouchEvent()会被调用,如此反复

Android动画——基础篇

发表于 2019-05-02

本文结构:

Android动画

1.种类

Android的动画可以分为三种:View动画、帧动画和属性动画;

  1. View动画通过对场景里的对象不断做图像变化(平移、缩放、旋转、渐变),从而产生动画效果,它是一种渐近式动画,并且View动画支持自定义;
  2. 帧动画通过顺序播放一系列图像从而达到动画效果,可以理解为图片切换动画;
  3. 属性动画通过动态地改变对象的属性从而达到动画效果,属性动画为API11的新特性。

2.View动画

View动画的四种变换如下表所示:

名称 标签 子类
平移动画 <translate> TranslateAnimation
缩放动画 <scale> ScaleAnimation
旋转动画 <rotate> RotateAnimation
透明度动画 <alpha> AlphaAnimation

上述的四种动画既可以通过XML来定义,也可以通过代码来动态创建。

2.1 在XML中定义动画

动画xml文件定义在:res/anim/目录下,一些常用语法及属性解释如下所示:

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
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="utf-8"?>
<!--set:interpolator:动画采用的插值器
shareInterpolator:集合中的动画是否和集合共享同一个插值器-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:shareInterpolator="true">
<!--平移:fromXDelta、toXDelta:水平方向移动的起始值和结束值
fromYDelta、toYDelta:竖直方向移动的起始值和结束值-->
<translate
android:duration="2000"
android:fromXDelta="0"
android:toXDelta="100%"
android:fromYDelta="0"
android:toYDelta="100%"></translate>
<!--缩放:fromXScale、toXScale:水平方向缩放的起始值和结束值
fromYScale、toYScale:竖直方向缩放的起始值和结束值
pivotX、pivotY:轴点的位置-->
<scale
android:duration="2000"
android:fromXScale="1"
android:toXScale="0.5"
android:fromYScale="1"
android:toYScale="0.5"
android:pivotX="50%"
android:pivotY="50%"></scale>
<!--旋转 fromDegrees:旋转的起始值;toDegrees:旋转的结束值
pivotX、pivotY:轴点的位置
fillAfter:动画结束后View是否停留在结束位置-->
<rotate
android:duration="2000"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:fillAfter="true"></rotate>

<!--渐变 fromAlpha:渐变的起始值,toAlpha:渐变的结束值-->
<alpha
android:duration="2000"
android:fromAlpha="1.0"
android:toAlpha="0"
></alpha>
</set>

<set>标签表示动画集合,对应AnimationSet类,它可以包含若干个动画,并且它的内部也是可以嵌套其他动画集合。

动画定义如上所示,如何应用上面的动画呢,如下所示:

1
2
3
Button button = (Button)findViewById(R.id.button);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.view_anim);
button.startAnimation(animation)

2.2 在Java中定义动画

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class AnimationTest {

//平移动画
public void clickToTranslate(View targetView){
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1);
translateAnimation.setDuration(2000);
targetView.startAnimation(translateAnimation);
}

//缩放动画
public void clickToScale(View targetView){
ScaleAnimation scaleAnimation = new ScaleAnimation(
1, 0.5f,
1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(2000);
targetView.startAnimation(scaleAnimation);
}

//旋转动画
public void clickToRotate(View targetView){
RotateAnimation rotateAnimation = new RotateAnimation(
0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5F
);
rotateAnimation.setDuration(2000);
targetView.startAnimation(rotateAnimation);
}

//渐变动画
public void clickToAlpha(View targetView){
AlphaAnimation alphaAnimation = new AlphaAnimation(
1, 0.5f
);
alphaAnimation.setDuration(2000);
targetView.startAnimation(alphaAnimation);
}

//组合动画
public void clickToSet(View targetView){
AlphaAnimation alphaAnimation = new AlphaAnimation(
1, 0.5f
);
alphaAnimation.setDuration(2000);

RotateAnimation rotateAnimation = new RotateAnimation(
0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5F
);
rotateAnimation.setDuration(2000);

ScaleAnimation scaleAnimation = new ScaleAnimation(
1, 0.5f,
1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(2000);

TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1);
translateAnimation.setDuration(2000);

AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(alphaAnimation);
animationSet.addAnimation(translateAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.addAnimation(scaleAnimation);
//设置动画过程监听
animationSet.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {

}

@Override
public void onAnimationEnd(Animation animation) {

}

@Override
public void onAnimationRepeat(Animation animation) {

}
});
targetView.startAnimation(animationSet);
}

//帧动画
public void clickToAnimationList(View targetView){
targetView.setBackgroundResource(R.drawable.fram_animation_list);
AnimationDrawable drawable = (AnimationDrawable) targetView.getBackground();
drawable.start();
}
}

View的基础使用就如上所述,当然除了系统提供的这四种View动画之外,我们还可以自定义View动画。这里简单说下它的用法:派生一个新动画需要继承Animation抽象类,然后重写它的initialize和applyTransformation方法,在initialize方法中做一些初始化工作,在applyTransformation中进行相应的矩形变换。

3. 帧动画

帧动画就是顺序播放一组预先设定好的图片,系统提供了AnimationDrawable类来使用帧动画,系统提供的标签有:

1
<animation-list>、<animation-rotate>、<animation-selector>、<animation-vector>

帧动画的使用比较简单,以<animation-list>标签为例使用如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_launcher_background" android:duration="200"></item>
<item android:drawable="@drawable/ic_launcher_background" android:duration="200"></item>
<item android:drawable="@drawable/ic_launcher_background" android:duration="200"></item>
<item android:drawable="@drawable/ic_launcher_background" android:duration="200"></item>
</animation-list>

然后将上述Drawable作为view的背景并通过Drawable来播放即可:

1
2
3
4
5
6
//帧动画
public void clickToAnimationList(View targetView){
targetView.setBackgroundResource(R.drawable.fram_animation_list);
AnimationDrawable drawable = (AnimationDrawable) targetView.getBackground();
drawable.start();
}

需要注意的是帧动画比较容易引起内存溢出OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片

4. 属性动画

属性动画是对任意对象的属性进行动画而不仅仅是View,因此所有补间动画的内容都可以通过属性动画实现。

4.1 用法

先来看一下属性动画中常用的几个类和对应标签:

标签 子类
<set> AnimatorSet
<animator>` ValueAnimator
<objectAnimator> ObjectAnimator

4.1.1 在XML中定义动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<objectAnimator
android:propertyName="x"
android:duration="3000"
android:valueTo="200"
android:valueType="intType"/>
<objectAnimator
android:propertyName="y"
android:duration="3000"
android:valueTo="300"
android:valueType="intType"/>
</set>

如何使用上面的属性动画呢,如下所示:

1
2
3
4
5
private void animatorFroResourse(Context context, View view){
AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.property_anim);
animatorSet.setTarget(view);
animatorSet.start();
}

4.1.2 在Java中定义动画

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
36
37
38
39
40
/**
* 属性动画
*/
public class PropertyAnimation {

//旋转
private void rtateAnimation(View view){
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "rotation",
0f, 360f);
objectAnimator.setDuration(2000);
objectAnimator.start();
}

//渐变
private void alphaAnimation(View view){
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha",
1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0f);
//setRepeatCount:设置动画的重复次数(-1表示无限重复)
objectAnimator.setRepeatCount(-1);
//setRepeatMode:设置动画的重复模式(reverse:逆向重复,restart:连续重复)
objectAnimator.setRepeatMode(ObjectAnimator.REVERSE);
objectAnimator.setDuration(2000);
objectAnimator.start();
}

//组合
private void setAnimation(View view){
ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha",
1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 0.0f, 1.0f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 0.0f, 1.0f);
ObjectAnimator rotation = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
ObjectAnimator translateX = ObjectAnimator.ofFloat(view, "translationX", 100, 400);
ObjectAnimator translateY = ObjectAnimator.ofFloat(view, "translationY", 100, 750);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(scaleX, scaleY, rotation, translateX, translateY);
animatorSet.setDuration(2000);
animatorSet.start();
}
}

4.2 工作原理

属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。

属性动画的运行机制是通过不断地对值进行操作来实现的。而初始值和结束值之间的动画过渡则由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑的过渡到结束值这样的效果。

ValueAnimator是怎么样实现从初始值平滑过渡到结束值的呢,这个就是由类型估值器TypeEvaluator和时间插值器TimeInterpolator共同决定的。

4.2.1 插值器和估值器

  1. TimeInterpolator(时间插值器),它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,它决定了动画从初始值过渡到结束值的节奏,系统预置的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)
  2. TypeInterpolator(类型估值器),它的作用是根据当前属性改变的百分比来计算改变后的属性值,它决定了动画如何从初始值过渡到结束值,系统预置的有IntEvaluator(针对整型属性)、FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)

对于属性动画也可以自定义方式实现所需效果。

5. 使用动画的注意实现

  1. OOM问题:这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现OOM。
  2. 内存泄漏:在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而导致内存泄漏;而View动画并不存在此问题
  3. 兼容性问题:动画在3.0以下的系统上有兼容性问题
  4. View动画的问题:View动画是对View的影像做动画,并不是真正的改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.Gone)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决问题
  5. 不要使用px:尽量使用dp
  6. 动画元素的交互:将view移动后,在Android3.0以前,不管是View动画还是属性动画,新位置均无法触发单击事件,同时老位置仍可触发单击事件;在Android3.0以后,属性动画的单击事件触发位置为移动后的位置,但是View动画仍在老位置。
  7. 硬件加速

Android自定义View学习记录

发表于 2019-04-02

前言:这是一个关于Android自定义View的学习记录系列,系列中出现的知识点均出自《Android艺术开发探索》,部分摘录自热心网友文章

本文结构:

Android自定义View

1. View的基础知识

1.1 View的概念

什么是View,在Android中不管是Button还是TextView,又或者是LinearLayout,它们的共同基类都是View,所以View是界面层的控件的一种抽象,它代表了一种控件。除了View,还有ViewGroup,ViewGroup是控件组,即一组控件(或者说是一组View),它的父类是View,综上,View可以解释为单个控件,也可以解释为由多个控件组件的一组控件。

View示例

1.2 View的位置参数

View的位置主要由它的四个属性决定:top、left、right、bottom。如下图坐标系所示:

View的坐标轴

View的坐标值都是相对于父容器而言的。

这四个属性值的获取方式:

1
2
3
4
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();

而从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,默认值为0。

2.自定义View的原因和方式

2.1 为什么要自定义View

通过自定义View,我们可以实现各种五花八门的效果

2.2 自定义View的方式

  1. 把系统内置的控件组合起来成一个新的控件
  2. 继承系统现有的控件,加入新的功能
  3. 自己绘制控件,继承系统View

3. View的工作流程

View的工作流程主要是指measure、layout和draw这三个流程,即测量、布局和绘制。其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。

View的绘制流程是从ViewRoot的performTraversals,它经过measure、layout和draw三个过程才能最终将一个view绘制出来,针对performTraversals的大致流程,如下图所示:

performTraversals的工作流程

performTraversals会依次调用performMeasure、performLayout和performDraw这三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中performMeasure会调用measure方法(measure方法是一个final类型的方法),在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个view树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的。

3.1 measure过程

在上面的分析中我们知道在View的measure方法中会去调用View的onMeasure,因此只需要看onMeasure的实现即可,View的onMeasure方法如下所示:

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置控件的宽高,记住这里默认是px,记得要分辨率转换实现适配,这里不做说明
setMeasuredDimension(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
}

private int getSize(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode){
case MeasureSpec.EXACTLY:
//父控件告诉我们子控件了一个确定的大小,你就按这个大小来布局。
// 比如我们指定了确定的dp值和macth_parent的情况。
result = 200;
break;
case MeasureSpec.AT_MOST:
//当前控件不能超过一个固定的最大值,一般是wrap_content的情况。
result = Math.min(100, specSize);
break;
case MeasureSpec.UNSPECIFIED:
//当前控件没有限制,要多大就有多大,这种情况很少出现
result = 400;
break;
}
return result;
}

onMeasure方法里有两个重要的参数, widthMeasureSpec和heightMeasureSpec,它们包含了两个信息:mode和size;

mode(测量模式)代表了我们当前控件的父控件告诉我们控件,你应该按怎样的方式来布局。mode的分类如下表格所示

模式 二进制数值 描述
UNSPECIFIED 00 父容器不对View有任何限制,要多大给多大
EXACTLY 01 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应LayoutParams的match_parent和具体的数值这两种模式
AT_MOST 10 父容器指定了一个可用大小SpecSize,View的大小不能大于这个值,它对应LayoutParams的wrap_content

注意:
如果对View的宽高进行修改了,不要调用 super.onMeasure( widthMeasureSpec, heightMeasureSpec); 要调用 setMeasuredDimension( widthsize, heightsize); 这个函数,否则会报异常。

onSizeChanged(确定View的大小):这个函数在view第一次被指定了大小值或者view的大小发生改变时会被调用。所以一般用来计算一些位置和与view的size有关的值。

3.2 layout过程

layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦被确定,那么View在父容器中的位置也就被确定了;接着会调用onLayout方法,onLayout的具体实现同样和具体的布局有关。

3.3 draw过程

draw过程比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循:

  1. 绘制背景background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

4. 自定义View的官方套路

步骤 关键字 作用
1 构造函数 初始化和定义自定义属性
2 onMeasure 确定view的测量宽/高
3 onSizeChanged 确定view的大小
4 onLayout 确定view在父容器中的布局
5 onDraw 实际绘制内容
6 提供接口 控制View或监听View某些状态

参考资料:

《Android艺术开发探索》

http://www.gcssloop.com/customview/CustomViewProcess

https://juejin.im/entry/579865765bbb500063fd66f4

单例模式的七种写法

发表于 2019-03-14

原文链接:https://blog.csdn.net/itachi85/article/details/50510124

在《Head First设计模式》的定义:

单例模式确保一个类只有一个实例,并提供一个全局访问点。

单例模式UML类图如下:

单例模式结构图

  1. 饿汉模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){

    }
    public static Singleton getInstance(){
    return instance;
    }
    }

    优点:获取对象的速度快

    缺点:在类加载时就完成了初始化,所以类加载较慢

  2. 懒汉模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Singleton{
    private static Singleton instance;
    private Singleton(){

    }
    public static Singleton getInstance(){
    if(instance == null){
    instance = new Singleton();
    }
    return instance;
    }
    }

    优点:在用户第一次调用时初始化,节约资源

    缺点:第一次加载需要实例化,反应较慢,且在多线程不能正常工作

  3. 懒汉模式(线程安全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Singleton{
    private static Singleton instance;
    private Singleton(){

    }
    public static synchronized Singleton getInstance(){
    if(instance == null){
    instance = new Singleton();
    }
    return instance;
    }
    }

    优点:能在多线程中很好的工作

    缺点:每次调用getInstance函数时都需要进行同步,造成不必要的同步开销

  4. 双重检查模式(DCL)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Singleton{
    private static volatile Singleton instance;
    private Singleton(){

    }
    public static Singleton getInstance(){
    if(instance == null){
    synchronized(Singleton.class){
    if(instance == null){
    instance = new Singlerton();
    }
    }
    }
    return instance;
    }
    }

    下面这段分析来自原文:

    这种写法在getSingleton方法中对singleton进行了两次判空,第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。 在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。 DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。

  5. 静态内部类单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Singleton{
    private Singleton(){

    }
    public static Singleton getInstance(){
    return SingletonHolder.instance;
    }
    private static class SingletonHolder{
    private static final Singleton instance = new Singleton();
    }
    }

    第一次加载Singleton类时不会初始化instance,只有第一次调用getInstance函数时虚拟机加载SingletonHolder并初始化instance,这样不仅能确保线程安全也能保证Singleton类的唯一性。

  6. 使用枚举单例

    1
    2
    3
    4
    5
    6
    public enum Singleton{
    INSTANCE;
    public void doSomeThing(){

    }
    }

    默认枚举实例的创建是线程安全的,并且在任何情况下都是单例

  7. 使用容器实现单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class SingletonManager{
    private static Map<String, Object> map = new HashMap<String, Object>();
    private SingletonManager(){

    }
    public static void registerService(String key, Object instance){
    if(!map.containsKey(key)){
    map.put(key, instance);
    }
    }
    public Object getService(String key){
    return map.get(key);
    }
    }

    用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

Appium入坑指南——第一个测试用例(Python篇)

发表于 2018-11-05

安装Appium Python Client

三种安装方式:

  1. pip在线安装(推荐方式):pip install Appium-Python-Client

  2. 在Pypi上下载源码安装

    1
    2
    3
    tar -xvf Appium-Python-Client-X.X.tar.gz
    cd Appium-Python-Client-X.X
    sudo python3.6 setup.py install
  3. 通过git安装

    1
    2
    3
    git clone git@github.com:appium/python-client.git
    cd python-client
    sudo python3.6 setup.py install

编写测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from appium import webdriver

desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '5.0'
desired_caps['deviceName'] = 'SA17607795'
desired_caps['appPackage'] = 'com.android.calculator2'
desired_caps['appActivity'] = '.Calculator'
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
driver.find_element_by_android_uiautomator('new UiSelector().text("1")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("2")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("3")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("+")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("3")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("2")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("1")').click()
driver.find_element_by_android_uiautomator('new UiSelector().text("=")').click()
driver.quit()

执行命令:

python3.6 android_calculator.py

遇到的问题

  1. 问题:driver.find_element_by_name("=").click()
1
selenium.common.exceptions.InvalidSelectorException: Message: Locator Strategy 'name' is not supported for this session

谷歌后得知:通过name获取控件的方式在Appium 1.5就被移除了。

  1. 问题:driver.find_element_by_accessibility_id('com.android.calculator2:id/digit_1').click()

报错:

1
selenium.common.exceptions.NoSuchElementException: Message: An element could not be located on the page using the given search parameters.

经请教后得知在最新版本Appium中不再支持通过id的方式来定位元素。

改用:driver.find_element_by_android_uiautomator('new UiSelector().text("1")').click(),可运行

  1. 问题: 执行appium程序时遇到如下报错,ImportError: cannot import name ‘InvalidArgumentException’,

报错原因 selenium.common.exceptions.py中未定义InvalidArgumentException类,导致出现该报错,我的解决办法是

在selenium.common.exceptions.py中直接定义了InvalidArgumentException,代码如下(C:\Users\Python35\Lib\site-packages\selenium\common)找到exceptions.py,添加下面的代码,再次执行脚本,ok.

1
2
3
4
class InvalidArgumentException(WebDriverException):
"""
"""
pass

Appium入坑指南——Appium中文版介绍

发表于 2018-11-05

Appium 介绍

Appium 是一个开源工具,用于自动化 iOS 手机 、 Android 手机和 Windows 桌面平台上的原生、移动 Web 和混合应用。“原生应用”指那些用 iOS 、 Android 或者 Windows SDK 编写的应用。“移动 web 应用”是用移动端浏览器访问的应用(Appium 支持 iOS 上的 Safari 、Chrome 和 Android 上的内置浏览器)。“混合应用”带有一个 “webview” 的包装器——用来和 Web 内容交互的原生控件。类似 Phonegap 的项目,让用 Web 技术开发然后打包进原生包装器创建一个混合应用变得容易了。

重要的是,Appium 是跨平台的:它允许你用同样的 API 对多平台写测试,做到在 iOS 、Android 和 Windows 测试套件之间复用代码。

了解 Appium “支持”这些平台意味着什么、有哪些自动化方式的详细信息,请参见 Appium 支持的平台。

Appium 的理念

Appium 旨在满足移动端自动化需求的理念,概述为以下四个原则:

  1. 你没有必要为了自动化而重新编译你的应用或者以任何方式修改它。
  2. 你不应该被限制在特定的语言或框架上来编写运行测试。
  3. 移动端自动化框架在自动化接口方面不应该重造轮子。
  4. 移动端自动化框架应该开源,不但在名义上而且在精神和实践上都要实至名归。

Appium 的设计

那么 Appium 项目的架构如何实现这一理念呢?为了实现第一点要求,我们其实使用了系统自带的自动化框架。这样,我们不需要把 Appium 特定的或者第三方的代码编译进你的应用。这意味着你测试使用的应用与最终发布的应用并无二致。我们使用以下系统自带的自动化框架:

  • iOS 9.3 及以上:苹果的 XCUITest
  • iOS 9.3 及以下:苹果的 UIAutomation
  • Android 4.2+: 谷歌的 UiAutomator
  • Android 2.3+: 谷歌的 Instrumentation(通过绑定另外的项目—— Selendroid 提供 Instrumentation 的支持)
  • Windows: 微软的 WinAppDriver

为了实现第二点要求,我们把这些(系统本身的)供应商提供的框架包装进一套 API —— WebDriver API 中。WebDriver(也叫 “Selenium WebDriver”)规定了一个客户端-服务器协议(称为 JSON Wire Protocol),按照这种客户端-服务器架构,可以使用任何语言编写的客户端向服务器发送适当的 HTTP 请求。已经有各个流行编程语言编写的客户端了。这也意味着你可以自由使用任何你想要的测试运行器和测试框架;客户端程序库不过是 HTTP 客户端,可以以任何你喜欢的方式混入你的代码。换句话说,Appium 和 WebDriver 客户端不是严格意义上的“测试框架”,而是“自动化程序库”。你可以以任何你喜欢的方式管理你的测试环境!

我们以同样的方式实现第三点要求:WebDriver 已经成为 Web 浏览器自动化事实上的标准,并且是一个 W3C 工作草案。何必在移动端做完全不同的尝试?我们通过附加可用于移动端自动化的 API 方法扩展了协议。

显然第 4 点是你在阅读的前提—— Appium 是开源的

Appium 概念

客户端/服务器架构

Appium 的核心是暴露 REST API 的网络服务器。它接受来自客户端的连接,监听命令并在移动设备上执行,答复表示执行结果的 HTTP 响应。客户端/服务器架构实际给予了许多可能性:我们可以使用任何有 http 客户端 API 的语言编写我们的测试代码,不过选一个 Appium 客户端程序库 用更容易。我们可以把服务器放在另一台机器上,而不是执行测试的机器。我们可以编写测试代码,并依靠类似 Sauce Labs 的云服务接收和解释命令。

会话(session)

自动化始终在一个会话的上下文中执行,这些客户端程序库以各自的方式发起与服务器的会话,但都以发给服务器一个 POST /session 请求结束,请求中包含一个被称作 ‘desired capabilities’ 的 JSON 对象。这时服务器就会开启这个自动化会话,并返回一个用于发送后续命令的会话 ID。

Desired Capabilities

Desired capabilities 是一些发送给 Appium 服务器的键值对集合 (比如 map 或 hash),告诉服务器我们想要启动什么类型的自动化会话。也有各种可以在自动化运行时修改服务器行为的 capabilities。例如,我们可以把 platformName capability 设置为 iOS,告诉 Appium 我们想要 iOS 会话,而不是 Android 或者 Windows 会话。我们也可以设置 safariAllowPopups capability 为 true ,确保我们在 Safari 自动化会话中可以使用 javascript 打开新窗口。有关 Appium capabilities 的完整列表,请参阅 capabilities doc 。

Appium 服务器

Appium 是用 Node.js 写的服务器。它可以从源码构建安装或者从 NPM 直接安装:

1
2
$ npm install -g appium
$ appium

Appium 客户端
有多个客户端程序库(Java、Ruby、Python、PHP,、JavaScript 和 C# 的)支持 Appium 对 WebDriver 协议的扩展,你需要用这些客户端程序库代替通常的 WebDriver 客户端。在这里浏览所有程序库的列表。

Appium.app, Appium.exe
有 Appium 服务器的图形界面包装器可以下载。它们打包了 Appium 服务器运行需要的所有东西,所以你不需要为 Node 而烦恼。它们还提供一个 Inspector 使你可以查看你应用的层级结构,这在写测试时很方便。

Appium入坑指南——Appium原理分析

发表于 2018-11-05

架构图

Appium架构图

Appium架构图-2

主要原理

  1. Appium Client

    主要是指实现了Appium功能的WebDriver协议的客户端Library,它负责与Appium服务器建立连接(JSON Wire Protocol),并将测试脚本的指令发送到Appium服务器。正如官网中文版介绍一样,它支持多种语言的客户端。

  2. Bootstrap.jar

    Bootstrap是Appium运行在目标测试机器上的一个UiAutomator测试脚本,该脚本的唯一一个测试方法所做的事情是在目标机器开启一个socket服务器来把一个session中Appium从PC端过来的命令发送给UiAutomator来执行处理。这个定义说明了Bootstrap在appium和uiautomator中究竟处于一个什么样的角色:
    首先,它是一个uiautomator的测试脚本,它的入口类Bootstrap继承于UiAutomatorTestCase,所以UiAututomator可以正常运行它,它也可以正常的使用uiautomator的方法,这个就是Appium的命令可以转换成uiautomator的命令的关键
    其次,它是一个socket服务器,它专门监听4724端口过来的appium的连接和命令数据,并把appium的命令转换成uiautomator的命令来让uiautomator进行处理
    最后,它处理的是appium从pc端过来的命令,而非一个文件。这在初次接触appium的朋友是很容易困惑的,以为appium是整个脚本文件发送到目标机器再由bootstrap进行分析处理的,事实并非如此

  3. Appium Server

    Appium服务器是Appium框架的核心。它是一个基于Node.js实现的HTTP服务器。Appium服务器的主要功能是起了一个Server(4723端口),跟selenium WebDriver测试框架类似,Appium支持标准的WebDriver JSONWireProtocol。Appium Server接收web driver标准请求,解析请求内容,调用对应的框架响应操作。将命令发送给bootstrap.jar(iOS手机为bootstrap.js)执行,并将命令的执行结果通过HTTP应答反馈给Appium客户端。

结论

Appium在Android上基于UIAutomator实现了测试的代理程序(Bootstrap.jar),在iOS上基于UIAutomation实现了测试的代理程序(Bootstrap.js)。当测试脚本运行时,每行WebDriver的脚本都将转换成Appium的指令发送给Appium服务器,而Appium服务器将测试指令交给代理程序,将由代理程序负责执行测试。比如脚本上的一个点击操作,在Appium服务器上都是touch指令,当指令发送到Android系统上时,Android系统上的Bootstrap.jar将调用UIAutomator的方法实现点击操作;而当指令发送到iOS系统上时,iOS的Bootstrap.js将调用UIAutomation的方法实现点击操作。由于Appium有了这样的能力,同样的测试脚本可以实现跨平台运行。

参考资料

https://www.jianshu.com/p/06aa73eb9b90

https://testerhome.com/topics/1972

https://discuss.appium.io/t/what-is-working-on-android-side/10222/2

Appium入坑指南——环境搭建

发表于 2018-11-05

前言:本文操作系统基于ubuntu 16.04(Windows搭建步骤大同小异),Appium有两种方式搭建:1、通过npm安装;2、通过Appium Desktop安装。

注:本系列文章仅为笔者学习笔记,大量文字均是摘录自热心网友的文章,如未注明转载皆为无心之失。

通过npm安装

安装node.js和npm

  1. 安装node.js和npmd

    从官网下载node.js最新稳定版本

  2. 本地解压: tar -Jxvf node-v8.12.0-linux-x64.tar.xz

  3. 在/etc/profile文件中添加环境变量:

    1
    2
    3
    export PATH=/home/hezj/Software/node/node-v8.12.0-linux-x64/bin:$PATH

    export PATH=/home/hezj/Software/node/node-v8.12.0-linux-x64/lib/node_modules:$PATH

    重启电脑使之生效。

  4. 验证node和npm是否安装成功

    node -v,npm -v

通过npm安装Appium

  1. npm安装需要翻墙,我们使用淘宝镜像

    配置npm的下载镜像:

    npm config set registry https://registry.npm.taobao.org

  2. 使用淘宝定制的cnpm工具代替默认的npm

    npm install -g cnpm --registry=https://registry.npm.taobao.org

  3. 使用:cnpm install express

  4. 安装Appium:cnpm install -g appium

  5. 检查Appium是否安装成功:appium -v

安装appium-doctor

安装appium-doctor,检查appium的所有依赖环境是否已经安装好

  1. npm install -g appium-doctor

  2. 运行appium-doctor:

    appium-doctor

    附上一张安装成功的截图:

遇到的问题:

  1. 未设置ANDROID_HOME环境变量:

    export ANDROID_HOME=/home/hezj/Software/AndroidStudio/SDK/

    export PATH=${ANDROID_HOME}/tools:$PATH

    export PATH=${ANDROID_HOME}/platform-tools:$PATH

通过Appium Desktop安装

  1. 在官网上下载appium-desktop,注意:ubuntu环境下要下载.AppImage结尾的文件

  2. 执行chmod a+x appium-desktop-1.7.0-x86_64.AppImage,然后sudo ./appium-desktop-1.7.0-x86_64.AppImage

    遇到的问题:

    在直接启动appium-desktop-1.7.0-x86_64.AppImage之后,点击Start Server V1.9.0,会报错,错误截图如下:

解决办法:在安装Appium时用的是root用户,而双击执行的时候是当前用户,导致报权限拒绝的错误,所以修改appium-desktop-1.7.0-x86_64.AppImage的用户组即可。

1234

Forwards

Keep coding

37 日志
GitHub 简书
© 2023 Forwards
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4