袁来的code空间


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

利用AlarmManager完成精准的轮询

发表于 2018-06-07 | 分类于 AlarmManager | 阅读次数: ℃

问题分析

想起轮询我们一般会想起利用Handler和Timer,然而AlarmManager相比于Handler和Timer有优势,具体的分析我参考了一个大神的博客:

最近在做一个需求:客户端按照规定的时间间隔向服务端发送定位。一看到这个需求就想到了使用 AlarmManager 来实现。 AlarmManager 经常被用来执行定时任务,比如设置闹铃、发送心跳包等。也许有人会有疑问:为什么不能使用相同具有定时效果的 Timer 和 Handler 呢?

其实答案非常简单,相对于 Handler 来说,使用 sendEmptyMessageDelayed 方法是依赖于 Handler 所在的线程的,如果线程结束,就起不到定时任务的效果;而 AlarmManager 依赖的是 Android 系统的服务,具备唤醒机制。比起 Handler 也就更合适了。

而至于 Timer 可以精确地做到定时操作,但是相比于 AlarmManager 而言还是差了一截。同理,如果手机关屏后长时间不使用, CPU 就会进入休眠模式。这个使用如果使用 Timer 来执行定时任务就会失败,因为 Timer 无法唤醒 CPU 。

所以,综上所述,AlarmManager 就成为了最佳选择。

综上可知,AlarmManager不仅仅只是用了完成闹钟操作,还可以用来完成一些定时任务,但是因为系统的不断升级和优化,出于省电的考虑,在Android KITKAT版本及之后,setRepeating开始变得不准确,想要精准的定时我们只能使用setExact,在Android M及之后的版本,我们只能使用setExactAndAllowWhileIdle来应对Android M出现的Doze模式,具体的Android M版本的变化可以参考谷歌的官网Android 6.0 变更。随意为了覆盖所有的版本,我们必须考虑在不同的Android版本来实现定时设置,接下来我们来试着实现它。

实现精准轮询的原理

轮询一般意味着我们需要Service来完成我们的一些具体的请求,这里我想让AlarmManager来定时唤醒一个Receiver用来启动请求的Service,这里比较困难的是,Android KITKAT及之后的版本设置的定时都只能管一次,所以我们需要做到的是完成一次定时任务以后接着设置第二次定时任务,一次类推……
所以我们梳理一下这里我们要靠什么完成后台的精准轮询,我们需要一个Service来完成我们真正想做的事情,比如说请求;我们需要一个Recever来启动Service;我们还需要一个工具类来专门设置定时(当然你不想这样子也可以),我们设置的定时是定时发送一个广播。

实现精准轮询的步骤

构造一个Service类(PollingService),在该类里面实现具体要实现的事情,具体的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PollingService extends IntentService {

private static final String TAG = "PollingService";

@Override
protected void onHandleIntent(@Nullable Intent intent) {

// 做你想做的事情,比如说发送请求

}

public static Intent newIntent(Context context){

Intent intent = new Intent(context, PollingService.class);
return intent;

}

public PollingService() {
super(TAG);
}

}

构造一个Receiver类(PollingReceiver)类来接收AlarmManager向该receiver发送的广播,接收到广播以后我们就可以来启动指定service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PollingReceiver extends BroadcastReceiver {

public static String TAG = "PollingReceiver";

@Override
public void onReceive(Context context, Intent intent) {

Log.d(TAG, "调用了onReceive");
PollingUtils.startExactAgain(context, 60, PollingReceiver.class, PollingUtils.ACTION);
Intent i = PollingService.newIntent(context);
context.startService(i);

}

}

别忘了在Manifest里面注册之前构造的Receiver和Service,接着是我们打包的一个AlarmManager的工具类,在这里我们需要注意是使用setExactAndAllowWhileIdle和setExact方法的第二个参数的时间都是基于ELAPSED_REALTIME,所以第二个参数都应该是SystemClock.elapsedRealtime()+间隔时间,具体的代码如下:

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
public class PollingUtils {

public static String ACTION = "com.example.yuanyuanlai.servicetest.pollingutils";

public static void startPollingService(Context context, int seconds, Class<?> cls, String action){

AlarmManager alarmManager = (AlarmManager)context
.getSystemService(Context.ALARM_SERVICE);

Intent intent = new Intent(context, cls);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

//触发服务的起始时间
long triggerTime = SystemClock.elapsedRealtime();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime+seconds*1000, pendingIntent);
}else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime+seconds*1000, pendingIntent);
}else {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, seconds*1000, pendingIntent);
}

}

public static void startExactAgain(Context context, int seconds, Class<?> cls, String action){

AlarmManager alarmManager = (AlarmManager)context
.getSystemService(Context.ALARM_SERVICE);

Intent intent = new Intent(context, cls);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

//触发服务的起始时间
long triggerTime = SystemClock.elapsedRealtime();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime+seconds*1000, pendingIntent);
}else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime+seconds*1000, pendingIntent);
}

}

public static void stopPollingService(Context context, Class<?> cls, String action){

AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);

Intent intent = new Intent(context, cls);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.cancel(pendingIntent);

}

}

启动定时和停止定时的代码具体如下:

1
PollingUtils.startPollingService(MainActivity.this, 60, PollingReceiver.class, PollingUtils.ACTION);
1
PollingUtils.stopPollingService(MainActivity.this, PollingReceiver.class, PollingUtils.ACTION);

实现效果

SSR局域网共享(Mac)

发表于 2018-06-02 | 分类于 科学上网 | 阅读次数: ℃

前几天自己的Pixel申请升级成P,发现很多软件出现不兼容的问题,比如说Tim的QQ空间和邮件功能,支付宝的共享单车功能会直接闪退,正常使用一些软件,比如说在Tim里面查看文件和用WPS浏览文件时会出现OOM(内存不足)…… 这能忍吗?果断降级,问题来了,退回到OREO(Android 8.0)的时候会出现一开始要先连接Google浏览器(需要翻墙😭),这不是坑爹吗?手机还没开就要我翻墙,在网上查看了相关的教程,发现了一些不适用的问题,最后在室友的提醒下解决了问题。

解决思路

其实这个问题也不是不能解决,有几个思路,其中刷系统我是不想考虑的,因为我用的Pixel是买的美版带锁的,所以刷机会有变砖的风险,以前用小米的时候遇见过手机变砖,自己没能解决~~另外的就是想办法在手机没有打开的情况下翻墙,一个是靠路由器翻墙,另一个是让我的手机共享我电脑的端口,也就说我的手机发送一些请求都会先通过电脑的端口,我选择第二种。

具体操作

这里我要再强调一遍啊,我翻墙用的是SSR不是SS,网上很多的教程都是基于SS的,所以我之前试了很多的教程都不对,就是因为那些都是基于SS的。
我用的SSR图标
其实那些教程里面大多数都推荐使用privoxy,我以前不知道它是干什么用的,于是在Wiki里面查了一下,一下是查询的内容:

Privoxy是一款不进行网页缓存且自带过滤功能的代理服务器,针对HTTP、HTTPS协议。通过其过滤功能,用户可以保护隐私、对网页内容进行过滤、管理Cookie,以及拦阻各种广告等。Privoxy可以单机使用,也可以应用到多用户的网络。==它也可以与其他代理相连(通常与Squid一起使用),更可以突破互联网审查。==

🤣打黄线的部分可以看出Privoxy就是来设置代理和其它设备相连的。
步骤很简单,总结起来就是三步:

  1. 查看本电脑的ip地址,mac下面打开命令行,在命令行里面敲ifconfig,会出现一大串文字,我们需要找一段字,具体在下面画红线的地方:
  2. 接着我们打开shadowsocks,在菜单页里面找HTTP代理设置,打开以后设置监听的地址和端口,注意端口不能起常用的端口名,要不然会占用端口的。

    接着在HTTP代理监听地址里面填写本电脑ip地址,HTTP代理监听端口随便填,这样就会监听这个ip地址下的这个端口(也就是说我的手机请求会被监听)
  3. 接着在手机上面连接你电脑所处的局域网,先把代理模式调成手动模式,在高级设置里面设置代理服务器主机名和代理服务器端口,具体如图所示:

    更多的用法

    默认情况下我们电脑的终端是没有翻墙的,如果你想翻墙,你需要在终端里面设置
    我的终端用的是zsh,所以我们需要编辑.zshrc,在里面加
    export ALL_PROXY=socks5://XXXX.XXXX.XXXX.XXXX:XXXX(你的ip地址加端口名),最后source .zshrc使文件生效。这里的不是用的HTTP代理而是socks5代理,需要在shadowsocks的高级设置里面查看socks5的监听地址和监听端口,具体的内容如图:

Android 滑动解锁页

发表于 2018-05-26 | 分类于 自定义View | 阅读次数: ℃

最近的一个项目里面需要使用锁屏页,实现的方法是用Activity来伪装锁屏页,网上对这个方面的教程不多,大多数都是基于这篇文章,但是这篇文章有些细节点没有关注到,所以我在这里进行一些完善和补充,并把整个完整的过程写出来,供大家参考。

我们怎么做到锁屏的时候显示Activity

首先我们在这里创建一个Activity,名字叫作LockScreenActivity,然后我们来着手解决如何知道屏幕锁屏,思路可以看下面的流程图(这里借鉴QQ音乐的资料图)😁。

通过上图的流程可知屏幕关闭会发出一个叫作SCREEN_OFF的系统广播,然后在Service里面去监听这个系统广播,一旦App启动,就启动这个Service,然后一旦系统发出SCREEN_OFF的广播,Receiver就会接受到。
接着,我们按上面得出来的步骤,来完成程序的设计,首先new一个Service,这个Service主要用来注册一个广播来监听SCREEN_OFF这一系统广播,但在此之前我们还得创建一个广播的接受器(LockScreenReceiver),其具体代码如下:

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

@Override
public void onReceive(Context context, Intent intent) {

String action = intent.getAction();

if (Intent.ACTION_SCREEN_OFF.equals(action)) {
Intent mLockIntent = new Intent(context, LockScreenActivity.class);
mLockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(mLockIntent);
}

}

}

接着我们来创建用来注册监听广播的Service(LockService),其具体代码如下:

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
public class LockService extends Service {

private LockScreenReceiver lockScreenReceiver;

@Override
public void onCreate() {

lockScreenReceiver = new LockScreenReceiver();
IntentFilter mScreenOffFilte = new IntentFilter();
mScreenOffFilte.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(lockScreenReceiver,mScreenOffFilte);

}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(lockScreenReceiver);
}
}

当然注意了,在Service被Destroy的时候别忘了调用unregisterReceiver来解除注册的广播。
我们只用在App启动的时候来启动这个服务,具体的代码如下:

1
2
Intent intent = new Intent(this,LockService.class);
startService(intent);

当然上面的这一些还不够,我们要加一个权限,加上使锁屏失效(当然如果你设置了密码是不能失效的)。

1
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>

同时你还得在Activity里面设置一大堆属性:

1
2
3
4
5
6
7
8
9
10
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE);

这上面的属性分别设置使布局锁屏的时候出现,去掉锁屏,全屏,去掉导航栏和状态栏等等,具体一些属性的效果有些我也说不清楚,大家有兴趣的可以自行Google一下。

实现锁屏的滑动效果

这里我们定义一个UnderView来处理滑动逻辑。在这里盗一张QQ音乐团队的图,我觉得它的图解释的很清楚。

这里把视图分为MoveView和UnderView,废话不多说,上完整的代码:

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
public class UnderView extends View {

private float mStartX;
private View mMoveView;
private int mWidth;
private OnSlideFinishListener mOnSlideFinishListener;

public UnderView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mWidth = ScreenUtils.getScreenWidth(context);

}

public void setSlideListener(OnSlideFinishListener onSlideFinishListener){
mOnSlideFinishListener = onSlideFinishListener;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
final float nx = event.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
mStartX = nx;
onAnimationEnd();
case MotionEvent.ACTION_MOVE:
handleMoveView(nx);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
doTriggerEvent(nx);
break;
}
return true;
}

private void handleMoveView(float x) {
float movex = x - mStartX;
if (movex < 0)
movex = 0;
mMoveView.setTranslationX(movex);

float mWidthFloat = (float) mWidth;//屏幕显示宽度
if(getBackground()!=null){
getBackground().setAlpha((int) ((mWidthFloat - mMoveView.getTranslationX()) / mWidthFloat * 200));//初始透明度的值为200
}
}

private void doTriggerEvent(float x) {
float movex = x - mStartX;
if (movex > (mWidth * 0.4)) {
moveMoveView(mWidth-mMoveView.getLeft(),true);//自动移动到屏幕右边界之外,并finish掉

} else {
moveMoveView(-mMoveView.getLeft(),false);//自动移动回初始位置,重新覆盖
}
}

private void moveMoveView(float to,boolean exit){

ObjectAnimator animator = ObjectAnimator.ofFloat(mMoveView, "translationX", to);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if(getBackground()!=null){
getBackground().setAlpha((int) (((float) mWidth - mMoveView.getTranslationX()) / (float) mWidth * 200));
}
}
});//随移动动画更新背景透明度
animator.setDuration(250).start();

if(exit){
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mOnSlideFinishListener.onSlideFinish();
}
});
}//监听动画结束,利用Handler通知Activity退出
}

public void setMoveView(View moveView){
mMoveView = moveView;
}

public interface OnSlideFinishListener{
void onSlideFinish();
}
}

接下来我们来分析一下这个自定义View,在handleMoveView里面处理当手在屏幕上面滑动时mMoveView随着手指滑动,doTriggerEvent是处理松开手指时mMoveView的反应,当滑动的距离不超过屏幕的40%时,mMoveView返回到原处,超过了就直接划过去。
当然重点是要在UnderView里面获取mMoveView和设置OnSlideFinishListener来处理当锁屏划开的时候finish掉当前的Activity。

细节处理

完成了上面的两步,大家可以运行一下程序,这时候会出现两个问题,滑动时,滑动过的背景不是透明的,如果你没退出Activity又反复打开屏幕关闭屏幕,会有多个LockScreenActivity在Activity栈里面。
首先第二个问题比较好解决,直接在Manifest里面指定LockScreenActivity的launchMode为singleInstance,即单例模式。这样栈里面就只会有一个LockScreenActivity存在。
第二个问题就需要设置Activity的背景为透明色,接着在Activity的theme,我们需要在style文件里为LockScreenActivity指定一个theme,具体的代码如下:

1
2
3
4
5
6
7
8
9
<style name="LockScreenTheme" parent="AppTheme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowContentOverlay">@null</item>
</style>

这里为止,我们就可以实现一个完整的锁屏页了(^-^)。

用RxJava完成轮询请求

发表于 2018-05-23 | 分类于 Retrofit , RxJava | 阅读次数: ℃

最近做一个软件,需要后台服务轮询请求数据,这里我是用RxJava来实现轮询。这里我用金山词霸的接口来做测试。

一般的做法

为什么说这是一般的做法,是因为我们平时一般就是使用嵌套的形式,就是外面是一个轮询,内部嵌套一个网络请求。其中interval是RxJava的一个操作符,我们用interval举一个例子,具体的代码如下:

1
2
3
4
5
6
7
8
Observable
.interval(2,1,TimeUnit.SECONDS)
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
Log.d("InternetService","---->"+aLong);
}
});

输出的效果如下:

接着我的网络请求如下,我网络请求用的是Retrofit和RxJava来请求,具体的代码如下:

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
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://fy.iciba.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();

GetRequest_Interface request = retrofit.create(GetRequest_Interface.class);

Observable<Translation> observable = request.getCall();

observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Translation>() {
@Override
public void onSubscribe(Disposable d) {
}

@Override
public void onNext(Translation result) {
result.show() ;
}

@Override
public void onError(Throwable e) {
Log.d(TAG, "请求失败");
}

@Override
public void onComplete() {

}
});

通过上面的代码我们需要注意的是我们需要通过addCallAdapterFactory()添加RxJava2CallAdapterFactory,这样就可以让RxJava可以直接跟Retrofit搭配使用,Retrofit的初始化我在之前的文章里面谈过,其中subscribeOn() 指定的是上游发送事件的线程, observeOn() 指定的是下游接收事件的线程,RxJava为我们内置了很多线程选项,具体的选项如下:

  • Schedulers.io() 代表io操作的线程, 通常用于网络,读写文件等io密集型的操作
  • Schedulers.computation() 代表CPU计算密集型的操作, 例如需要大量计算的操作
  • Schedulers.newThread() 代表一个常规的新线程
  • AndroidSchedulers.mainThread() 代表Android的主线程

这些内置的Scheduler已经足够满足我们开发的需求, 因此我们应该使用内置的这些选项, 在RxJava内部使用的是线程池来维护这些线程, 所有效率也比较高.

链式做法

RxJava最大的好处就是逻辑清楚,上面的例子是通过嵌套来实现轮询请求的,所以我们可以借助RxJava的操作符来完成上面同样的效果,这里我们用flatMap这个操作符把long类型转换成我们想要的Observable<T>类型,具体的代码如下:

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
Observable
.interval(2,5, TimeUnit.SECONDS)
.doOnNext(new Consumer<Long>() {
@Override
public void accept(Long integer) throws Exception {
Log.d("InternetService", "第 " + integer + " 次轮询" );


}
})
.subscribeOn(Schedulers.io())
.flatMap(new Function<Long, ObservableSource<Translation>>() {
@Override
public ObservableSource<Translation> apply(Long aLong) throws Exception {
return request.getCall();
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Translation>() {
@Override
public void accept(Translation translation) throws Exception {

Log.d("InternetService",translation.getContent().getOut());

}
});

通过上面可以看到flatMap这个操作符把long转换成Observable<Translation>,这个返回的就是转换的类型,同时也可以体现出链式操作。

停止轮询

现在问题解决了但我们还剩下一个问题就是怎么停止轮询,首先链式操作返回的是一个Disposable,然后我们调用disposable.dispose()来断开上游与下游的联系。

butterknife - Android Studio插件

发表于 2018-05-03 | 分类于 Android 插件推荐 | 阅读次数: ℃

不知道有时候你在开发Android程序的时候有没有为反反复复的findViewById和设置onclick而苦恼吗?那你就可以用butterKnife来解决你的苦恼,接下来我们就来教大家怎么设置butterKnife。

下载插件和添加依赖:

下载插件

打开Preferences里面的Plugins,接着选择Browse Repositories,这里你就可以搜索Android Studio里面的所有插件,具体内容可以参考图。

接着搜索Android ButterKnife Zelezny这个插件,再网上看了一些教程,都只是在这里为止了,但是我的电脑一直都没有效,最后自己看了它的GitHub仓库,发现还需要添加两个依赖。

添加依赖

我们还得在dependencies里面添加两个依赖,具体的依赖如下,大家直接添加即可,如果需要最新的版本,可以直接在它的Github仓库看看,我把仓库地址附在这里butterknife。

1
2
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

简单地教大家使用这个插件

其实这个插件用起来挺简单的,我们就直接按照平时地习惯在xml里面给控件添加id,然后在每个activity或者fragment对应的布局视图的地方按住Command键和N键,这时候就会出现一个菜单,我们就选择Generate Butterknife injections,菜单如图所示。

接着我们点击了那个以后就会出现一个菜单,具体的菜单如下所示:

左边的红框最上面的是全选,或者单个的选择,OnClick选择生成点击事件,Variable Name是用来设置button的名字,最下面的split OnClick methods是把点击事件分开,Create ViewHolder在Recyclerview里面使用的,最后贴上几张效果图。

Material Design的研究(一)

发表于 2018-05-01 | 分类于 Material Design | 阅读次数: ℃

由沉浸式状态栏引出

援引一句郭霖大神的话来破除大家对沉浸式的误解。

其实说到沉浸式状态栏这个名字我也是感到很无奈,真不知道这种叫法是谁先发起的。因为Android官方从来没有给出过沉浸式状态栏这样的命名,只有沉浸式模式(Immersive Mode)这种说法。而有些人在没有完全了解清楚沉浸模式到底是什么东西的情况下,就张冠李戴地认为一些系统提供的状态栏操作就是沉浸式的,并且还起了一个沉浸式状态栏的名字。

所以说真正的沉浸式是什么,还是援引郭霖大神的话:

那么对应到Android操作系统上面,怎样才算是沉浸式体验呢?这个可能在大多数情况下都是用不到的,不过在玩游戏或者看电影的时候就非常重要了。因为游戏或者影视类的应用都希望能让用户完全沉浸在其中,享受它们提供的娱乐内容,但如果这个时候在屏幕的上方还显示一个系统状态栏的话,可能就会让用户分分钟产生跳戏的感觉。

但是我们就是想要我们以前以为的沉浸式的效果,就像Tim和网易云音乐那种效果我们到底要怎么达成呢?我们有两种思路,一个是指定状态栏的颜色,也就是纯色的,还有一种就是通过全屏模式

这张是Tim的截图

这是Tim的图片

这张是Tim的截图

这是网易云音乐的状态栏

介绍一下几个属性:

  1. fitsSystemWindows:
    我们的理解是:在Android版本大于5.0的情况下,对于CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout一起使用的时候,如果对其中的一个子控件设置fitsSystemWindows时,需要对它们的父控件都设置该属性;但是对于其它情况,这个属性的含义是为了不让我们的控件占据系统窗口的空间,比如说状态栏和导航栏(这一属性需要设置状态栏或者导航栏透明或者半透明的时候才会有效)。

指定状态栏的颜色来实现沉浸式(这种说法不准确,但是还是暂时这么说)

在style.xml里面设置对应的Activity的theme,通过设置android:statusBarColor这一属性来直接设置状态栏的颜色,这种方案适用于==纯色==的toolBar或者ActionBar,具体代码如下:

1
2
3
<style name="MainActivityTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/colorPrimary</item>
</style>

在AndroidManifest.xml中设置刚才定义的style,具体的代码如下:

1
2
3
4
5
6
7
8
<activity android:name=".MainActivity"
android:theme="@style/MainActivityTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

通过设置全屏和状态栏透明来实现沉浸式

我们首先在需要设置全屏,保持状态栏悬浮,具体的代码如下:

1
2
3
View view = getWindow().getDecorView();            
int options = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
view.setSystemUiVisibility(options);

接着我们设置状态栏,具体的代码如下:

1
getWindow().setStatusBarColor(Color.TRANSPARENT);

通过设置全屏和设置状态栏为TRANSLUCENT来实现沉浸式

首先也要设置全屏,和之前的一样,接着要设置状态栏透明(在5.0以上为半透明)

1
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

Toolbar——Actionbar的替代者

一个Toolbar的基本效果如下图所示:

我们通常会设置Toolbar的navigationicon、subtitle、title、menu和logo,设置这些属相的代码我贴在下面了:

1
2
3
4
5
6
7
8
9
10
11
12
13
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:paddingTop="24dp"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
app:navigationIcon="@drawable/ic_menu"
app:logo="@drawable/ic_head"
app:popupTheme="@style/Base.ThemeOverlay.AppCompat"
app:title="@string/app_name"
app:subtitle="yuanlai" />

因为我们指定的style为Light,所以文字会变成深色,这会很难看的,这里我们需要把Actionbar的theme设置为深色,这样文字就会变成浅色,但是这样弹出的菜单项会变成深色,所以我们又需要设置弹出菜单的theme。xmlns:app是因为Material Design库在Android 5.0系统中才出现,在之前的系统中并不存在,所以为了兼容之前的系统,才会有app:这种写法。

同时我们发现ToolBar和statusbar的颜色保持一致,这里我们用到之前设置statusbar的知识,设置为全屏然后接着设置statusbar为透明色,这里特殊之处在于我们为Toolbar设置的高度为wrap_content并设置了一个最小的高度,这样就可以保证statusbar处的颜色和Toolbar的颜色一致。

接下来我们来具体分析这些属性该怎么用:

title、subtitle、logo、navigationicon的设置

这几个属性直接用app:加上对应的属性名即可。

menu 和 navigationicon的点击事件设置:

其中menu的填充一定要在代码中执行,具体定义的步骤如下:

  • 先在menu文件下面新建一个menu文件用来承载具体的menu的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/backup"
android:icon="@drawable/ic_backup_white"
android:title="Backup"
app:showAsAction="always"/>

<item
android:id="@+id/delete"
android:icon="@drawable/ic_delete_white"
android:title="Delete"
app:showAsAction="ifRoom"/>

<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings_white"
android:title="Setting"
app:showAsAction="never"/>
</menu>
  • 在代码里面填充刚刚定义的menu文件
1
2
3
4
5
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar,menu);
return true;
}
  • 设置menu的点击事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.backup:
Toast.makeText(this, "You clicked backup", Toast.LENGTH_SHORT).show();
break;
case R.id.delete:
Toast.makeText(this, "You clicked delete", Toast.LENGTH_SHORT).show();
break;
case R.id.settings:
Toast.makeText(this, "you clicked settings", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
return true;
}

而我们之前设置的navigationicon的点击事件设置则更加简单,其具体代码如下:

1
2
3
4
5
6
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

附:Toolbar可是一个viewgroup

在实际开发中我们会遇见一个两难的情况,我们希望Toolbar的title到中间去,但是Toolbar似乎不给我们留什么修改它的接口,但是Material Design的许多效果要搭配Toolbar使用才行,比如说CollapsingToolbarLayout。所以说我们得换一个思路,即把Toolbar作为一个viewgroup,具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:paddingTop="24dp"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar">

<TextView
android:layout_gravity="center"
android:text="@string/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"/>

</android.support.v7.widget.Toolbar>

这一段代码关键是设置了layot_gravity和style属性,其中style把自定义的Textview显示的和Actionbar.Title保持一致,同时还需要在代码里面控制Actionbar的title不显示,具体的代码如下:

1
getSupportActionBar().setDisplayShowTitleEnabled(false);

附:利用actionLayout来模仿网易云

我在网上看到大神亦枫的博客,看到他利用Toolbar做出了网易云音乐搜索栏的效果,感觉很牛逼。在通常情况下,Toolbar的Menu item只有一个icon,但是有时候有些特殊情况,需要我们自定义Menu item,最后完成的效果如下图所示:

我们需要额外定义一个layout文件,然后通过actionLayout属性引入到标签中。layout具体代码如下:

1
2
3
4
5
6
7
8
9
10
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="搜索音乐、歌手、歌词、用户"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>

这里需要注意两点:

  • 那个layout的跟布局必须是以RelativeLayout作为根布局,否则,视图无法填满Toolbar或者是Actionbar。
  • actionLayout这一属性必须是以app为命名空间。

将menu item移至中间

如果我们想把menu移到中间来,而不是把它放到右边去,这个时候我们就需要想到用ActionMenuView,其作为Toolbar的一个子控件,把menu item都放到自己里面,我们先来看看它的布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:paddingTop="24dp"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:layout_height="wrap_content"
android:background="@color/colorRed"
android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/Base.ThemeOverlay.AppCompat" >
<android.support.v7.widget.ActionMenuView
android:layout_gravity="center"
android:id="@+id/amv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</android.support.v7.widget.Toolbar>

接着在代码里把menu资源文件加载到通过findViewById()找到的ActionMenuView里面去,具体的代码如下所示:

1
2
3
4
5
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar, amvSearch.getMenu());
return true;
}

接着直接对ActionMenuView设置点击事件,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
amvSearch.setOnMenuItemClickListener(new ActionMenuView.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {

switch (item.getItemId()){

}

return false;
}
});

具体的效果图如下:

Retrofit2 学习笔记

发表于 2018-04-28 | 分类于 Retrofit And RxJava | 阅读次数: ℃

准备工作

Retrofit 2 是一款Android网络请求框架,其底层是基于OkHttp实现的,它与其它网络框架的不同之处在于,它更多使用运行时注解的功能提供此功能。、

我学习这个框架的目的是为学习Rxjava打下基础

一开始当然是引入retrofit的库,具体的可以看retrofit的官方网站,里面列举了所有用法的例子,为了帮助大家快速的上手,我把导入包的方式列举在下面,当然最新的还是要去它的官网看的。

GRADLE

compile 'com.squareup.retrofit2:retrofit:2.4.0'

然而引入这个是不够的,大家还需要引入GSON转换的库,也在下面跟大家列举出来了。

com.squareup.retrofit2:converter-gson:2.3.0

当然不止这些转化的类型,还有其它很多库,我在这里就不一一列举了,大家想要的可以去它的官网看,至于有哪些类型大家可以参考下图:

2018-4-27-1

Retrofit的Get请求

我们通过向淘宝的ip地址库发送Get请求来讲解如何使用Retrofit的Get请求,请求的顺序大致是:

  1. 定义返回值的实体类
  2. 定义一个网络接口
  3. 创建一个Retrofit对象
  4. 创建一个接口对象
  5. 实现接口

定义返回值的实体类

我们通过分析返回值来构造一个实体类,拿淘宝的ip地址库的返回值举一个例子,其返回值具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"code": 0,
"data": {
"ip": "59.108.54.37",
"country": "中国",
"area": "",
"region": "北京",
"city": "北京",
"county": "XX",
"isp": "方正宽带",
"country_id": "CN",
"area_id": "",
"region_id": "110000",
"city_id": "110100",
"county_id": "xx",
"isp_id": "100063"
}
}

我们通过返回的值来构造一个Java Bean类,具体的代码如下:

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
public class TaoBaoInfo {

private int code;

private DataBean data;

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public DataBean getData() {
return data;
}

public void setData(DataBean data) {
this.data = data;
}

public class DataBean {

private String ip;
private String country;
private String area;
private String region;
private String city;
private String county;
private String isp;
private String country_id;
private String area_id;
private String region_id;
private String city_id;
private String county_id;
private String isp_id;

public String getIp() {
return ip;
}

public void setIp(String ip) {
this.ip = ip;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}

public String getArea() {
return area;
}

public void setArea(String area) {
this.area = area;
}

public String getRegion() {
return region;
}

public void setRegion(String region) {
this.region = region;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public String getCounty() {
return county;
}

public void setCounty(String county) {
this.county = county;
}

public String getIsp() {
return isp;
}

public void setIsp(String isp) {
this.isp = isp;
}

public String getCountry_id() {
return country_id;
}

public void setCountry_id(String country_id) {
this.country_id = country_id;
}

public String getArea_id() {
return area_id;
}

public void setArea_id(String area_id) {
this.area_id = area_id;
}

public String getRegion_id() {
return region_id;
}

public void setRegion_id(String region_id) {
this.region_id = region_id;
}

public String getCity_id() {
return city_id;
}

public void setCity_id(String city_id) {
this.city_id = city_id;
}

public String getCounty_id() {
return county_id;
}

public void setCounty_id(String county_id) {
this.county_id = county_id;
}

public String getIsp_id() {
return isp_id;
}

public void setIsp_id(String isp_id) {
this.isp_id = isp_id;
}
}
}

定义一个网络接口

接着我们需要定义一个网络接口,具体的代码如下:

1
2
3
4
5
6
public interface TaoBaoService {

@GET("getIpInfo.php")
Call<TaoBaoInfo> getIpMsg(@Query("ip")String ip);

}

这里有几个特殊的地方,在@Get后面的括号里面接着的是该请求的相对地址,其中Call的一堆尖括号里面对应的是返回的数据类型,也就是我们第一步定义的实体类@Query("ip")对应的是请求的数据对应的key值,具体的我们可以分析一个请求的url,具体url值如下:
http://ip.taobao.com/service/getIpInfo.php?ip=59.108.54.37
这个url中?号后面跟着的是请求的数据,假如有多个数据的话要加上&号,我们来举个例子,还是这个url,假如我们要加几个请求的数据,比如说时间,我们就可以这样写url:
http://ip.taobao.com/service/getIpInfo.php?ip=59.108.54.37&time=12:00那我们请求对于这种多个请求的情况就可以通过传入map,那么具体的接口写法如下:

1
2
3
4
5
public interface TaoBaoService {

@GET("getIpInfo.php")
Call<TaoBaoInfo> getIpMsg(@QueryMap Map<String,String> options);
}

创建一个Retrofit对象

1
2
3
4
5
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://ip.taobao.com/service/")
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.build();

其重有两个要点:

  • 设置baseUrl,也就是请求的基地址
  • 设置转换的格式,我们这里把返回的结果转换成Gson或者String,当然还有其它的选择,具体可以参照我们一开始提的准换格式的问题

    创建一个接口对象

    这个主要是创建一个接口对象,没有什么好说的
    TaobaoService api = retrofit.create(TaoBaoService.class);

实现接口

实现接口的具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
api.getIpMsg("59.108.54.37").enqueue(new Callback<TaoBaoInfo>() {

@Override
public void onResponse(Call<TaoBaoInfo> call, Response<TaoBaoInfo> response) {
Toast.makeText(MainActivity.this, response.body().getData().getCity(), Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call<TaoBaoInfo> call, Throwable t) {
Toast.makeText(MainActivity.this, "获取失败", Toast.LENGTH_SHORT).show();
}

});

这里不用管线程切换的问题,回调就发生在主线程中。
对于第二种定义接口的方式,我们的实现方式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Map map = new HashMap<String,String>;
map.put("ip","59.108.54.37");
api.getIpMsg(map).enqueue(new Callback<TaoBaoInfo>() {

@Override
public void onResponse(Call<TaoBaoInfo> call, Response<TaoBaoInfo> response) {
Toast.makeText(MainActivity.this, response.body().getData().getCity(), Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call<TaoBaoInfo> call, Throwable t) {
Toast.makeText(MainActivity.this, "获取失败", Toast.LENGTH_SHORT).show();
}

});

用Path来代替路径

也许看这个标题会很疑惑,我们来用一个url来举一个具体的例子:
http://ip.taobao.com/service/getIpInfo.php?ip=59.108.54.37
我们可以把基地址写成http://ip.taobao.com/,后面接着的service可以被指定为一种路径,这种方式方便减少基地址的数量,也可以用来传输值,比如说之前的http://ip.taobao.com/service/getIpInfo.php?ip=59.108.54.37&time=12:00可以被写成http://ip.taobao.com/service/getIpInfo.php/59.108.54.37/12:00,也就是说传输的值直接被写进路径里面去了,接着我们来写如何具体实现这一方法,首先我们得重新写一个接口,具体的代码如下:

1
2
3
4
5
6
7
public interface TaoBaoService {

@GET("{path}/getIpInfo.php")
Call<TaoBaoInfo> getIpMsg(@Path("path") String path, @Query("ip") String ip);
}

}

@Path后面跟着的括号里面的值必须和@Get括号里面的{path}占位符一样。接着我们写实现接口的代码,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
api.getIpMsg("service","59.108.54.37").enqueue(new Callback<TaoBaoInfo>() {

@Override
public void onResponse(Call<TaoBaoInfo> call, Response<TaoBaoInfo> response) {
Toast.makeText(MainActivity.this, response.body().getData().getCity(), Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call<TaoBaoInfo> call, Throwable t) {
Toast.makeText(MainActivity.this, "获取失败", Toast.LENGTH_SHORT).show();
}

});

Retrofit的Post请求

Retrofit的Post请求和Get请求有很多相似的地方,Post的步骤也可以总结如下:

  1. 定义返回值的实体类
  2. 定义一个网络接口
  3. 创建一个Retrofit对象
  4. 创建一个接口对象
  5. 实现接口

其重步骤1和步骤3,4都是一样的,我们在这里只用讲解下步骤2,5,其具体的代码如下:

定义一个网络接口

1
2
3
4
5
6
7
public interface UserService {

@FormUrlEncoded
@POST("api/user/dogetInfo")
Call<UserInfo> getUserInfo(@Field("username") String username);

}

其中要注意两个地方其一是FormUrlEncoded,意思是Form表单提交,另一个@POST("api/user/dogetInfo")是指定请求的类型,这里的是一个Post请求,UserInfo处指的是返回的类型,也就是我们刚才根据返回的json数据构造的类,@Field("username")这里的括号里面的值是提交表单对应的key值。
写完相应的接口我们在代码里面使用它,具体的代码如下先初始化接口:

实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
api.getUserInfo("yuanlai").enqueue(new Callback<UserInfo>() {
@Override
public void onResponse(Call<UserInfo> call, Response<UserInfo> response) {

Toast.makeText(MainActivity.this, response.body().getData().getSignature(), Toast.LENGTH_SHORT).show();

}

@Override
public void onFailure(Call<UserInfo> call, Throwable t) {

}
});

这里要插一句,这里是异步请求网络,回调的callback是在UI线程里面执行的,所以我们可以直接地Toast。当然了,Retrofit除了表单提交数据外,还可以通过把提交的数据转换成Json格式发送到数据库。这里我只把一些代码简单地贴出来:

1
2
3
4
5
6
public interface UserService {

@POST("api/user/dogetInfo")
Call<UserInfo> getUserInfoByJson(@Body User user);

}

初始化接口一些操作跟我们刚才的操作没有区别,接着我们要定义一个提交的json类,具体代码如下:

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

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public User(String username) {
this.username = username;
}

String username;

}

具体操作的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
api.getUserInfoByJson(new User("yuanlai")).enqueue(new Callback<UserInfo>() {
@Override
public void onResponse(Call<UserInfo> call, Response<UserInfo> response) {

Toast.makeText(MainActivity.this, response.body().getData().getSignature(), Toast.LENGTH_SHORT).show();

}

@Override
public void onFailure(Call<UserInfo> call, Throwable t) {

}
});

Jsoup 模拟登录

发表于 2018-04-23 | 分类于 爬虫 | 阅读次数: ℃

在日常开发的时候,我们爬取数据,有些数据需要我们登陆之后才能够爬取。举一个例子,我们想要爬取学校图书馆里面的历史图书,我们就必须登陆以后再获取,这个时候我们可以选择模拟登陆——即我们可以用程序来模拟整个登陆的过程。

Jsoup模拟登陆的大概步骤(以待验证码的登陆为例)

  1. 首先对整个登陆过程进行网络分析,搞清楚需要用到哪些参数,以及请求的网址。
  2. 输入参数或者在网页中爬取相应的参数。
  3. 对请求网址发起网络请求,获取返回的数据,一般情况下是一个网页。
  4. 登录成功以后就可以通过获取之前网络请求的cookie来访问一些需要登录后才能浏览的网页

Jsoup模拟登陆的详细步骤

  1. 我们想要模拟登陆就必须了解整个登陆的完整过程,这里我推荐使用Google的Chrome浏览器来分析整个网络请求的过程。先用Chrome浏览器加载出登陆的页面,然后点反键选择检查,接着选择network。

按照提示刷新页面,mac使用command加R刷新后才能看到网络请求。我们先试着登录一遍,填好了所有的数据以后登录,我们可以看到我这个请求的所有请求参数可以在Headers里面的Form Data里面看到,下面的例子中我们一共看到了五个参数:1. number 2.passwd 3.captcha 4.select 5.returnUrl

同时在Headers里面的General可以看到登陆的请求网址。

这样我们需要的请求网址和所需的所有参数都知道了。

  1. 通过上面的一个步骤我们知道我们的所有参数里面有一个验证码需要我们获取,我的思路是先把他下载到本地再输入,那这个验证码如何获取呢?我们可以在所有的网络请求的列表里面看到验证码的请求,并且得到验证码的请求地址。

我们先向验证码的地址发送请求,获取到回应是二进制数组,再下载到本地。
这里还要补充一句,我所有的网络请求和爬取数据用的都是Jsoup,需要在网络上面下载Jsoup的Jar包再添加到项目的lib里面,这里具体的做法我就不做阐述,具体的可以自己在网上搜索,这里可以参考Eclipse的三个方法,我是用的是外部添加Jar的方法。具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Response response = Jsoup.connect("http://opac.lib.wust.edu.cn:8080/reader/captcha.php")
.ignoreContentType(true)
.execute();
byte[] data = response.bodyAsBytes();
cookie = response.cookies();

if (data != null) {
String filepath = "/Users/yuanyuanlai/Pictures"+"\\"+"yzm.gif";
File file = new File(filepath);
if (file.exists()) {
file.delete();
}
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(data, 0, data.length);
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}

}

这里我们一定要注意上面的一个获取cookie的这个步骤,因为这个cookie联系整个步骤,要不然验证码怎么和后面的登录请求联系到一起

  1. 这一步里我们可以正式发送请求,具体的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
System.out.println("请输入验证码:");
Scanner input = new Scanner(System.in);
String yzm = input.next();
input.close();

Map<String, String> datas = new HashMap<String,String>();
datas.put("number", "201613137217");
datas.put("passwd", "310810");
datas.put("captcha", yzm);
datas.put("select", "bar_no");
datas.put("returnUrl", "");

Connection connection2 = Jsoup.connect("http://opac.lib.wust.edu.cn:8080/reader/redr_verify.php");
connection2.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0");

res = connection2
.data(datas)
.cookies(cookie)
.method(Method.POST)
.execute();

将之前获取的验证码输入进去,以Map的形式储存请求的参数,简单的设置浏览器代理以后发送请求,这里最为重要的是在发送请求的时候也要带上cookie。

  1. 完成登录之后我们可以尝试着使用登录成功后的cookie爬取需要登录后的网页,比如我登录成功后想要爬取历史图书,比如下图:

我们在代码里面尝试着登录,爬取到历史图书,具体代码如下:

1
2
3
4
5
6
7
8
document = Jsoup.connect("http://opac.lib.wust.edu.cn:8080/reader/book_hist.php")
.cookies(cookie)
.get();
Elements elements = document.select("table.table_line").select("tbody")
.select("tr");
for(int i=1 ; i<elements.size() ;i++) {
System.out.println(elements.get(i).select("td").get(2).text());
}

爬取到的结果是:

说明登录成功,这里还得补充一句,Jsoup的具体教程得参考它的官方教程Jsoup教程

所有的代码

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
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import org.jsoup.Connection;
import org.jsoup.Connection.Method;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;


public class loginTest1 {

public static void main(String[] args) {
// TODO Auto-generated method stub
Document document = null;
Response res = null;
Document doc = null;
Map<String, String> cookie = new HashMap<String,String>();

try {

Response response = Jsoup.connect("http://opac.lib.wust.edu.cn:8080/reader/captcha.php")
.ignoreContentType(true)
.execute();
byte[] data = response.bodyAsBytes();
cookie = response.cookies();

if (data != null) {
String filepath = "/Users/yuanyuanlai/Pictures"+"\\"+"yzm.gif";
File file = new File(filepath);
if (file.exists()) {
file.delete();
}
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(data, 0, data.length);
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}

}

System.out.println("请输入验证码:");
Scanner input = new Scanner(System.in);
String yzm = input.next();
input.close();

Map<String, String> datas = new HashMap<String,String>();
datas.put("number", "201613137217");
datas.put("passwd", "310810");
datas.put("captcha", yzm);
datas.put("select", "bar_no");
datas.put("returnUrl", "");

Connection connection2 = Jsoup.connect("http://opac.lib.wust.edu.cn:8080/reader/redr_verify.php");
connection2.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0");

res = connection2
.data(datas)
.cookies(cookie)
.method(Method.POST)
.execute();

document = Jsoup.connect("http://opac.lib.wust.edu.cn:8080/reader/book_hist.php")
.cookies(cookie)
.get();
Elements elements = document.select("table.table_line").select("tbody")
.select("tr");
for(int i=1 ; i<elements.size() ;i++) {
System.out.println(elements.get(i).select("td").get(2).text());
}
} catch (IOException e) {
e.printStackTrace();
}

}

}

稳稳的科学上网教程

发表于 2018-04-23 | 分类于 科学上网 | 阅读次数: ℃

服务器选择Centos 6x64

  1. 锐速更换内核:

    1
    rpm -ivh http://soft.91yun.org/ISO/Linux/CentOS/kernel/kernel-firmware-2.6.32-504.3.3.el6.noarch.rpm
  2. 1
    rpm -ivh http://soft.91yun.org/ISO/Linux/CentOS/kernel/kernel-2.6.32-504.3.3.el6.x86_64.rpm --force
  3. reboot

  4. 1
    wget -N --no-check-certificate https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/ssr.sh && chmod +x ssr.sh && bash ssr.sh
  5. 安装锐速

  6. 安装libsodium(chacha20)
  7. 安装ShadowSocksR

配置后的相关信息:


相关链接:
服务器网址,价格和可靠性都不错
ping测试网站,用来检测ip地址是否被黑

袁来

袁来

欲戴其冠,必承其重

9 日志
9 分类
17 标签
GitHub E-Mail
© 2018 袁来