Android 性能优化

布局优化

  • 删除布局中无用的控件和层级,其次有选择的使用性能较低的 ViewGroup,比如 RelativeLayout, ConstraintLayout 。同样层级的话更优先选择使用 LinearLayout,因为RelativeLayout 的布局过程需要花费更多的 CPU 时间。如果需要嵌套的方式来做,更建议采用 RelativeLayout ,因为 ViewGroup 的嵌套就相当于增加了布局的层级,同样会降低程序的性能。
  • 采用 <include> 标签,如果多个布局中需要一个相同的 layout ,可以使用<include> 来避免写重复的布局文件。
  • 采用 <merge> 标签。一般和 <include> 标签一起使用而减少布局的层级。
  • ViewStub。它非常轻量级,而且高/宽都是 0,因此它本身不参与任何的布局和绘制过程。ViewStub 的意义在于按需加载所需的布局文件,在实际开发中,有很多布局文件在正常情况下不会显示,这个时候就没有必要在整个界面初始化的时候将其加载进来,通过 ViewStub 就可以做到在使用的时候再加载。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <ViewStub
    android:id="@+id/stub_import"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:inflatedId="@+id/id_login"
    android:layout="@layout/refresh_layout" />

    其中 stub_import 是 ViewStub 的 id,id_login 是 refresh_layout 这个布局根元素的 id, 需要加载 ViewStub 的布局时,有两种方式:

    ((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);

    或者

    View layout = ((ViewStub)findViewById(R.id.stub_import)).inflate();

绘制优化

绘制优化是指 ViewonDraw 方法要避免执行大量的操作。

onDraw 里不要创建新的局部对象 不要做耗时的操作。

内存泄露优化

内存泄露优化分为两个方面,一方面是在开发过程中避免写出有内存泄漏的代码,另一方面是通过一些分析工具找出潜在的内存泄漏继而解决。

场景1 静态变量导致的内存泄漏

下面的代码将导致 Activity 无法正常销毁,因为静态变量 sContext 引用了它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends Activity{

private static final String TAG = "MainActivity";

private static Context sContext;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

sContext = this;
}
}

解决方案:

  1. onDestroy 中释放 sContext = null
  2. 变成虚引用 private static WeakReference<Context> context

场景2 单例模式导致的内存泄漏

如下所示,提供一个单例模式的 TestManagerTestManager可以接受外部的注册并将外部的监听器存储起来。

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
public class TestManager{
private List<OnDataArrivedListener> mOnDataArrivedListeners = new ArrayList<>();

private static class SingletonHolder {
public static final TestManager INSTANCE = new TestManager();
}

private TestManager(){

}

public static TestManager getInstance(){
return SingletonHolder.INSTANCE;
}

public synchronized void registerListener(OnDataArrivedListener listener){
if(!mOnDataArrivedListeners.contains(listener)){
mOnDataArrivedListeners.add(listener);
}
}

public synchronized void unregisterListener(OnDataArrivedListener listener){
mOnDataArrivedListeners.remove(listener);
}

public interface OnDataArrivedListener{
void onDataArrived(Object data);
}
}

Activity 实现 OnDataArrivedListener 并向 TestManager 注册监听,如下所示。下面的代码由于缺少解注册的操作而引起内存泄漏,泄露的原因是 Activity 的对象被单例模式的 TestManager 所持有,而单例模式的特点是其生命周期和 Application 保持一致,因此 Activity 无法被及时释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends Activity{

private static final String TAG = "MainActivity";

private static Context sContext;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TestManager.getInstance().registerListener(this);
}
}

解决方案:

onDestroyTestManager.getInstance().unregisterListener(this);

场景3 属性动画导致的内存泄漏

属性动画中有一类无限循环的动画,如果在 Activity 中播放此类动画并且没有在 onDestroy 中去停止动画,那么动画会一直播放下去,尽管已经无法在页面上看到动画效果了,并且这个时候 ActivityView 会被动画持有,而 View 又持有了 Activity,最终 Activity 无法释放。

解决方法

是在 onDestroy 中停止动画。

场景4 非静态内部类导致的内存泄漏

比如 Handler

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
public class MainActivity extends AppCompatActivity {

private MyHandler handler;

private static class MyHandler extends Handler {

private WeakReference<MainActivity> reference;

public MyHandler(MainActivity activity) {
reference = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
//TODO: do something
reference.get().doSomeThing();
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

handler = new MyHandler(this);
}

@Override
protected void onDestroy() {
handler.removeMessages(1);
super.onDestroy();
}
}

响应速度优化和 ANR 日志分析

响应速度优化的核心思想是避免在主线程中做耗时操作。响应速度过慢更多的体现在 Activity 的启动速度上,如果在主线程中做太多事情,会导致 Activity 启动时出现黑屏现象,甚至出现 ANR(Application not response)Android 规定,Activity 如果 5s 内无法响应屏幕触摸事件或者键盘输入事件就会出现 ANRBroadcastReceiver 如果 10s 之内还未执行完操作也会出现 ANRService 如果 20s 之内还未执行完操作也会出现 ANR

当一个进程发生了 ANR 以后,系统会在data/anr 目录下创建一个 traces.txt,通过分析这个文件就能定位出 ANR 的原因。

ListView 优化 和 Bitmap 优化

ListView 优化方式

  • 布局复用
  • 使用 ViewHolder,减小 findViewById 的使用,避免在 getView 中执行耗时操作
  • 在列表快速滑动时不适合开启大量的异步任务的。
  • 数据可以分页加载。
  • 可以尝试开启硬件加速来使 ListView 的滑动更加流畅。
  • 尽量让 ItemView 的 Layout 层次结构简单。
  • ItemView 元素避免半透明。
  • 尽量能保证 Adapter 的 hasStableIds() 返回 true,这样在 notifyDataSetChanged() 的时候,如果 id 不变,ListView 将不会重新绘制这个 View,达到优化的目的。
  • 每个 ItemView 不能太高,特别是不要超过屏幕的高度。

Bitmap 优化方式

  • 加载合适尺寸的图片(二次采样,inSampleSize)。

线程优化

线程优化的思想是使用线程池,避免程序中存在大量的 Thread。线程池可以重用内部的线程,从而避免了线程的创建和销毁所带来的性能开销,同时线程池还可以有效的控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而导致堵塞现象的发生。

一些性能优化建议

  • 避免创建过多的对象。
  • 不要过多使用枚举,枚举占用的内存空间要比整形大。(不过枚举要是能影响性能的话,就太差了)
  • 常量请使用 static final 来修饰。
  • 使用 Android 特有的数据结构,比如 SparseArrayPair 等。
  • 适当使用软引用和弱引用。
  • 尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄漏。

Android View事件分发

前段时间看了《Android开发艺术探索》,关于 Android 事件分发,写了一些笔记。
事件分发机制的伪代码。

1
2
3
4
5
6
7
8
9
10
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else{
consume = child.dispatchTouchEvent(ev);
}

return consume;
}

对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,它的 dispatchTouchEvent 方法会被调用,如果这个 ViewGrouponInterceptTouchEvent 方法返回 true 则表示它要拦截此事件,事件就会交给它自己处理,即调用它的 onTouchEvent 方法;如果它的 onInterceptTouchEvent 方法返回 false 表示不会拦截这个事件,当前事件就会传递给它的子元素,子元素的 dispatchTouchEvent 方法会被调用,如此循环知道事件被处理。

当一个 View 需要处理事件时,如果它已经设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调,这是事件如何处理还要看 onTouch 的返回值,如果返回 true 则当前 ViewonTouchEvent 方法会被调用;如果返回 false ,那么 onTouchEvent 方法不会被调用。由此可见方法的优先级 onTouch > onTouchEvent > onClick

点击事件的传递过程遵循以下顺序:Activity -> Window -> View,即事件总是先传递给 ActivityActivity 再传递给 Window,最后再传递给顶级 View,顶级 View 接收到事件后,就会按照事件分发机制分发事件。

如果一个 ViewOnTouchEvent 方法返回 false,那么他的父容器的 OnTouchEvent 会被调用,以此类推。如果所有的元素都不处理这个事件,那么这个事件最终会传递给 Activity 处理。

注意:

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up事件结束。
  2. 正常情况下,一个事件序列只能被一个 View 拦截并消耗。因为一旦一个元素拦截了某事件,那么同一个序列内的所有事件都会直接交给他处理,因此同一个序列中的事件不能分别有两个 View 同时处理。但是通过特殊手段可以做到,比如一个 View 将本该有它处理的事件通过 OnTouchEvent 强行传递给其他 View 处理。
  3. 某个 View 一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 方法不会再被调用。
  4. 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一个事件序列里的其他事件都不会再交给他处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 方法会被调用。
  5. 如果 View 不消除掉 ACTION_DOWN 之外的其他事件,那么这个点击事件就会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  6. ViewGroup 方法默认不拦截任何事件。Android 源码中 ViewGrouponInterceptTouchEvent 方法默认返回 false
  7. View 没有 onInterceptTouchEvent 方法,只有有子元素的元素才有。所以只要有点击事件传递给 View,就会调用它的 onTouchEvent 方法。
  8. ViewonTouchEvent 默认都会消耗事件,除非它是不可点击的。ViewlongClickable 属性默认都为 false
  9. Viewenable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 Viewdisable 状态的,只要它的 clickablelongClickable 有一个为 true,那么它的 onTouchEvent 就返回 true
  10. onClick 发生的前提是当前 View 是可点击的。并且它收到了 downup 的事件。
  11. 事件传递过程是由外到内的,即事件总是先传递给父元素,然后再由父元素下发到子元素。

CrashHandler

源码

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
public class CrashHandler implements Thread.UncaughtExceptionHandler {

private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";
private static final String FILE_NAME = "crash";
private static final String FILE_NAME_SUFFIX = ".trace";

private static CrashHandler sInstance = new CrashHandler();
private Thread.UncaughtExceptionHandler mDefaultCrashHandler;
private Context mContext;

private CrashHandler() {
}

public static CrashHandler getInstance() {
return sInstance;
}

public void init(Context context) {
mContext = context.getApplicationContext();
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}

@Override
public void uncaughtException(Thread t, Throwable e) {
try {
dumpExceptionToSDCard(e);
uploadExceptionToServer();
} catch (IOException e1) {
e1.printStackTrace();
}

if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(t, e);
} else {
Process.killProcess(Process.myPid());
}

}

private void dumpExceptionToSDCard(Throwable t) throws IOException {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.w(TAG, "sdcard unmounted, skip dump exception");
return;
}
}

File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}

long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(current));
File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);

try {
PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
printWriter.println(time);
dumpPhoneInfo(printWriter);
printWriter.println();
t.printStackTrace(printWriter);
printWriter.close();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "dump crash info failed ");
e.printStackTrace();
}
}

private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
PackageManager manager = mContext.getPackageManager();
PackageInfo pi = manager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.print("App Version: ");
pw.print(pi.versionName);
pw.print("_");
pw.println(pi.versionCode);

//Android版本号
pw.print("OS Version: ");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.println(Build.VERSION.SDK_INT);

//手机制造商
pw.print("Vendor: ");
pw.println(Build.MANUFACTURER);

//手机型号
pw.print("Model: ");
pw.println(Build.MODEL);

//CPU架构
pw.print("CPU ABI: ");
pw.println(Build.CPU_ABI);
}

private void uploadExceptionToServer() {
//TODO: Upload Exception Message To Web Server
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BaseApplication extends Application {

private static BaseApplication sInstance;

@Override
public void onCreate() {
super.onCreate();
sInstance = this;
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
}

public static BaseApplication getInstance() {
return sInstance;
}

}

RxJava详解

RxJava

RxJava 到底是什么

一个词:异步。

RxJava 在 GitHub 主页上的自我介绍是 “a library for composing asynchronous and event-based programs using observable sequences for the Java VM”(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)。这就是 RxJava ,概括得非常精准。

然而,对于初学者来说,这太难看懂了。因为它是一个『总结』,而初学者更需要一个『引言』。

其实, RxJava 的本质可以压缩为异步这一个词。说到根上,它就是一个实现异步操作的库,而别的定语都是基于这之上的。

RxJava 好在哪

换句话说,『同样是做异步,为什么人们用它,而不用现成的 AsyncTask / Handler / XXX / … ?』

一个词:简洁。

异步操作很关键的一点是程序的简洁性,因为在调度过程比较复杂的情况下,异步代码经常会既难写也难被读懂。 Android 创造的 AsyncTask 和Handler ,其实都是为了让异步代码更加简洁。RxJava 的优势也是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁。

API 介绍和原理简析

1.概念:扩展的观察者模式

RxJava 的异步实现,是通过一种扩展的观察者模式来实现的。

观察者模式

观察者模式面向的需求是:A 对象(观察者)对 B 对象(被观察者)的某种变化高度敏感,需要在 B 变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者( 例如 A 不需要每过 2ms 就检查一次 B 的状态),而是采用注册 ( Register ) 或者称为订阅 ( Subscribe )的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 Android 开发中一个比较典型的例子是点击监听器 OnClickListener 。对设置 OnClickListener 来说, View 是被观察者, OnClickListener 是观察者,二者通过 setOnClickListener() 方法达成订阅关系。订阅之后用户点击按钮的瞬间,Android Framework 就会将点击事件发送给已经注册的 OnClickListener 。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。

OnClickListener 的模式大致如下图:
OnClickListener模式

如图所示,通过 setOnClickListener() 方法,Button 持有 OnClickListener 的引用;当用户点击时,Button 调用 OnClickListeneronClick() 方法。另外,如果把这张图中的概念抽象出来(Button -> 被观察者、OnClickListener -> 观察者、setOnClickListener() -> 订阅,onClick() -> 事件),就由专用的观察者模式(例如只用于监听控件点击)转变成了通用的观察者模式。如下图:
通用的观察者模式

RxJava 的观察者模式

RxJava 有四个基本概念:Onservable(被观察者)、Observer(观察者)、subscribe(订阅)、Event(事件)。ObservableObserver 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出 Event 来通知 Observer

与传统观察者模式不同, RxJava 的事件回调方法除了普通事件 onNext() (相当于 onClick() / onEvent())之外,还定义了两个特殊的事件:onCompleted()onError()

  • onCompleted(): 事件队列完结。RxJava 不仅把每个事件单独处理,还会把它们看做一个队列。RxJava 规定,当不会再有新的 onNext() 发出时,需要触发 onCompleted() 方法作为标志。
  • onError(): 事件队列异常。在事件处理过程中出异常时,onError() 会被触发,同时队列自动终止,不允许再有事件发出。
  • 在一个正确运行的事件序列中, onCompleted()onError() 有且只有一个,并且是事件序列中的最后一个。需要注意的是,onCompleted()onError() 二者也是互斥的,即在队列中调用了其中一个,就不应该再调用另一个。

RxJava 的观察者模式大致如下图:
RxJava 的观察者模式

2. 基本实现

基于以上的概念, RxJava 的基本实现主要有三点:

1) 创建 Observer

Observer 即观察者,它决定事件触发的时候将有怎样的行为。 RxJava 中的 Observer 接口的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Observer<String> observer = new Observer<String>() {
@Override
public void onNext(String s) {
Log.d(tag, "Item: " + s);
}

@Override
public void onCompleted() {
Log.d(tag, "Completed!");
}

@Override
public void onError(Throwable e) {
Log.d(tag, "Error!");
}
};

除了 Observer 接口之外,RxJava 还内置了一个实现了 Observer 的抽象类:Subscriber。 Subscriber 对 Observer 接口进行了一些扩展,但他们的基本使用方式是完全一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Subscriber<String> subscriber = new Subscriber<String>() {
@Override
public void onSubscribe(Subscription s) {
Log.d(tag, "onSubscribe");
}

@Override
public void onNext(String s) {
Log.d(tag, "Item: " + s);
}

@Override
public void onError(Throwable e) {
Log.d(tag, "Error!");
}

@Override
public void onComplete() {
Log.d(tag, "Completed!");
}
};

不仅基本使用方式一样,实质上,在 RxJava 的 subscribe 过程中,Observer 也总是会先被转换成一个 Subscriber 再使用。所以如果你只想使用基本功能,选择 Observer 和 Subscriber 是完全一样的。它们的区别对于使用者来说主要有:

  • onSubscribe(): 这是 Subscriber 增加的方法。它会在 subscribe 刚开始,而事件还未发送之前被调用,可以用于做一些准备工作,例如数据的清零或重置。这是一个可选方法,默认情况下它的实现为空。需要注意的是,如果对准备工作的线程有要求(例如弹出一个显示进度的对话框,这必须在主线程执行), onSubscribe() 就不适用了,因为它总是在 subscribe 所发生的线程被调用,而不能指定线程。要在指定的线程来做准备工作,可以使用 doOnSubscribe() 方法,具体可以在后面的文中看到。

2) 创建 Observable

Observable 即被观察者,它决定什么时候触发事件以及触发怎样的事件。 RxJava 使用 create() 方法来创建一个 Observable ,并为它定义事件触发规则:

1
2
3
4
5
6
7
8
9
Observable observable = Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext("Hello");
subscriber.onNext("Hi");
subscriber.onNext("Aloha");
subscriber.onCompleted();
}
});

可以看到,这里传入了一个 OnSubscribe 对象作为参数。OnSubscribe 会被存储在返回的 Observable 对象中,它的作用相当于一个计划表,当 Observable 被订阅的时候,OnSubscribecall() 方法会自动被调用,事件序列就会依照设定依次触发(对于上面的代码,就是观察者 Subscriber 将会被调用三次 onNext() 和一次 onCompleted())。这样,由被观察者调用了观察者的回调方法,就实现了由被观察者向观察者的事件传递,即观察者模式。

这个例子很简单:事件的内容是字符串,而不是一些复杂的对象;事件的内容是已经定好了的,而不像有的观察者模式一样是待确定的(例如网络请求的结果在请求返回之前是未知的);所有事件在一瞬间被全部发送出去,而不是夹杂一些确定或不确定的时间间隔或者经过某种触发器来触发的。总之,这个例子看起来毫无实用价值。但这是为了便于说明,实质上只要你想,各种各样的事件发送规则你都可以自己来写。至于具体怎么做,后面都会讲到,但现在不行。只有把基础原理先说明白了,上层的运用才能更容易说清楚。

create() 方法是 RxJava 最基本的创造事件序列的方法。基于这个方法, RxJava 还提供了一些方法用来快捷创建事件队列,例如:

  • just(T...): 将传入的参数依次发送出来。
1
2
3
4
5
6
Observable observable = Observable.just("Hello", "Hi", "Aloha");
// 将会依次调用:
// onNext("Hello");
// onNext("Hi");
// onNext("Aloha");
// onCompleted();
  • from(T[]) / from(Iterable<? extends T>) : 将传入的数组或 Iterable 拆分成具体对象后,依次发送出来。
1
2
3
4
5
6
7
String[] words = {"Hello", "Hi", "Aloha"};
Observable observable = Observable.from(words);
// 将会依次调用:
// onNext("Hello");
// onNext("Hi");
// onNext("Aloha");
// onCompleted();

3) Subscribe (订阅)

创建了 ObservableObserver 之后,再用 subscribe() 方法将它们联结起来,整条链子就可以工作了。代码形式很简单:

1
2
3
observable.subscribe(observer);
// 或者:
observable.subscribe(subscriber);

有人可能会注意到, subscribe() 这个方法有点怪:它看起来是『observalbe 订阅了 observer / subscriber』而不是『observer / subscriber 订阅了 observalbe』,这看起来就像『杂志订阅了读者』一样颠倒了对象关系。这让人读起来有点别扭,不过如果把 API 设计成 observer.subscribe(observable) / subscriber.subscribe(observable) ,虽然更加符合思维逻辑,但对流式 API 的设计就造成影响了,比较起来明显是得不偿失的。

Observable.subscribe(Subscriber) 的内部实现是这样的(仅核心代码):

1
2
3
4
5
6
7
// 注意:这不是 subscribe() 的源码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的核心代码。
// 如果需要看源码,可以去 RxJava 的 GitHub 仓库下载。
public Subscription subscribe(Subscriber subscriber) {
subscriber.onStart();
onSubscribe.call(subscriber);
return subscriber;
}

可以看到,subscriber() 做了3件事:

  1. 调用 Subscriber.onStart() 。这个方法在前面已经介绍过,是一个可选的准备方法。

  2. 调用 Observable 中的 OnSubscribe.call(Subscriber) 。在这里,事件发送的逻辑开始运行。从这也可以看出,在 RxJava 中, Observable 并不是在创建的时候就立即开始发送事件,而是在它被订阅的时候,即当 subscribe() 方法执行的时候。

  3. 将传入的 Subscriber 作为 Subscription 返回。这是为了方便 unsubscribe().

整个过程中对象间的关系如下图:
image

或者可以看动图:

image

4) 场景示例

a. 打印字符串数组

将字符串数组 names 中的所有字符串依次打印出来:

1
2
3
4
5
6
7
8
String[] names = ...;
Observable.from(names)
.subscribe(new Action1<String>() {
@Override
public void call(String name) {
Log.d(tag, name);
}
});

b. 由 id 取得图片并显示

由指定的一个 drawable 文件 id drawableRes 取得图片,并显示在 ImageView 中,并在出现异常的时候打印 Toast 报错:

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
int drawableRes = ...;
ImageView imageView = ...;
Observable.create(new OnSubscribe<Drawable>() {
@Override
public void call(Subscriber<? super Drawable> subscriber) {
Drawable drawable = getTheme().getDrawable(drawableRes));
subscriber.onNext(drawable);
subscriber.onCompleted();
}
}).subscribe(new Observer<Drawable>() {
@Override
public void onNext(Drawable drawable) {
imageView.setImageDrawable(drawable);
}

@Override
public void onCompleted() {

}

@Override
public void onError(Throwable e) {
Toast.makeText(activity, "Error!", Toast.LENGTH_SHORT).show();
}
});

注意:在 RxJava 的默认规则中,事件的发出和消费都是在同一个线程的。也就是说,如果只用上面的方法,实现出来的只是一个同步的观察者模式。观察者模式本身的目的就是『后台处理,前台回调』的异步机制,因此异步对于 RxJava 是至关重要的。而要实现异步,则需要用到 RxJava 的另一个概念:Scheduler

3. 线程控制 —— Scheduler (一)

在不指定线程的情况下, RxJava 遵循的是线程不变的原则,即:在哪个线程调用 subscribe(),就在哪个线程生产事件;在哪个线程生产事件,就在哪个线程消费事件。如果需要切换线程,就需要用到 Scheduler (调度器)。

Scheduler 的 API

RxJava 中,Scheduler ——调度器,相当于线程控制器,RxJava 通过它来指定每一段代码应该运行在什么样的线程。RxJava 已经内置了几个 Scheduler ,它们已经适合大多数的使用场景:

  • Schedulers.immediate(): 直接在当前线程运行,相当于不指定线程。这是默认的 Scheduler
  • Schedulers.newThread(): 总是启用新线程,并在新线程执行操作。
  • Schedulers.io(): I/O 操作(读写文件、读写数据库、网络信息交互等)所使用的 Scheduler。行为模式和 newThread() 差不多,区别在于 io() 的内部实现是是用一个无数量上限的线程池,可以重用空闲的线程,因此多数情况下 io()newThread() 更有效率。不要把计算工作放在 io() 中,可以避免创建不必要的线程。
  • Schedulers.computation(): 计算所使用的 Scheduler。这个计算指的是 CPU 密集型计算,即不会被 I/O 等操作限制性能的操作,例如图形的计算。这个 Scheduler 使用的固定的线程池,大小为 CPU 核数。不要把 I/O 操作放在 computation() 中,否则 I/O 操作的等待时间会浪费 CPU。
  • 另外, Android 还有一个专用的 AndroidSchedulers.mainThread(),它指定的操作将在 Android 主线程运行。

有了这几个 Scheduler ,就可以使用 subscribeOn()observeOn() 两个方法来对线程进行控制了。

  • subscribeOn(): 指定 subscribe() 所发生的线程,即 Observable.OnSubscribe 被激活时所处的线程。或者叫做事件产生的线程。
  • observeOn(): 指定 Subscriber 所运行在的线程。或者叫做事件消费的线程。
1
2
3
4
5
6
7
8
9
10
Observable.just(1, 2, 3, 4)
.subscribeOn(Schedulers.io()) // 指定 subscribe() 发生在 IO 线程
.observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回调发生在主线程
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer number) {
Log.d(TAG, "number: " + number);
Log.d(TAG, "Thread: " + Thread.currentThread());
}
});

4. 变换

RxJava 提供了对事件序列进行变换的支持,这是它的核心功能之一,也是大多数人说『RxJava 真是太好用了』的最大原因。所谓变换,就是将事件序列中的对象或整个序列进行加工处理,转换成不同的事件或事件序列。概念说着总是模糊难懂的,来看 API。

1) API

首先看一个 map() 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
Observable.just("images/logo.png") // 输入类型 String
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String filePath) { // 参数类型 String
return getBitmapFromPath(filePath); // 返回类型 Bitmap
}
})
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) { // 参数类型 Bitmap
showBitmap(bitmap);
}
});
  • map(): 事件对象的直接变换,具体功能上面已经介绍过。它是 RxJava 最常用的变换。 map() 的示意图:

image

  • flatMap(): 这是一个很有用但非常难理解的变换,因此我决定花多些篇幅来介绍它。 首先假设这么一种需求:假设有一个数据结构『学生』,现在需要打印出一组学生的名字。实现方式很简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Student[] students = ...;
Subscriber<String> subscriber = new Subscriber<String>() {
@Override
public void onNext(String name) {
Log.d(tag, name);
}
...
};
Observable.from(students)
.map(new Func1<Student, String>() {
@Override
public String call(Student student) {
return student.getName();
}
})
.subscribe(subscriber);

很简单。那么再假设:如果要打印出每个学生所需要修的所有课程的名称呢?(需求的区别在于,每个学生只有一个名字,但却有多个课程。)首先可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Student[] students = ...;
Subscriber<Student> subscriber = new Subscriber<Student>() {
@Override
public void onNext(Student student) {
List<Course> courses = student.getCourses();
for (int i = 0; i < courses.size(); i++) {
Course course = courses.get(i);
Log.d(tag, course.getName());
}
}
...
};
Observable.from(students)
.subscribe(subscriber);

依然很简单。那么如果我不想在 Subscriber 中使用 for 循环,而是希望 Subscriber 中直接传入单个的 Course 对象呢(这对于代码复用很重要)?用 map() 显然是不行的,因为 map() 是一对一的转化,而我现在的要求是一对多的转化。那怎么才能把一个 Student 转化成多个 Course 呢?

这个时候,就需要用 flatMap() 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Student[] students = ...;
Subscriber<Course> subscriber = new Subscriber<Course>() {
@Override
public void onNext(Course course) {
Log.d(tag, course.getName());
}
...
};
Observable.from(students)
.flatMap(new Func1<Student, Observable<Course>>() {
@Override
public Observable<Course> call(Student student) {
return Observable.from(student.getCourses());
}
})
.subscribe(subscriber);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//RxJava2
Flowable.fromIterable(students)
.flatMap(new Function<Student, Publisher<?>>() {
@Override
public Publisher<?> apply(Student student) throws Exception {
return Flowable.fromIterable(student.mSources);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
Log.d(TAG, "accept: " + ((Cource) o).name);
}
});

从上面的代码可以看出, flatMap()map() 有一个相同点:它也是把传入的参数转化之后返回另一个对象。但需要注意,和 map() 不同的是, flatMap() 中返回的是个 Observable 对象,并且这个 Observable 对象并不是被直接发送到了 Subscriber 的回调方法中。 flatMap() 的原理是这样的:

  1. 使用传入的事件对象创建一个 Observable 对象;
  2. 并不发送这个 Observable, 而是将它激活,于是它开始发送事件;
  3. 每一个创建出来的 Observable 发送的事件,都被汇入同一个 Observable ,而这个 Observable 负责将这些事件统一交给 Subscriber 的回调方法。这三个步骤,把事件拆成了两级,通过一组新创建的 Observable 将初始的对象『铺平』之后通过统一路径分发了下去。而这个『铺平』就是 flatMap() 所谓的 flat。

flatMap() 示意图:
image

5. 线程控制:Scheduler (二)

1) Scheduler 的 API (二)

前面讲到了,可以利用 subscribeOn() 结合 observeOn() 来实现线程控制,让事件的产生和消费发生在不同的线程。可是在了解了 map() flatMap() 等变换方法后,有些好事的(其实就是当初刚接触 RxJava 时的我)就问了:能不能多切换几次线程?

答案是:能。因为 observeOn() 指定的是 Subscriber 的线程,而这个 Subscriber 并不是(严格说应该为『不一定是』,但这里不妨理解为『不是』)subscribe() 参数中的 Subscriber ,而是 observeOn() 执行时的当前 Observable 所对应的 Subscriber ,即它的直接下级 Subscriber 。换句话说,observeOn() 指定的是它之后的操作所在的线程。因此如果有多次切换线程的需求,只要在每个想要切换线程的位置调用一次 observeOn() 即可。上代码:

1
2
3
4
5
6
7
8
Observable.just(1, 2, 3, 4) // IO 线程,由 subscribeOn() 指定
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
.map(mapOperator) // 新线程,由 observeOn() 指定
.observeOn(Schedulers.io())
.map(mapOperator2) // IO 线程,由 observeOn() 指定
.observeOn(AndroidSchedulers.mainThread)
.subscribe(subscriber); // Android 主线程,由 observeOn() 指定

如上,通过 observeOn() 的多次调用,程序实现了线程的多次切换。

不过,不同于 observeOn()subscribeOn() 的位置放在哪里都可以,但它是只能调用一次的。

2) Scheduler 的原理(二)

其实, subscribeOn()observeOn() 的内部实现,也是用的 lift()。具体看图(不同颜色的箭头表示不同的线程):

subscribeOn() 原理图:
image

observeOn() 原理图:
image

从图中可以看出,subscribeOn()observeOn() 都做了线程切换的工作(图中的 “schedule…” 部位)。不同的是, subscribeOn() 的线程切换发生在 OnSubscribe 中,即在它通知上一级 OnSubscribe 时,这时事件还没有开始发送,因此 subscribeOn() 的线程控制可以从事件发出的开端就造成影响;而 observeOn() 的线程切换则发生在它内建的 Subscriber 中,即发生在它即将给下一级 Subscriber 发送事件时,因此 observeOn() 控制的是它后面的线程。

最后,我用一张图来解释当多个 subscribeOn()observeOn() 混合使用时,线程调度是怎么发生的(由于图中对象较多,相对于上面的图对结构做了一些简化调整):
image

图中共有 5 处含有对事件的操作。由图中可以看出,①和②两处受第一个 subscribeOn() 影响,运行在红色线程;③和④处受第一个 observeOn() 的影响,运行在绿色线程;⑤处受第二个 onserveOn() 影响,运行在紫色线程;而第二个 subscribeOn() ,由于在通知过程中线程就被第一个 subscribeOn() 截断,因此对整个流程并没有任何影响。这里也就回答了前面的问题:当使用了多个 subscribeOn() 的时候,只有第一个 subscribeOn() 起作用。

链接

Github 源码地址:

引入依赖:

  • implementation "io.reactivex.rxjava2:rxjava:2.1.7"
  • implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

参考资料

Hala 2018

刚看了眼积分榜,习惯性的去榜首找渣团的战绩,突然回过神来,直接拉到最下面然后再一点点的往上翻,哦,原来还在前5。这可能是唯一能让我觉得欣慰的了,明年有可能还能打欧冠。

终场哨响后,深深地无力感把我压在床上不能动。从来没像现在这么绝望,哪怕是被巴萨踢了4:0的时候,哪怕前年贝秃带队的时候,因为那时候总还有赢得冠军的希望。联赛没了还有欧冠,国王杯没了还有欧冠。其实想起来,皇马现在的状况在去年就开始显现了,不过当时替补阵容比较厚加上运气的成分吧。说起来也是事后诸葛亮吧。没想到今年bbc状态这么差,替补席又不给力,还可能是四年三冠预支了未来几年的好运气导致现在这糟糕的战绩。

之前从来觉得不应该批评队员,但是看到这次国家德比第一个丢球时,还是有怪科娃的想法,后来一想,没办法,谁站在那个地方谁背锅。

感觉自己是幸运的。第一年看皇马就赢到了第十冠还有个国王杯。十二年后重新夺冠,十六郎的时代我没经历,不知道当时是怎样过来的。记得当时贴吧的背景图是“十冠皇马,欧洲之王”,随后就四大皆空。然后再欧冠卫冕,真正站在了欧洲顶峰。

刚开始看球是pptv,后来是乐视,现在连西甲都收费了。

朋友圈看到了哈工大的定位,想起了五年前我那写满哈工大的桌布和当了两年头像的哈工大校徽。

强哥是真的文艺。我服,我是伪文艺,他是真文青。“人不似少年游”,我能想到的只有“莫欺少年穷”,“少年不识愁滋味”还有坑王的“龙与少年游”。

17年的总结和18的规划一直不想写,因为觉得写了后 这年就是真的过去了。我可是最害怕时间的。

python(爬虫,数据处理,web),Android高级。

拥抱开源,享受技术。

独立思考,用心生活。

待从头收拾旧山河朝天阙。

知乎用户基本分析

知乎用户统计

昨天爬取了知乎大v “朱炫” 的关注者的用户基本信息。

image

如图所示,朱炫总共有 676697 位关注者,半小时爬取了 117913 条用户的基本信息。用户信息里每个用户只有 18 个字段的信息,都是一些很基本的信息,如图
image

通过 requests 爬取,存进 mongodb 里。下面是一些基本分析:

性别

image

12万的用户里,男性有 25819 位,女性有 31095 位,还有 60999 位没有设置性别信息。

回答

image

其中有 63109 位用户从没有回答过问题,32376 位用户回答过 5 个以内的问题,而回答数量超过 50 的只有 2536 位用户。

关注者

image

有 60749 名用户是没有被任何人关注的,47116 名用户有10个以下的关注者,只有 3349 位用户有50个以上的关注者。

机构号

在 12w 的关注者里有 29 个机构用号关注了大师兄,只选取了关注者最多的 10 个机构号来显示。

image

关注者

查取了10位关注者最多的用户。
image

有趣

用的最多的用户名

你们猜用的最多的用户名是什么?
image

好像没有一个真名==

用的最多的个人简介是这些:
image

看来知乎是学生党的天下。。

总结

爬虫使用 requests

数据库是 MongoDB

绘图用了 matplotlib.pyplotpylab

MongoDB 作为非关系型数据库的代表,不用 sql 语句,使用起来确实是方便了一些。

绘图刚学,柱状图的标签还都没有居中==

python 是世界上最好用的语言

python 是世界上最好用的语言

python 是世界上最好用的语言

Android 百度地图仿饿了么地图定位

今天遇到了一个需求,就是在用户选择地理位置的时候,现在地图上选择一个位置,然后再手动输入具体门牌号信息。和饿了么类似。

想法是 在屏幕中间放一个 ImageView 固定,然后拖动屏幕的话,只有地图在动。因为如果是用 Marker 来绘制的话,有可能会出现重影而且可能会卡。

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178

public class MapActivity extends BaseActivity implements View.OnClickListener {

private MapView mapView;
private BaiduMap baiduMap;
public static final int LOADING = 997;
public static final int FINISH = 998;
private String address;
private TextView tvAddress;
private GeoCoder geoCoder;
private TextView tvConfirm;
public LocationClient locationClient = null;
//是否首次定位
boolean isFirstLoc = true;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case LOADING:
tvAddress.setText("正在获取位置");
break;
case FINISH:
break;
}
}
};

private LocationListener myLitenner = new BDLocationListener {

@Override
public void onReceiveLocation(BDLocation bdLocation) {
//MAP VIEW 销毁后不在处理新接收的位置
if (mapView == null)
return;
MyLocationData locData = new MyLocationData.Builder()
//此处设置开发者获取到的方向信息,顺时针0-360
.accuracy(bdLocation.getRadius())
.direction(100).latitude(bdLocation.getLatitude())
.longitude(bdLocation.getLongitude()).build();
baiduMap.setMyLocationData(locData);
//设置定位数据
if (isFirstLoc) {
isFirstLoc = false;
LatLng ll = new LatLng(bdLocation.getLatitude(), bdLocation.getLongitude());
MapStatusUpdate mapStatusUpdate = MapStatusUpdateFactory.newLatLngZoom(ll, 16);
//设置地图中心点以及缩放级别
baiduMap.animateMapStatus(mapStatusUpdate);
}
}
};

private OnMapStatusChangeListener mapChangeListener = new BaiduMap.OnMapStatusChangeListener() {
@Override
public void onMapStatusChangeStart(MapStatus mapStatus) {
handler.sendEmptyMessage(LOADING);
}

@Override
public void onMapStatusChangeStart(MapStatus mapStatus, int i) {
handler.sendEmptyMessage(LOADING);
}

@Override
public void onMapStatusChange(MapStatus mapStatus) {

}

@Override
public void onMapStatusChangeFinish(MapStatus mapStatus) {
searchAddress(mapStatus.target);
}
};


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map);

initView();
}

@Override
protected void onResume() {
mapView.onResume();
super.onResume();
}

@Override
protected void onPause() {
mapView.onPause();
super.onPause();
}

private void initView() {
findViewById(R.id.btn_back).setOnClickListener(this);
tvAddress = (TextView) findViewById(R.id.tv_address);
tvAddress.setText("正在获取位置");
tvConfirm = (TextView) findViewById(R.id.tv_confirm);
tvConfirm.setOnClickListener(this);

mapView = (MapView) findViewById(R.id.map_view);
baiduMap = mapView.getMap();
baiduMap.setMapType(BaiduMap.MAP_TYPE_NORMAL);
baiduMap.setMyLocationEnabled(true);

//定位
locationClient = new LocationClient(getApplicationContext());
locationClient.registerLocationListener(myLitenner);//注册监听函数

LocationClientOption option = new LocationClientOption();
option.setOpenGps(true);//打开GPS
option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);//设置定位模式
option.setCoorType("bd09ll");//返回的定位结果是百度经纬度,默认值是gcj02
option.setScanSpan(5000);//设置发起定位请求的时间间隔为5000ms
option.setIsNeedAddress(true);//返回的定位结果包含地址信息
option.setNeedDeviceDirect(true);// 返回的定位信息包含手机的机头方向
locationClient.setLocOption(option);

baiduMap.setOnMapStatusChangeListener(mapChangeListener);
locationClient.start();
}

private OnGetGeoCoderResultListener geoCodeListener = new OnGetGeoCoderResultListener() {
@Override
public void onGetGeoCodeResult(GeoCodeResult geoCodeResult) {

}

@Override
public void onGetReverseGeoCodeResult(ReverseGeoCodeResult reverseGeoCodeResult) {

if (reverseGeoCodeResult != null) {
tvAddress.setText("当前位置:" + reverseGeoCodeResult.getAddress());
address = reverseGeoCodeResult.getAddress();
} else {
tvAddress.setText("暂无地址");
}
}
};

//根据 Laglng 来获取位置信息
private void searchAddress(LatLng latLng) {
geoCoder = GeoCoder.newInstance();
geoCoder.setOnGetGeoCodeResultListener(geoCodeListener);
ReverseGeoCodeOption codeOption = new ReverseGeoCodeOption();
codeOption.location(latLng);
geoCoder.reverseGeoCode(codeOption);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_back:
finish();
break;
case R.id.tv_confirm:
if (address != null) {
Intent intent = new Intent();
intent.putExtra("address", address);
setResult(RESULT_OK, intent);
}
finish();
break;
}
}

@Override
protected void onDestroy() {
locationClient.stop();
baiduMap.setMyLocationEnabled(false);
mapView.onDestroy();
if (geoCoder != null) {
geoCoder.destroy();
}
super.onDestroy();
}
}

布局文件

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/head_bg"
android:gravity="center_horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp">

<!-- 返回 -->

<LinearLayout
android:id="@+id/btn_back"
android:layout_width="50dp"
android:layout_height="fill_parent"
android:gravity="center">

<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/back" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回"
android:textColor="@color/white" />
</LinearLayout>
<!-- 页面标题 -->

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:layout_weight="2.5"
android:gravity="center"
android:orientation="horizontal">

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/map"
android:textColor="@color/white" />

</LinearLayout>
<!-- 小工具 -->

<LinearLayout
android:layout_width="50dp"
android:layout_height="fill_parent"
android:gravity="center">

<TextView
android:id="@+id/tv_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/border_white_jiaofeijilu"
android:paddingBottom="@dimen/dimen_3_dp"
android:paddingLeft="@dimen/dimen_5_dp"
android:paddingRight="@dimen/dimen_5_dp"
android:paddingTop="@dimen/dimen_3_dp"
android:text="确定"
android:textColor="@color/white"
android:textSize="12sp" />

</LinearLayout>
</LinearLayout>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<com.baidu.mapapi.map.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ImageView
android:layout_width="@dimen/dimen_15_dp"
android:layout_height="@dimen/dimen_15_dp"
android:layout_centerInParent="true"
android:src="@drawable/location" />

</RelativeLayout>

<TextView
android:id="@+id/tv_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:maxLines="1"
android:padding="10dp"
android:textColor="@color/text_black" />

</LinearLayout>

遇到的几个坑:

  • 百度的 sdk 文档真是垃圾,能不能用点心,好好写行不行。
  • option.setCoorType("bd09ll") 类型一定要写对 是 l 不是 1。因为这个问题,导致定位一直不太准确。
  • 开始想用 POI检索 来通过 Laglng 来获取到位置信息,但是不知道为什么一直获取不到,而且 POI检索 还需要参数 keyword 就是关键词。后来通过查阅发现使用 GeoCoderReverseGeoCodeOption 反向地理编码来获取位置信息。
  • 因为百度地图需要注册时需要用到 SHA1 ,所以每次都要打签名包,然后我并不知道AS要怎么配置才能生成 debug 签名包,很尴尬,所以每次都要生成签名包然后拷到手机上,很麻烦。

Scrapy Spiders

Spiders

Spider 类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider 就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。

对spider来说,爬取的循环类似下文:

  1. 以初始的 URL 初始化 Request,并设置回调函数。当该 response 下载完毕并返回时,将生成 response ,并作为参数传给该回调函数。spider 中初始的 request 是通过调用 start_requests() 来获取的。 start_requests() 读取 start_urls 中的 URL, 并以 parse 为回调函数生成 Request 。

  2. 在回调函数内分析返回的(网页)内容,返回 Item 对象、 dict 、Request 或者一个包含三者的可迭代容器。返回的 Request 对象之后会经过 Scrapy 处理,下载相应的内容,并调用设置的回调函数进行下一步的操作。

  3. 在回调函数内,您可以使用 选择器(Selectors) (您也可以使用 BeautifulSoup, lxml 或者您想用的任何解析器) 来分析网页内容,并根据分析的数据生成 item。

  4. 最后,由 spider 返回的 item 将被存到数据库(由某些 Item Pipeline 处理)或使用 Feed exports 存入到文件中。

该循环对任何类型的spider都(多少)适用。

scrapy.Spider

Spider 是最简单的 spider。每个其他的 spider 必须继承自该类(包括 Scrapy 自带的其他 spider 以及您自己编写的 spider )。 Spider 并没有提供什么特殊的功能。 其仅仅提供了 start_requests() 的默认实现,读取并请求 spider 属性中的 start_urls, 并根据返回的结果(resulting responses)调用 spider 的 parse 方法。

  • name

    定义 spider 名字的字符串(string)。spider 的名字定义了 Scrapy 如何定位(并初始化) spider,所以其必须是唯一的。 不过您可以生成多个相同的 spider 实例(instance),这没有任何限制。 name 是 spider 最重要的属性,而且是必须的。

    如果该 spider 爬取单个网站(single domain),一个常见的做法是以该网站(domain)(加或不加后缀)来命名 spider。 例如,如果 spider 爬取 mywebsite.com ,该 spider 通常会被命名为 mywebsite

  • allowed_domains

    可选。包含了 spider 允许爬取的域名(domain)列表(list)。 当 OffsiteMiddleware 启用时, 域名不在列表中的URL不会被跟进。

  • start_urls

    URL 列表。当没有制定特定的 URL 时,spider 将从该列表中开始进行爬取。 因此,第一个被获取到的页面的 URL 将是该列表之一。 后续的URL将会从获取到的数据中提取。就是爬虫的入口网址。

  • custom_settings

    该设置是一个 dict .当启动 spider 时,该设置将会覆盖项目级的设置. 由于设置必须在初始化(instantiation)前被更新,所以该属性 必须定义为 class 属性.

  • crawler

    该属性在初始化 class 后,由类方法 from_crawler() 设置, 并且链接了本 spider 实例对应的 Crawler 对象.

    Crawler 包含了很多项目中的组件, 作为单一的入口点 (例如插件,中间件,信号管理器等).

  • start_requests()

    该方法必须返回一个可迭代对象(iterable)。该对象包含了 spider 用于爬取的第一个 Request。

    当 spider 启动爬取并且未指定 URL 时,该方法被调用。 当指定了 URL 时,make_requests_from_url() 将被调用来创建 Request 对象。 该方法仅仅会被 Scrapy 调用一次,因此您可以将其实现为生成器。

    该方法的默认实现是使用 start_urls 中的 url 生成 Request。

    如果您想要修改最初爬取某个网站的 Request 对象,您可以重写(override)该方法。 例如,如果您需要在启动时以 POST 登录某个网站,你可以这么写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MySpider(scrapy.MySpider):
    name = 'myspider'

    def start_request(self):
    return [scrapy.FormRequest("http://www.example.com/login",
    formdata={'user': 'john', 'pass': 'secret'},
    callback=self.logged_in)]

    def logged_in(self, response):
    pass
  • make_requests_from_url(url)

    该方法接受一个 URL 并返回用于爬取的 Request 对象。 该方法在初始化 request 时被 start_requests() 调用,也被用于转化 url 为 request 。

    默认未被复写(overridden)的情况下,该方法返回的 Request 对象中, parse() 作为回调函数,dont_filter 参数也被设置为开启。

  • parse(response)

    当 response 没有指定回调函数时,该方法是 Scrapy 处理下载的 response 的默认函数。

    parse 负责处理 response 并返回处理的数据以及(/或)跟进的 URL。 Spider 对其他的 Request 的回调函数也有相同的要求。

    该方法及其他的 Request 回调函数必须返回一个包含 Request、dict 或 Item 的可迭代的对象。

  • log(message[, level, component])

    使用 scrapy.log.msg() 方法记录(log)message。 log 中自动带上该 spider 的 name 属性。

  • closed(reason)

    当 spider 关闭时,该函数被调用。 该方法提供了一个替代调用 signals.connect() 来监听 spider_closed 信号的快捷方式。

Spider arguments

Spider arguments are passed through the crawl command using the -a option. For example:

scrapy crawl myspider -a category=electronics

1
2
3
4
5
6
7
8
import scrapy

class MySpider(scrapy.Spider):
name = 'myspider'

def __init__(self, category=None, *args, **kwargs):
super(MySpider, self).__init__(*args, **kwargs)
self.start_urls = ['http://www.example.com/categories/%s' % category]

Generic Spiders

CrawlSpider

class scrapy.spiders.CrawlSpider

爬取一般网站常用的 spider 。其定义了一些规则(rule)来提供跟进 link 的方便的机制。 也许该 spider 并不是完全适合您的特定网站或项目,但其对很多情况都使用。 因此您可以以其为起点,根据需求修改部分方法。当然您也可以实现自己的 spider。

除了从 Spider 继承过来的(您必须提供的)属性外,其提供了一个新的属性:

  • rules

    一个包含一个(或多个) Rule 对象的集合(list)。 每个 Rule 对爬取网站的动作定义了特定表现。 Rule 对象在下边会介绍。 如果多个rule匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用。

该spider也提供了一个可复写(overrideable)的方法:

  • parse_start_url(response)

    当start_url的请求返回时,该方法被调用。 该方法分析最初的返回值并必须返回一个 Item 对象或者 一个 Request 对象或者 一个可迭代的包含二者对象。

爬取规则(Crawling rules)

class scrapy.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)

  • link_extractor 是一个 Link Extractor 对象。 其定义了如何从爬取到的页面提取链接。

  • callback 是一个 callable 或 string (该spider中同名的函数将会被调用)。 从 link_extractor 中每获取到链接时将会调用该函数。该回调函数接受一个 response 作为其第一个参数, 并返回一个包含 Item 以及(或) Request 对象(或者这两者的子类)的列表(list)。

  • cb_kwargs 包含传递给回调函数的参数(keyword argument)的字典。

  • follow 是一个布尔(boolean)值,指定了根据该规则从 response 提取的链接是否需要跟进。 如果 callback 为None, follow 默认设置为 True ,否则默认为 False 。

  • process_links 是一个 callable 或 string (该spider中同名的函数将会被调用)。 从 link_extractor 中获取到链接列表时将会调用该函数。该方法主要用来过滤。

  • process_request 是一个 callable 或 string (该spider中同名的函数将会被调用)。 该规则提取到每个 request 时都会调用该函数。该函数必须返回一个request或者None。 (用来过滤request)

CrawlSpider样例

接下来给出配合rule使用CrawlSpider的例子:

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
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
name = 'myspider'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com']

rules = [
# 提取匹配 'category.php' (但不匹配 'subsection.php') 的链接并跟进链接(没有callback意味着follow默认为True)
Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

# 提取匹配 'item.php' 的链接并使用spider的parse_item方法进行分析
Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
]

def parse_item(self, response):
self.logger.info('Hi, this is an item page! %s', response.url)

item = scrapy.Item()
item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
item['name'] = response.xpath('//td[@id="item_name"]/text()').extract()
item['description'] = response.xpath('//td[@id="item_description"]/text()').extract()
return item

该 spider 将从 example.com 的首页开始爬取,获取 category 以及 item 的链接并对后者使用 parse_item 方法。 当 item 获得返回(response)时,将使用 XPath 处理 HTML 并生成一些数据填入 Item 中。

XMLFeedSpider

class scrapy.spiders.XMLFeedSpider

XMLFeedSpider 被设计用于通过迭代各个节点来分析 XML 源(XML feed)。 迭代器可以从 iternodes , xml , html 选择。 鉴于 xml 以及 html 迭代器需要先读取所有DOM再分析而引起的性能问题, 一般还是推荐使用 iternodes 。 不过使用 html 作为迭代器能有效应对错误的 XML。

您必须定义下列类属性来设置迭代器以及标签名(tag name):

  • iterator

    用于确定使用哪个迭代器的string。可选项有:

    • iternodes - 一个高性能的基于正则表达式的迭代器
    • html - 使用 Selector 的迭代器。 需要注意的是该迭代器使用DOM进行分析,其需要将所有的DOM载入内存, 当数据量大的时候会产生问题。
    • xml - 使用 Selector 的迭代器。 需要注意的是该迭代器使用DOM进行分析,其需要将所有的DOM载入内存, 当数据量大的时候会产生问题。

    默认值为 iternodes

  • itertag

    一个包含开始迭代的节点名的string。例如:
    itertag = 'product'

  • namespaces

    一个由 (prefix, url) 元组(tuple)所组成的 list。 其定义了在该文档中会被 spider 处理的可用的 namespace 。 prefix 及 uri 会被自动调用 register_namespace() 生成 namespace。

    您可以通过在 itertag 属性中制定节点的 namespace。

    例如:

    1
    2
    3
    4
    5
    class YourSpider(XMLFeedSpider):

    namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
    itertag = 'n:url'
    # ...

除了这些新的属性之外,该spider也有以下可以覆盖(overrideable)的方法:

  • adapt_response(response)

    该方法在 spider 分析 response 前被调用。您可以在 response 被分析之前使用该函数来修改内容(body)。 该方法接受一个 response 并返回一个 response (可以相同也可以不同)。

  • parse_node(response, selector)

    当节点符合提供的标签名时(itertag)该方法被调用。 接收到的 response 以及相应的 Selector 作为参数传递给该方法。 该方法返回一个 Item 对象或者 Request 对象 或者一个包含二者的可迭代对象(iterable)。

  • process_results(response, results)

    当 spider 返回结果(item或request)时该方法被调用。 设定该方法的目的是在结果返回给框架核心(framework core)之前做最后的处理, 例如设定 item 的 ID 。其接受一个结果的列表(list of results)及对应的 response。 其结果必须返回一个结果的列表(list of results)(包含Item或者Request对象)。

XMLFeedSpider例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy.spiders import XmlFeedSpider
from myproject.items import TestItem

class MySpider(XmlFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.xml']
iterator = 'iternodes' # This is actually unnecessary, since it's the default value
itertag = 'item'

def parse_node(self, response, node):
self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.extract()))

item = TestItem()
item['id'] = node.xpath('@id').extract()
item['name'] = node.xpath('name').extract()
item['description'] = node.xpath('description').extract()
return item

简单来说,我们在这里创建了一个 spider ,从给定的 start_urls 中下载 feed , 并迭代 feed 中每个 item 标签,输出,并在 Item 中存储有些随机数据。

CSVFeedSpider

class scrapy.spiders.CSVFeedSpider

该 spider 除了其按行遍历而不是节点之外其他和 XMLFeedSpider 十分类似。 而其在每次迭代时调用的是 parse_row() 。

  • delimiter
    在CSV文件中用于区分字段的分隔符。类型为string。 默认为 ‘,’ (逗号)。

  • quotechar
    A string with the enclosure character for each field in the CSV file Defaults to ‘“‘ (quotation mark).

  • headers
    在CSV文件中包含的用来提取字段的行的列表。参考下边的例子。

  • parse_row(response, row)
    该方法接收一个 response 对象及一个以提供或检测出来的 header 为键的字典(代表每行)。 该 spider 中,您也可以覆盖 adapt_response 及 process_results 方法来进行预处理(pre-processing)及后(post-processing)处理。

CSVFeedSpider例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from scrapy.spiders import CSVFeedSpider
from myproject.items import TestItem

class MySpider(CSVFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.csv']
delimiter = ';'
quotechar = "'"
headers = ['id', 'name', 'description']

def parse_row(self, response, row):
self.logger.info('Hi, this is a row!: %r', row)

item = TestItem()
item['id'] = row['id']
item['name'] = row['name']
item['description'] = row['description']
return item

SitemapSpider

class scrapy.spiders.SitemapSpider

SitemapSpider 使您爬取网站时可以通过 Sitemaps 来发现爬取的URL。

其支持嵌套的 sitemap,并能从 robots.txt 中获取 sitemap 的url。

  • sitemap_urls
    包含您要爬取的 url 的 sitemap 的 url 列表(list)。 您也可以指定为一个 robots.txt ,spider 会从中分析并提取url。

  • sitemap_rules
    一个包含 (regex, callback) 元组的列表(list):

    • regex 是一个用于匹配从sitemap提供的url的正则表达式。 regex 可以是一个字符串或者编译的正则对象(compiled regex object)。

    • callback指定了匹配正则表达式的url的处理函数。 callback 可以是一个字符串(spider中方法的名字)或者是callable。

    例如:sitemap_rules = [('/product/', 'parse_product')]

    规则按顺序进行匹配,之后第一个匹配才会被应用。

    如果您忽略该属性,sitemap中发现的所有url将会被 parse 函数处理。

  • sitemap_follow

    一个用于匹配要跟进的sitemap的正则表达式的列表(list)。其仅仅被应用在使用 Sitemap index files 来指向其他 sitemap文件的站点。

    默认情况下所有的 sitemap 都会被跟进。

  • sitemap_alternate_links

    指定当一个 url 有可选的链接时,是否跟进。 有些非英文网站会在一个 url 块内提供其他语言的网站链接。

    例如:

    1
    2
    3
    4
    <url>
    <loc>http://example.com/</loc>
    <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/>
    </url>

    sitemap_alternate_links 设置时,两个 URL 都会被获取。 当 sitemap_alternate_links 关闭时,只有 http://example.com/ 会被获取。

    默认 sitemap_alternate_links 关闭。

  • SitemapSpider样例

    简单的例子: 使用 parse 处理通过sitemap发现的所有url:

    1
    2
    3
    4
    5
    6
    7
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']

    def parse(self, response):
    pass # ... scrape item here ...

    用特定的函数处理某些url,其他的使用另外的callback:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']
    sitemap_rules = [
    ('/product/', 'parse_product'),
    ('/category/', 'parse_category'),
    ]

    def parse_product(self, response):
    pass # ... scrape product ...

    def parse_category(self, response):
    pass # ... scrape category ...

    跟进 robots.txt 文件定义的 sitemap 并只跟进包含有 ..sitemap_shop 的 url:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
    ('/shop/', 'parse_shop'),
    ]
    sitemap_follow = ['/sitemap_shops']

    def parse_shop(self, response):
    pass # ... scrape shop here ...

    在SitemapSpider中使用其他url:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
    ('/shop/', 'parse_shop'),
    ]

    other_urls = ['http://www.example.com/about']

    def start_requests(self):
    requests = list(super(MySpider, self).start_requests())
    requests += [scrapy.Request(x, self.parse_other) for x in self.other_urls]
    return requests

    def parse_shop(self, response):
    pass # ... scrape shop here ...

    def parse_other(self, response):
    pass # ... scrape other here ...

Scrapy命令行

新建项目

scrapy startproject Demo

该命令会在当前目录下建立一个名为 Demo 的 scrapy 项目

控制项目

cd Demo 进入到项目目录中

scrapy genspider changoal changoal.cn
创建一个新的 spider,该命令会在 spiders 文件夹下新建一个叫 changoal.py 的文件。文件里有以下内容:

1
2
3
4
5
6
7
8
9
import scrapy

class ChangoalSpider(scrapy.Spider):
name = "changoal"
allowed_domains = ["changoal.cn"]
start_urls = ['http://changoal.cn/']

def parse(self, response):
pass

可用的工具命令

scrapy -h 可以查看所有可用的命令。

Scrapy提供了两种类型的命令。一种必须在Scrapy项目中运行(针对项目(Project-specific)的命令),另外一种则不需要(全局命令)。全局命令在项目中运行时的表现可能会与在非项目中运行有些许差别(因为可能会使用项目的设定)。

全局命令:

  • startproject
  • settings
  • runspider
  • shell
  • fetch
  • view
  • version

项目(Project-only)命令:

  • crawl
  • check
  • list
  • edit
  • parse
  • genspider
  • bench

startproject

  • 语法: scrapy startproject <project_name>
  • 是否需要项目: no

project_name 文件夹下创建一个名为 myproject 的 Scrapy 项目。
scrapy startproject myproject

genspider

  • 语法: scrapy genspider [-t template] <name> <domain>
  • 是否需要项目: yes

在当前项目中创建spider。

这仅仅是创建spider的一种快捷方法。该方法可以使用提前定义好的模板来生成spider。您也可以自己创建spider的源码文件。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ scrapy genspider -l
Available templates:
basic
crawl
csvfeed
xmlfeed

$ scrapy genspider -d basic
import scrapy

class $classname(scrapy.Spider):
name = "$name"
allowed_domains = ["$domain"]
start_urls = (
'http://www.$domain/',
)

def parse(self, response):
pass

$ scrapy genspider -t basic example example.com
Created spider 'example' using template 'basic' in module:
mybot.spiders.example

crawl

  • 语法:scrapy crawl <spider>
  • 是否需要项目: yes

使用spider进行爬取。
scrapy crawl myspider

check

  • 语法: scrapy check [-l] <spider>
  • 是否需要项目: yes

运行contract检查。(不懂有什么用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ scrapy check -l
first_spider
* parse
* parse_item
second_spider
* parse
* parse_item

$ scrapy check
[FAILED] first_spider:parse_item
>>> 'RetailPricex' field is missing

[FAILED] first_spider:parse
>>> Returned 92 requests, expected 0..4

list

  • 语法: scrapy list
  • 是否需要项目: yes

列出当前项目中所有可用的spider。每行输出一个spider。

edit

  • 语法: scrapy edit <spider>
  • 是否需要项目: yes
    使用 EDITOR 中设定的编辑器编辑给定的spider

该命令仅仅是提供一个快捷方式。开发者可以自由选择其他工具或者IDE来编写调试spider。

fetch

  • 语法: scrapy fetch <url>
  • 是否需要项目: no

使用Scrapy下载器(downloader)下载给定的URL,并将获取到的内容送到标准输出。

该命令以spider下载页面的方式获取页面。例如,如果spider有 USER_AGENT 属性修改了 User Agent,该命令将会使用该属性。

因此,您可以使用该命令来查看spider如何获取某个特定页面。

该命令如果非项目中运行则会使用默认Scrapy downloader设定。

scrapy fetch http://www.changoal.cn

view

  • 语法: scrapy view <url>
  • 是否需要项目: no

在浏览器中打开给定的URL,并以Scrapy spider获取到的形式展现。 有些时候spider获取到的页面和普通用户看到的并不相同。因此该命令可以用来检查spider所获取到的页面,并确认这是您所期望的。

scrapy view http://www.changoal.cn

shell

  • 语法: scrapy shell [url]
  • 是否需要项目: no

以给定的URL(如果给出)或者空(没有给出URL)启动Scrapy shell。

scrapy shell http://www.changoal.cn

parse

  • 语法: scrapy parse <url> [options]
  • 是否需要项目: yes

获取给定的URL并使用相应的 spider 分析处理。如果您提供 --callback 选项,则使用 spider 的该方法处理,否则使用 parse

支持的选项:

  • --spider=SPIDER: 跳过自动检测spider并强制使用特定的spider
  • --a NAME=VALUE: 设置spider的参数(可能被重复)
  • --callback or -c: spider中用于解析返回(response)的回调函数
  • --pipelines: 在pipeline中处理item
  • --rules or -r: 使用 CrawlSpider 规则来发现用来解析返回(response)的回调函数
  • --noitems: 不显示爬取到的item
  • --nolinks: 不显示提取到的链接
  • --nocolour: 避免使用pygments对输出着色
  • --depth or -d: 指定跟进链接请求的层次数(默认: 1)
  • --verbose or -v: 显示每个请求的详细信息

settings

  • 语法: scrapy settings [options]
  • 是否需要项目: no

获取Scrapy的设定。

在项目中运行时,该命令将会输出项目的设定值,否则输出Scrapy默认设定。

1
2
3
4
$ scrapy settings --get BOT_NAME
scrapybot
$ scrapy settings --get DOWNLOAD_DELAY
0

runspider

  • 语法: scrapy runspider <spider_file.py>
  • 是否需要项目: no

在未创建项目的情况下,运行一个编写在Python文件中的spider。

scrapy runspider myspider.py

version

  • 语法: scrapy version [-v]
  • 是否需要项目: no

输出Scrapy版本。配合 -v 运行时,该命令同时输出Python, Twisted以及平台的信息,方便bug提交。

自定义项目命令

您也可以通过 COMMANDS_MODULE 来添加您自己的项目命令。您可以以 scrapy/commandsScrapy commands 为例来了解如何实现您的命令。

Recyclerview加载更多

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</android.support.v4.widget.SwipeRefreshLayout>

普通 item_with_image.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">

<ImageView
android:id="@+id/img_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@mipmap/ic_launcher"/>

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="@color/text_black"
android:textSize="15sp"
tools:text="seyo+"/>

</LinearLayout>

底部 item_foot.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:orientation="horizontal">

<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginRight="6dp"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/loading"/>

</LinearLayout>

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
public class TestAdapter extends RecyclerView.Adapter {
private Context mContext;
private List<DynamicModel> mVideoList;
private int TYPE_IMAGE = 0;
private int TYPE_FOOTER = 2;

public TestAdapter(Context context, List<DynamicModel> videoList) {
mContext = context;
mVideoList = videoList;
}

@Override
public int getItemViewType(int position) {
if (position + 1 == getItemCount()) {
return TYPE_FOOTER;
}
return TYPE_IMAGE;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

RecyclerView.ViewHolder holder = null;

if (viewType == TYPE_IMAGE) {
View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_with_image, parent, false);
holder = new ImageHolder(itemView);
} else if (viewType == TYPE_FOOTER) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_foot, parent,
false);
return new FootViewHolder(view);
}

return holder;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof ImageHolder) {
((ImageHolder) holder).bindView(mVideoList.get(position));
}
}


@Override
public int getItemCount() {
return mVideoList.size() == 0 ? 0 : mVideoList.size() + 1;
}

private class ImageHolder extends RecyclerView.ViewHolder {

private final ImageView imgAvatar;
private final TextView tvName;

public ImageHolder(View itemView) {
super(itemView);
imgAvatar = ((ImageView) itemView.findViewById(R.id.img_avatar));
tvName = ((TextView) itemView.findViewById(R.id.tv_name));
}

private void bindView(DynamicModel item) {

if (item.getUsers().size() > 0) {
Glide.with(mContext).load(AppConfig.GetImageUrl(item.getUsers().get(0).getAvatarImage())).transform(new GlideCircleTransform(mContext)).into(imgAvatar);
tvName.setText(item.getUsers().get(0).getUserName());
} else {
Glide.with(mContext).load(R.drawable.default_seyo).transform(new GlideCircleTransform(mContext)).into(imgAvatar);
tvName.setText("seyo+");
}
}
}


private class FootViewHolder extends RecyclerView.ViewHolder {

public FootViewHolder(View itemView) {
super(itemView);
}
}
}

Fragment

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
public void initView(
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(layoutManager);
adapter = new VideoAdapter(getActivity(), mList);
mRecyclerView.setAdapter(adapter);
mRefreshLayout = ((SwipeRefreshLayout) view.findViewById(R.id.view_refresh));
mRefreshLayout.setOnRefreshListener(this);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState){
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItemPosition + 1 == adapter.getItemCount()) {
boolean isRefreshing = mRefreshLayout.isRefreshing();
if (isRefreshing) {
adapter.notifyItemRemoved(adapter.getItemCount());
return;
}
if (!isLoading) {
isLoading = true;
loadData();
}
}
}

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
}
});
)

@Override
public void onRefresh() {
mList.clear();
loadData();
}

private void loadData() {

NetService service = new NetService();
service.getDynamicList(0, AppConfig.TYPE_RECOMMEND, AppConfig.PAGESIZE,curIndex, new Observer<DetailList<DynamicModel>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
MethodUtils.LoadingDialog(getContext(), "正在加载");
}

@Override
public void onNext(@NonNull DetailList<DynamicModel> list) {
mList.addAll(list.getData());
adapter.notifyDataSetChanged();
}

@Override
public void onError(@NonNull Throwable e) {
MethodUtils.loadingDialogDismiss();
ToastUtils.showMessage(getContext(), "网络异常,请求失败");
mRefreshLayout.setRefreshing(false);
isLoading = false;
if (adapter.getItemCount() > 0) {
adapter.notifyItemRemoved(adapter.getItemCount());
}
}

@Override
public void onComplete() {
mRefreshLayout.setRefreshing(false);
isLoading = false;
MethodUtils.loadingDialogDismiss();
if (adapter.getItemCount() > 0) {
adapter.notifyItemRemoved(adapter.getItemCount());
}
}
});
}

挺简单的,主要就是多布局,当滑动到最后一个 item 时,让他绑定 item_foot.xml 布局。然后在加载导数据之后 adapter.notifyItemRemoved(adapter.getItemCount()); 去掉最后 foot。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×

keyboard_arrow_up 回到顶端