0%

本系列主要分析RecyclerView的源码。本文主要分析RecyclerView的源码结构,以及其各机构的作用。首先来看一下RecyclerView的源码结构:

从图上可以看出RecyclerView依赖了很多别的类。接下来我们首先介绍各个类的作用。

  • RecyclerViewDataObserver 数据观察器
  • Recycler View循环复用系统,核心部件
  • SavedState RecyclerView状态
  • AdapterHelper 适配器更新
  • ChildHelper 管理子View
  • ViewInfoStore 存储子VIEW的动画信息
  • Adapter 数据适配器
  • LayoutManager 负责子VIEW的布局,核心部件
  • ItemAnimator Item动画
  • ViewFlinger 快速滑动管理
  • NestedScrollingChildHelper 管理子VIEW嵌套滑动

###创建布局管理器

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
private void createLayoutManager(Context context, String className, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
if (className != null) {
className = className.trim();
if (className.length() != 0) { // Can't use isEmpty since it was added in API 9.
className = getFullClassName(context, className);
try {
ClassLoader classLoader;
if (isInEditMode()) {
// Stupid layoutlib cannot handle simple class loaders.
classLoader = this.getClass().getClassLoader();
} else {
classLoader = context.getClassLoader();
}
Class<? extends LayoutManager> layoutManagerClass =
classLoader.loadClass(className).asSubclass(LayoutManager.class);
Constructor<? extends LayoutManager> constructor;
Object[] constructorArgs = null;
try {
constructor = layoutManagerClass
.getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE);
constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes};
} catch (NoSuchMethodException e) {
try {
constructor = layoutManagerClass.getConstructor();
} catch (NoSuchMethodException e1) {
e1.initCause(e);
throw new IllegalStateException(attrs.getPositionDescription() +
": Error creating LayoutManager " + className, e1);
}
}
constructor.setAccessible(true);
setLayoutManager(constructor.newInstance(constructorArgs));
} catch (ClassNotFoundException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Unable to find LayoutManager " + className, e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Could not instantiate the LayoutManager: " + className, e);
} catch (InstantiationException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Could not instantiate the LayoutManager: " + className, e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Cannot access non-public constructor " + className, e);
} catch (ClassCastException e) {
throw new IllegalStateException(attrs.getPositionDescription()
+ ": Class is not a LayoutManager " + className, e);
}
}
}
}

如果在布局文件里面设置了布局管理器的类型,那么这里会通过反射的方式实例化出对应的布局管理器。最后将实例化出的布局管理器设置到当前的RecyclerView

###设置布局管理器

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 void setLayoutManager(LayoutManager layout) {
if (layout == mLayout) {
return;
}
//停止滚动
stopScroll();
// TODO We should do this switch a dispachLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
if (mLayout != null) {
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
}
//清空缓存
mRecycler.clear();
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout +
" is already attached to a RecyclerView: " + layout.mRecyclerView);
}
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
requestLayout();
}

设置布局管理器之前会先清空所有之前的缓存VIEW。最后通知VIEW刷新

###onMeasure
onMeasure这个回调方法用于测量VIEW的大小

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
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();

// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
processAdapterUpdatesAndSetAnimationFlags();

if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}

if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}

从上述代码可以看出,

  • 如果布局是空的,那么RecyclerView会创建调用defaultOnMeasure方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Used when onMeasure is called before layout manager is set
*/
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));

setMeasuredDimension(width, height);
}

默认测量是布局管理器根据指定的宽,高规格,算出宽高。

  • 如果是自动布局,如果宽高都是明确指定的,那么跳过测量。否则,如果
  • 目前是初始阶段,那么调用dispatchLayoutStep1
1
2
3
4
5
6
7
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
  • 第二步,用老的尺寸规格来预布局,然后调用dispatchLayoutStep2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);

mState.mStructureChanged = false;
mPendingSavedState = null;

// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
  • 这之后我们就能够获取子VIEW的宽高了。如果RecyclerView没有明确的宽高,那么我们需要再次测量,然后重复上述步骤。
  • 如果不是自动布局,如果是固定大小,那么直接用现有规格测量。否则首先进行一些动画前置操作,最后依然由布局管理器来测量。

###onLayout
onLayout是确定位置时的回调方法。

1
2
3
4
5
6
7
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}

dispatchLayout

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
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
  • 如果是初始状态,调用dispatchLayoutStep1,测量,调用dispatchLayoutStep2。如果高度或宽度发生变化,测量,调用dispatchLayoutStep2。其他情况制作测量。最后统一调用dispatchLayoutStep3主要处理动画。

###onDraw
onDraw是绘制时的回调

1
2
3
4
5
6
7
8
9
@Override
public void onDraw(Canvas c) {
super.onDraw(c);

final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}

主要对ItemDecoration的绘制

至此,RecyclerView的主要回调方法已经简单介绍完毕。

定点和着色器

OpenGl只能绘制点、直线及三角形,定义定点时总是以逆时针的顺序排列定点。这称为卷曲顺序。

告诉GPU如何绘制数据的东西被称为着色器。着色器分为顶点着色器(vertex shader)和片段着色器(fragment shader)

创建第一个顶点着色器

1
2
3
4
5
attribute vec4 a_Position;

void main() {
gl_Position = a_Position;
}

OpenGL会把gl_Position中的值当作当前定点的最终位置。

创建第一个片段着色器

1
2
3
4
5
6
7
precision mediump float;

uniform vec4 u_Color;

void main() {
gl_FragColor = u_Color;
}

OpenGl会把gl_FragColor的值作为当前片段的最终颜色

加载着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TextureResourceReader {
public static String readTextFileFromResource(Context context, int resId) {
StringBuilder sb = new StringBuilder();

try {
InputStream is = context.getResources().openRawResource(resId);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String nextLine;

while ((nextLine = br.readLine()) != null) {
sb.append(nextLine);
sb.append('\n');
}
} catch (IOException e) {
throw new RuntimeException("Could not open resource: " + resId, e);
} catch (Resources.NotFoundException e) {
throw new RuntimeException("Resource not found: " + resId, e);
}
return sb.toString();
}
}

读入着色器代码

1
2
3
4
5
6
7
8
@Override protected String readVertexShader() {
return TextureResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_shader);
}

@Override protected String readFragmentShader() {
return TextureResourceReader.readTextFileFromResource(mContext,
R.raw.simple_fragment_shader);
}

编译着色器

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 static int compileVertexShader(String shaderCode) {
return compileShader(GL_VERTEX_SHADER, shaderCode);
}

public static int compileFragmentShader(String shaderCode) {
return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}

private static int compileShader(int type, String shaderCode) {
final int shaderObjectId = glCreateShader(type);

if (shaderObjectId == 0) {
LogHelper.w(TAG, "Could not create new shader.");
return 0;
}

glShaderSource(shaderObjectId, shaderCode);
glCompileShader(shaderObjectId);

final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

LogHelper.v(TAG,
"Results of compiling source:" + "\n" + shaderCode + "\n" + glGetShaderInfoLog(
shaderObjectId));

if (compileStatus[0] == 0) {
glDeleteShader(shaderObjectId);

LogHelper.w(TAG, "Compilation of shader failed");
return 0;
}

return shaderObjectId;
}

链接着色器

一个OpenGL程序就是把一个顶点着色器和一个片段着色器链接在一起变成单个对象,顶点着色器和片段着色器总是在一起工作的。片段着色器负责绘制那些组成每个点、直线和三角形的片段;顶点着色器确定在哪里绘制。

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
public static int linkProgram(int vertextShaderId, int fragmentShaderId) {
final int programObjectId = glCreateProgram();

if (programObjectId == 0) {
LogHelper.w(TAG, "Could not create new program");
return 0;
}

glAttachShader(programObjectId, vertextShaderId);
glAttachShader(programObjectId, fragmentShaderId);

glLinkProgram(programObjectId);

final int[] status = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, status, 0);

LogHelper.v(TAG, "Result of linking program: \n" + glGetProgramInfoLog(programObjectId));

if (status[0] == 0) {
glDeleteProgram(programObjectId);
LogHelper.w(TAG, "Linking of program failed.");
return 0;
}

return programObjectId;
}

最后的拼接

  • 获取一个uniform的位置
1
2
glGetUniformLocation(program, U_COLOR);

  • 获取属性的位置
1
glGetAttribLocation(program, A_COLOR);
  • 关联属性和定点数据的数组
1
2
3
4
vertexData.position(0);
//****** very import method *******//
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT, false, STRIDE,
vertexData);
  • 使能顶点数组
1
glEnableVertexAttribArray(aPositionLocation);
  • 绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
glClear(GL_COLOR_BUFFER_BIT);

//set triangle color
//glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
//draw two triangles
glDrawArrays(GL_TRIANGLE_FAN, 0, 6);

//set line color
//glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2);

//draw the first blue mallets
//glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);

//draw the second red mallets
//glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);

OpenGl如何把坐标映射到屏幕

OpenGl会把屏幕映射到x,y轴的[-1, 1]范围内,可以给gl_PointSize赋值来改变点的大小

好好学习git以及git-flow

分散又集中

我们的git模型中有个中央仓库”truth”。注意这个仓库只能被当做中央仓库(虽然技术层面上讲,git里并没有中央仓库这个概念)。我们把这个仓库当做origin,因为所有的git用户都对这个名字比较熟悉。

每个开发者从origin拉取或者推送代码。这种关系背后,每个开发者也可以从其他子团队的节点拉取改变的东西。对于多人协作开发时这可能非常有用。上图中有Alice和Bob, Alice和David,Clair和David多个子团队。

技术层面上讲,就是Alice定义了一个远程Git,名叫bob,指向Bob的仓库。

Read more »

原文

介绍

如果你经常关注Android开发动态的话,或者关注Reactive相关的动态,最近Google有个重大发布。它们发布了针对于Android的反应式编程库:Agera

By GoogleGoogle一个从事Google Play Movies的小组。当然,这听起来更像Google.

不管是谁发布的,我们要关注的是,它与现有的反应式编程库RxJava,Reactor以及Akka-Streams有什么区别

核心API

Agera基于少值观察者模式:被观察者通过update()拿到更新和变化的信号.然后响应这些变化来算出什么改变了。以下是一个无参反应式数据流,它依赖一端update

1
2
3
4
5
6
7
8
interface Updatable {
void update();
}

interface Observable {
void addUpdatable(Updatable u);
void removeUpdatable(Updatable u);
}

它们看起来挺合理的?不幸的事,它们也有java.util.Observable和其他基于addListener/removeListener的反应式API

Agera Observable

这对方法的问题是每个添加了Updatable行为的Observable都不得不去记住原始的Updatable以便能移除同样的Updatable:

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
public final class DoOnUpdate implements Observable {
final Observable source;

final Runnable action;

final ConcurrentHashMap<Updatable, DoOnUpdatable> map;

public DoOnUpdate(Observable source, Runnable action) {
this.source = source;
this.action = action;
this.map = new ConcurrentHashMap<>();
}

@Override
public void addUpdatable(Updatable u) {
DoOnUpdatable wrapper = new DoOnUpdatable(u, action);
if (map.putIfAbsent(u, wrapper) != null) {
throw new IllegalStateException("Updatable already registered");
}
source.addUpdatable(wrapper);
}

public void removeUpdatable(Updatable u) {
DoOnUpdatable wrapper = map.remove(u);
if (wrapper == null) {
throw new IllegalStateException("Updatable already removed");
}
source.removeUpdatable(wrapper);
}

static final class DoOnUpdatable {
final Updatable actual;

final Runnable run;

public DoOnUpdatable(Updatable actual, Runnable run) {
this.actual = actual;
this.run = run;
}

@Override
public void update() {
run.run();
actual.update();
}
}
}

这导致一个争论点,在管道的每个阶段独立的下游Updatabels。是的,RxJava's SubjectsConnectableObservables也有类似的争议点,但它们之后的链式操作符不会有争议。不幸的是,反应式流规范,在当前版本,Publishers也有类似的问题。现在RxJava2.x,RscReactor完全忽略了这些东西,结果是操作起来变得更严格了。

第二个微不足道的问题是你不能多次添加相同的Updatable。因为你无法在不同的subscriptions中通过Map区分它们,如果这么干,会出现异常。通常这是很少发生的,因为大多数的终端需求都很单一。

第三个比较大的问题:当Updatable不再注册在Observable时抛出。这在终端用户触发移除操作时,而这个操作又引发别的操作,它们当中一个会抛出异常。这就是为何现在反应式变成不能够被取消的缘故。

第四个理论问题,addUpdatableremoveUpdatable会相互竞争,一些下游的操作符想在某个上游操作符已经调用了addUpdatable之后断开。一个可能的问题是removeUpdate调用抛出异常而addUpdatable成功,这回导致信号流向任何地方和导致相关的对象不想要的持有。

Agera Updatable

我们来从消费者角度看看API。Updatable是一个单一函数接口的方法,这让它能简单的给Observable添加监听

1
2
Observable source = ...
source.addUpdatable(() -> System.out.println("Something happened"));

相当简单,现在我们移除我们的监听

1
source.removeUpdatable(() -> System.out.println("Something happened");

这会产生一个异常:两个lambdas不是相同的对象。这在基于addListener/removeListener的API当中是相当常见的问题。解决方案是存储lambda,当需要的时候去用它:

1
2
3
4
5
6
7
Updatable u = () -> System.out.println("Something happened");

source.addUpdatable(u);

// ...

source.removeUpdatable(u);

有点不方便,但不会更糟了。如果你有很多Observables和很多Updatables呢?你需要记住谁注册在谁上,在相同的字段保持引用它们。Rx.NET的原始设计有个好主意来减少这种必须的单引用:

1
2
3
4
5
6
7
8
9
interface Removable extends Closeable {
@Override
void close(); // remove the necessity of try-catch around close()
}

public static Removable registerWith(Observable source, Updatable consumer) {
source.addUpdatable(consumer);
return () -> source.removeUpdatable(consumer);
}

当然,我们在调用close()时同样需要考虑:

1
2
3
4
5
6
7
8
9
public static Removable registerWith(Observable source, Updatable consumer) {
source.addUpdatable(consumer);
final AtomicBoolean once = new AtomicBoolean();
return () -> {
if (once.compareAndSet(false, true)) {
source.removeUpdatable(consumer);
}
});
}

Agera MutableRepository

如果值发生变化,Agera MutableRepository会通过update()通知注册的Updatables。这和BehaviorSubject很像,区别是新值如果不调用get()不会流到消费者:

1
2
3
4
5
6
7
MutableRepository<integer> repo = Repositories.mutableRepository(0);

repo.addUpdatable(() -> System.out.println("Value: " + repo.get());

new Thread(() -> {
repo.accept(1);
}).start();

当通过工厂方法创建仓库时,Looper中会调用update()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Set<Integer> set = new HashSet<>();

MutableRepository<integer> repo = Repositories.mutableRepository(0);

repo.addUpdatable(() -> set.add(repo.get()));

new Thread(() -> {
for (int i = 0; i < 100_000; i++) {
repo.accept(i);
}
}).start();

Thread.sleep(20_000);

System.out.println(set.size());

20秒之后Set最终会有多大?有人可能会认为是100,000。事实上,值可能在1~100,000之间!原因是accept()get()并发运行,如果消费者运行比较慢,accept()会覆盖当前仓库的值。

错误处理

使用异步通常意味着会碰到异步相关的问题。RxJava和其他类似的东西都会有这种问题:出现某种错误了,这个流程自动清掉,而开发者希望的是能够重新开始。错误和清理在某些情况下很复杂,成熟的库都花费了大量的精力来解决这些问题,因此开发者不需要在这上面浪费很多时间。

Agera基础API不会自己处理错误,你需要自己对错误进行处理。如果你用Agera组成多个服务,你需要建立自己的错误处理框架.由于并发和中断状态使它处理起来很笨重和延迟。

终结

Agera对一个完成流不会发出通知,你需要自己知道它什么时候会完成。这在简单用户界面上不会有什么问题。然而,后台异步操作需要知道要发出多少信号,没有数据的话,你如何收到update()相关通知

如何设计现代化的无参反应式API

看看以下例子:

1
2
3
4
5
6
7
8
9
rx.Observable<Void> signaller = ...

rx.Observer<Void> consumer = ...

Subscription s = signaller.subscribe(consumer);

// ...

s.unsubscribe();

你很简单的就能获得它该有的功能。你如果要处理别的类型信号,将Void替换成你想要的类型就可以了。

如果一个库由于需要学习很多操作符而让人感到很笨重的话,你可以拷贝它,删除你不需要的东西。当然,你需要更新修复问题和性能优化后的代码。

如果拷贝和修改听起来不够有吸引力,你能按照Reactive-Streams规定开发自己的库;Publisher<Void>,<Subscriber<Void>和其他东西。你能很方便的与其他的Reactive-Streams库一起工作,你能通过它的兼容性测试来测试你的库。

当然,编写反应式库很麻烦,按照Reactive-Streams编写更麻烦。所以,综合考虑,你可以拓展API

如果你真的想要编写无参反应式流,这有几个你需要考虑的建议:

1)不要分开addListenerremoveListener单一的入口简化中间操作的开发。

1
2
3
4
5
6
7
interface Observable {
Removable register(Updatable u);
}

interface Removable {
void remove();
}

2)考虑支持取消和移除而不是返回一个可以取消的令牌或者可移除的动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Observable {
void register(Updatable u);
}

interface Updatable {
void onRegister(Removable remover);
void update();
}

// or

interface Updatable {
void update(Removable remover);
}

3)考虑至少添加一个错误信号接收:

1
2
3
4
5
interface Updatable {
void onRegister(Removable remover);
void update();
void error(Throwable ex);
}

4)考虑提供队列的异步操作.

结论

其实作者就是来黑Agera的。

APK瘦身系列

1.解剖APK

如果我问开发者他们的APP有多大,我能很确定,大部分人会去看下Android Studio生成的APK有多大,然后告诉我。这是最直接的答案。考虑以下例子:

  • 当你的APP安装在用户的设备上时会占用多大的空间?
  • 用户需要花费多少的网络流量来下载和安装您的APP
  • 更新APP时,需要下载多少内容?
  • 你的APP运行时占用了多少内存?
    Read more »

Android开发30条经验

有两种人,一种是自己摸索来学习,一种是通过别人的建议。这儿有些经验我想与你们分享:

Read more »

习惯JavaScript(7)

  1. 了解使用的JavaScript版本
  2. 理解JavaScript浮点数
  • JavaScript的数字都是双精度浮点数
  • JavaSCript的整型是浮点数的一个子集,不是单独的类型
  • 位运算符将数字视为32位有符号整数
  • 浮点数中的精度陷阱
  1. 当心隐式的强制转换
  • 类型错误可能被隐式的强制转换所隐藏
  • 重载的运算符+是进行加法运算还是字符串连接取决于参数类型
  • 对象通过valueOf方法强制转换成数字,通过toString方法强制转换为字符串
  • 具有valueOf方法的对象应该实现toString方法,返回一个valueOf方法产生的数字字符串表示
  • 测试一个值是否为未定义的值,应该使用typeof或者与undefined进行比较而不是用真值运算
  1. 原型类型优于封装对象
  • 当做相等比较时,原始类型的封装对象与其原始值行为不一样
  • 或者和设置原始类型值的属性会隐式地创建封装对象
  1. 避免对混合类型使用== 运算符
  • 当类型不同时,==运算符应用了一套难以理解的隐式强制转换规则
  • 使用===
  1. 了解分号插入的局限
  2. 视字符串为16位的代码单元序列
  • JavaScript字符串由16位的代码单元组成,而不是由Unicode组成
  • JavaScript使用两个代码单元表示2^16及其以上的Unicode的代码点。这两个代码单元被称为代理对。
  • 代理对甩开了字符串元素计数,length、charAt、charCodeAt方法以及正则表达式模式(例如.)受到了影响
  • 使用第三方库识别代码点的字符串操作

变量作用域(10)

  1. 尽量减少全局对象
  2. 始终声明局部变量
  3. 避免使用width
  4. 熟练掌握闭包
  • 函数可以引用定义在其外部作用域的变量
  • 闭包比创建它们的函数有更长的生命周期
  • 闭包在内部存储其外部变量的引用,并能读写这些变量
  1. 理解变量声明提升
  • 在代码块中的变量声明会被隐式地提升到闭包函数的顶部
  • 重声明变量可视为单个变量
  • 考虑手动提升局部变量的声明,从而避免混淆
  1. 使用立即调用的函数表达式创建局部作用域
  • 理解绑定和赋值的区别
  • 闭包通过引用而不是值捕获它们的外部变量
  • 使用立即调用的函数表达式来创建局部作用域
  • 当心在立即调用的函数表达式中包裹代码块可能会改变其行为的情形
  1. 当心命名函数表达式笨拙的作用域
  2. 当心局部块函数声明笨拙的作用域
  3. 避免使用Eval创建局部变量
  4. 间接调用Eval函数优于直接调用

使用函数(12)

  1. 理解函数调用、方法调用以及构造函数调用之间的不同
  • 方法调用将被查找方法属性的对象作为调用接收者
  • 函数调用将全局对象作为接收者。
  • 构造函数需要通过new运算符调用,并产生一个新的对象作为其接收者
  1. 熟练掌握高阶函数
  • 高阶函数是那些将函数作为参数或者返回值的函数
  1. 使用call方法自定义接收者来调用方法
  • 使用call方法可以调用在给定的对象中不存在的方法
  • 使用call方法定义高阶函数允许使用者给回调函数指定接收者
  1. 使用apply方法通过不同数量的参数调用函数
  • 使用apply方法指定一个可计算的参数数组来调用可变参数的函数
  • 使用apply方法的第一个参数给可变参数的方法提供一个接收者
  1. 使用arguments创建可变参数的函数
  • 使用隐式的arguments对象实现可变参数的函数
  • 考虑对可变参数提供一个额外的固定元数的版本,从而使使用者无需借助apply方法
  1. 永远不要修改arguments对象
  • 使用[].slice.call(arguments)arguments对象复制到一个真正的数组中再进行修改
  1. 使用变量保持arguments的引用
  • 当引用arguments时当心函数嵌套层级
  • 绑定一个明确作用域的引用到arguments变量,从而可以在嵌套的函数中引用它
  1. 使用bind方法提取具有确定接收者的方法
  • 要注意,提取一个方法不会将方法的接收者绑定到方法的对象上
  • 当给高阶函数传递对象方法时,使用匿名函数在适当的接收者上调用该方法
  • 使用bind方法创建绑定到适当接收者的函数
  1. 使用bind方法实现函数柯里化(将函数与其参数的一个子集绑定)
  • 使用bind方法实现函数柯里化,即创建一个固定需求参数子集的委托函数
  • 传入nullundefined作为接收者的参数来实现函数柯里化
  1. 使用闭包而不是字符串来封装代码
  • 当将字符串传递给eval函数以执行它们的API时,绝不要在字符串中包含局部变量的引用
  • 接受函数调用的API由于使用eval函数执行字符串的API
  1. 不要信赖函数对象的toString方法
  2. 避免使用非标准的栈检查属性

对象和原型(13)

  1. 理解prototype、getPrototypeOf和__proto__之间的不同
  • C.prototype属性是new C()创建的对象原型
  • Object.getPrototypeOf(obj)ES5中检索对象原型的标准函数
  • obj.__proto__是检索对象原型的非标准方法
  • 类是由一个构造函数和一个关联的原型组成的一种设计模式
  1. 使用Object.getPrototypeOf函数而不要使用__proto__属性
  2. 始终不要修改__proto__属性
  • 使用Object.create函数给新对象设置自定义的原型
  1. 使构造函数与new操作符无关
  2. 在原型中存储方法
  • 将方法存储在实例对象中将创建该函数的多个副本,因为每个实例对象都有一份副本
  • 将方法存储于原型中优于存储在实例对象中
  1. 使用闭包存储私有数据
  • 闭包变量是私有的,只能通过局部的引用获取
  • 将局部变量作为私有数据从而通过方法实现信息隐藏
  1. 只将实例状态存储在实例对象中
  • 共享可变数据可能会出问题,因为原型是被其所有的实例共享的
  • 将可变的实例状态存储在实例对象中
  1. 认识到this变量的隐式绑定问题
  • this变量的作用域总是由其最近的封闭函数所绑定
  • 使用一个局部变量(通常命名为selfmethat)使得this绑定对于内部函数是可用的
  1. 在子类的构造函数中调用父类的构造函数
  • 使用Object.create函数来构造子类的原型对象以避免调用父类的构造函数
  1. 不要重用父类的属性名
  2. 避免继承标准类
  • 使用属性委托优于继承标准类
  1. 将原型视为实现细节
  • 对象是接口,原型是实现
  1. 避免使用轻率的猴子补丁

数组和字典(10)

  1. 使用Object的直接实例构造轻量级的字典
  2. 使用null原型以防止原型污染
  3. 使用hasOwnProperty方法以避免原型污染
  • 使用词法作用域和call方法避免覆盖hasOwnProperty方法
  1. 使用数组而不要使用字典来存储有序集合
  2. 绝不要在Object.prototype中增加可枚举的属性
  • 避免在Object.prototype中增加属性
  • 考虑编写一个函数代替Object.prototype方法
  1. 避免在枚举期间修改对象
  • 当使用for..in循环枚举一个对象的属性时确保不要修改该对象
  • 当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应该使用while循环或经典的for循环来替代for..in循环
  • 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构
  1. 数组迭代要优先使用for循环而不是for..in循环
  2. 迭代方法优先于循环
  3. 在类数组对象上复用通用的数组方法
  4. 数组字面量优于数组构造函数

库和API设计(8)

  1. 保持一致的约定
  2. undefined看做“没有值”
  • 在允许0、NaN或空字符串为有效参数的地方,绝不要通过真值测试来实现参数默认值
  1. 接收关键字参数的选项对象
  • 使用extend函数抽象出从选项对象中提取值的逻辑
  1. 避免不必要的状态
  • 尽可能地使用无状态的API
  • 如果API是有状态的,标示出每个操作与哪些状态有关联
  1. 使用结构类型设计灵活的接口
  • 使用结构类型来设计灵活的对象接口
  • 结构接口更灵活、更轻便,所以应该避免使用继承
  1. 区分数组对象和类数组对象
  2. 避免过度的强制转换
  3. 支持方法链
  • 使用方法链来连接无状态的操作
  • 通过在无状态的方法中返回新对象来支持方法链
  • 通过在有状态的方法中返回this来支持方法链

并发(8)

  1. 不要阻塞I/O事件队列
  • 异步API使用回调函数来延缓代价高昂的操作以避免阻塞主应用程序
  • JavaScript并发地接收事件,但会使用一个事件队列按序地处理事件处理程序
  • 在应用程序事件队列中绝不要使用阻塞的I/O
  1. 在异步序列中使用嵌套或命名的回调函数
  • 使用嵌套或命名的回调函数按顺序地执行多个异步操作
  1. 当心丢弃错误
  2. 对异步循环使用递归
  • 循环不能是异步
  • 使用递归函数在事件循环的单独伦次中执行迭代
  • 在事件循环的单独轮次中执行递归,并不会导致调用栈溢出
  1. 不要在计算时阻塞事件队列
  2. 使用计数器来执行并行操作
  3. 绝不要同步地调用异步的回调函数
  4. 使用promise模式清洁异步逻辑
  • promise代表最终值,即并行操作完成时最终产生的结果
  • 使用promise组合不同的并行操作
  • 使用promise模式的API避免数据竞争
  • 在要求有意的竞争条件时使用select

开发记录

Debug

1
2
3
4
5
6
7
8
9
10
FragmentManager.enableDebugLoggin(true)

android.os.Debug.waitForDebugger();

//trigger your service
<service
android:name="...."
android:exported="true"
android:process=":sync" />

Log

hugo

使用Retrofit2.0和RxJava的MVP方案

设计符合android(MVP)方案的网络架构很有挑战的。当然,更具有挑战的是该设计也还完全符合Android的生命周期。我们能够使用RetrofitVolley,或者AsyncTask能构建出我们的MVP方案,但我们如何处理当屏幕旋转时的网络请求?

本文的目的是介绍一个能够很简单的使用MVP模式的网络框架,并且能够保证Activity/Fragment生命周期安全。本文需要RxJavaRetrofit的基本认识。

Read more »

什么是ReactiveX

ReactiveX是一种API,主要关注使用观察者模式,迭代器模式和具有函数式编程特点的可观察的数据流或者事件的组合和操作。能够处理实时数据,具有高效,简洁,拓展性强的特点。使用观察者和操作者来操作它们,ReactiveX提供一种可组合和弹性API来创建和操作数据流,简化异步编程的一些顾虑,如线程创建和并发问题。

Read more »