程序员徐公

微信公众号:【徐公】

0%

说在前面

本次推出 Android Architecture Components 系列文章,目前写好了四篇,主要是关于 lifecycle,livedata 的使用和源码分析,其余的 Navigation, Paging library,Room,WorkMannager 等春节结束之后会更新,欢迎关注我的公众号,有更新的话会第一时间会在公众号上面通知。

目录大概如下

1 LiveData 基本使用
2 自定义 Livedata
3 Livedata 共享数据
4 Livedata 小结

Android lifecycle 使用详解

Android LiveData 使用详解

Android lifecyle 源码解剖

Android livedata 源码解剖

github sample 地址: ArchiteComponentsSample

程序员徐公,一位不羁的码农。


前言

在上一篇博客中,我们讲解了 lifecycle 的使用及优点。这篇博客让我们一起来了解一下 LiveData 是怎样使用的?


为什么要引进 LiveData

LiveData 是一个可以被观察的数据持有类,它可以感知 Activity、Fragment或Service 等组件的生命周期。简单来说,他主要有一下优点。

  1. 它可以做到在组件处于激活状态的时候才会回调相应的方法,从而刷新相应的 UI
  2. 不用担心发生内存泄漏
  3. 当 config 导致 activity 重新创建的时候,不需要手动取处理数据的储存和恢复。它已经帮我们封装好了
  4. 当 Actiivty 不是处于激活状态的时候,如果你想 livedata setValue 之后立即回调 obsever 的 onChange 方法,而不是等到 Activity 处于激活状态的时候才回调 obsever 的 onChange 方法,你可以使用 observeForever 方法,但是你必须在 onDestroy 的时候 removeObserver。

回想一下,在你的项目中,是不是经常会碰到这样的问题,当网络请求结果回来的时候,你经常需要判断 Activity 或者 Fragment 是否已经 Destroy, 如果不是 destroy,才更新 UI。

而当你如果使用 Livedata 的话,因为它是在 Activity 处于 onStart 或者 onResume 的状态时,他才会进行相应的回调,因而可以很好得处理这个问题,不必写一大堆的 activity.isDestroyed()。接下来,让我们一起来看一下 LiveData 的使用


LiveData 使用

基本使用

  1. 引入相关的依赖包
1
2
3
4
5
6
// ViewModel and LiveData
implementation "android.arch.lifecycle:extensions:1.1.0"
// alternatively, just ViewModel
implementation "android.arch.lifecycle:viewmodel:1.1.0"
// alternatively, just LiveData
implementation "android.arch.lifecycle:livedata:1.1.0"
  1. 在代码中使用

LiveData 是一个抽象类,它的实现子类有 MutableLiveDataMediatorLiveData。在实际使用中,用得比较多的是 MutableLiveData。他常常结合 ViewModel 一起使用。下面,让我们一起来看一下怎样使用它?

首先,我们先写一个类继承我们的 ViewModel,里面持有 mNameEvent。

1
2
3
4
5
6
7
8
9
public class TestViewModel extends ViewModel {

private MutableLiveData<String> mNameEvent = new MutableLiveData<>();

public MutableLiveData<String> getNameEvent() {
return mNameEvent;
}

}

接着,我们在 Activity 中创建 ViewModel,并监听 ViewModel 里面 mNameEvent 数据的变化,当数据改变的时候,我们打印相应的 log,并设置给 textView,显示在界面上。这样我们就完成了对 mNameEvent 数据源的观察。

1
2
3
4
5
6
7
8
9
mTestViewModel = ViewModelProviders.of(this).get(TestViewModel.class);
MutableLiveData<String> nameEvent = mTestViewModel.getNameEvent();
nameEvent.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
Log.i(TAG, "onChanged: s = " + s);
mTvName.setText(s);
}
});

最后当我们数据源改变的时候,我们需要调用 livedata 的 setValue 或者 postvalue 方法。他们之间的区别是, 调用 setValue 方法,Observer 的 onChanged 方法会在调用 serValue 方法的线程回调。而
postvalue 方法,Observer 的 onChanged 方法将会在主线程回调。

1
mTestViewModel.getNameEvent().setValue(name);

可能部分同学有这样的疑问了,我们的 ViewModel 是通过 ViewModelProviders.of(this).get(TestViewModel.class); 方法创建出来的,如果我们要携带参数,怎么办?

其实,官方也替我们考虑好了,同样是调用 ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) 方法,只不过,需要多传递一个 factory 参数。

Factory 是一个接口,它只有一个 create 方法。

1
2
3
4
5
6
7
8
9
10
11
12
public interface Factory {
/**
* Creates a new instance of the given {@code Class}.
* <p>
*
* @param modelClass a {@code Class} whose instance is requested
* @param <T> The type parameter for the ViewModel.
* @return a newly created ViewModel
*/
@NonNull
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}

在实际当中,我们的做法是:实现 Factory 接口,重写 create 方法,在create 方法里面调用相应的构造函数,返回相应的实例。

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

private final String mKey;
private MutableLiveData<String> mNameEvent = new MutableLiveData<>();

public MutableLiveData<String> getNameEvent() {
return mNameEvent;
}

public TestViewModel(String key) {
mKey = key;
}

public static class Factory implements ViewModelProvider.Factory {
private String mKey;

public Factory(String key) {
mKey = key;
}

@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
return (T) new TestViewModel(mKey);
}
}

public String getKey() {
return mKey;
}
}

ViewModelProviders.of(this, new TestViewModel.Factory(mkey)).get(TestViewModel.class)


自定义 Livedata

Livedata 主要有几个方法

  1. observe
  2. onActive
  3. onInactive
  4. observeForever

void observe (LifecycleOwner owner, Observer observer)

Adds the given observer to the observers list within the lifespan of the given owner. The events are dispatched on the main thread. If LiveData already has data set, it will be delivered to the observer.

void onActive ()

Called when the number of active observers change to 1 from 0.
This callback can be used to know that this LiveData is being used thus should be kept up to date.

当回调该方法的时候,表示该 liveData 正在背使用,因此应该保持最新

void onInactive ()

Called when the number of active observers change from 1 to 0.
This does not mean that there are no observers left, there may still be observers but their lifecycle states aren’t STARTED or RESUMED (like an Activity in the back stack).
You can check if there are observers via hasObservers().

当该方法回调时,表示他所有的 obervers 没有一个状态处理 STARTED 或者 RESUMED,注意,这不代表没有 observers。

Void observeForever

跟 observe 方法不太一样的是,它在 Activity 处于 onPause ,onStop, onDestroy 的时候,都可以回调 obsever 的 onChange 方法,但是有一点需要注意的是,我们必须手动 remove obsever,否则会发生内存泄漏。

这里我们以观察网络状态变化为例子讲解

  1. 首先我们自定义一个 Class NetworkLiveData,继承 LiveData,重写它的 onActive 方法和 onInactive 方法
  2. 在 onActive 方法中,我们注册监听网络变化的广播,即ConnectivityManager.CONNECTIVITY_ACTION。在 onInactive 方法的时候,我们注销广播。
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
public class NetworkLiveData extends LiveData<NetworkInfo> {

private final Context mContext;
static NetworkLiveData mNetworkLiveData;
private NetworkReceiver mNetworkReceiver;
private final IntentFilter mIntentFilter;

private static final String TAG = "NetworkLiveData";

public NetworkLiveData(Context context) {
mContext = context.getApplicationContext();
mNetworkReceiver = new NetworkReceiver();
mIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
}

public static NetworkLiveData getInstance(Context context) {
if (mNetworkLiveData == null) {
mNetworkLiveData = new NetworkLiveData(context);
}
return mNetworkLiveData;
}

@Override
protected void onActive() {
super.onActive();
Log.d(TAG, "onActive:");
mContext.registerReceiver(mNetworkReceiver, mIntentFilter);
}

@Override
protected void onInactive() {
super.onInactive();
Log.d(TAG, "onInactive: ");
mContext.unregisterReceiver(mNetworkReceiver);
}

private static class NetworkReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager manager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
getInstance(context).setValue(activeNetwork);

}
}
}

这样,当我们想监听网络变化的时候,我们只需要调用相应的 observe 方法即可,方便又快捷。

1
2
3
4
5
6
7
NetworkLiveData.getInstance(this).observe(this, new Observer<NetworkInfo>() {
@Override
public void onChanged(@Nullable NetworkInfo networkInfo) {
Log.d(TAG, "onChanged: networkInfo=" +networkInfo);
}
});


https://www.jianshu.com/p/4b7945475a6f

共享数据

Fragment Activity 之间共享数据

我们回过头来再来看一下 ViewModelProvider 的 of 方法,他主要有四个方法,分别是

  1. ViewModelProvider of(@NonNull Fragment fragment)
  2. ViewModelProvider of(@NonNull FragmentActivity activity)
  3. ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory)
  4. ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory)

1,2 方法之间的主要区别是传入 Fragment 或者 FragmentActivity。而我们知道,通过 ViewModel of 方法创建的 ViewModel 实例, 对于同一个 fragment 或者 fragmentActivity 实例,ViewModel 实例是相同的,因而我们可以利用该特点,在 Fragment 中创建 ViewModel 的时候,传入的是 Fragment 所依附的 Activity。因而他们的 ViewModel 实例是相同的,从而可以做到共享数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// LiveDataSampleActivity(TestFragment 依赖的 Activity)
mTestViewModel = ViewModelProviders.of(this, new TestViewModel.Factory(mkey)).get(TestViewModel.class);
MutableLiveData<String> nameEvent = mTestViewModel.getNameEvent();
nameEvent.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
Log.i(TAG, "onChanged: s = " + s);
mTvName.setText(s);
}
});


// TestFragment 中
mViewModel = ViewModelProviders.of(mActivity).get(TestViewModel.class);
mViewModel.getNameEvent().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
Log.d(TAG, "onChanged: s =" + s + " mViewModel.getKey() =" + mViewModel.getKey());
mTvName.setText(s);
boolean result = mViewModel == ((LiveDataSampleActivity) mListener).mTestViewModel;
Log.d(TAG, "onChanged: s result =" + result);
}
});

这样,LiveDataSampleActivity 和 TestFragment 中的 ViewModel 是同一个实例。即 Activity 和 Fragment 共享数据。

全局共享数据

说到全局共享数据,我们想一下我们的应用全景,比如说我的账户数据,这个对于整个 App 来说,肯定是全局共享的。有时候,当我们的数据变化的时候,我们需要通知我们相应的界面,刷新 UI。如果用传统的方式来实现,那么我们一般才采取观察者的方式来实现,这样,当我们需要观察数据的时候,我们需要添加 observer,在界面销毁的时候,我们需要移除 observer。

但是,如果我们用 LiveData 来实现的话,它内部逻辑都帮我们封装好了,我们只需要保证 AccountLiveData 是单例的就ok,在需要观察的地方调用 observer 方法即可。也不需要手动移除 observer,不会发生内存泄漏,方便快捷。

这里 AccountLiveData 的实现就不贴出来了,可以参考上面的 NetworkLiveData 实现


小结

这里说一点关于 LiveData 与 ViewModel 的应用场景,我尽量说得通俗一点,不要说得那么官方,这样对新手很难理解。

觉得不错的,请点个赞,让我们看到你们的欢呼声。你们的支持就是我写作的最大动力。

  1. LiveData 内部已经实现了观察者模式,如果你的数据要同时通知几个界面,可以采取这种方式
  2. 我们知道 LiveData 数据变化的时候,会回调 Observer 的 onChange 方法,但是回调的前提是 lifecycleOwner(即所依附的 Activity 或者 Fragment) 处于 started 或者 resumed 状态,它才会回调,否则,必须等到 lifecycleOwner 切换到前台的时候,才回调。
  3. 因此,这对性能方面确实是一个不小的提升。但是,对于你想做一些类似与在后台工作的(黑科技), liveData 就不太适合了,你可以使用 observeForever 方法,或者自己实现观察者模式去吧。

Lifecycle,LiveData, ViewModel 的基本使用到此已经讲解完毕,想了解他们的实现原理的话可以阅读这两篇文章。

Android lifecyle 源码解剖

Android livedata 源码解剖

github sample 地址: ArchiteComponentsSample

推荐阅读

Android 启动优化(一) - 有向无环图

Android 启动优化(二) - 拓扑排序的原理以及解题思路

Android 启动优化(三)- AnchorTask 开源了

Android 启动优化(四)- AnchorTask 是怎么实现的

Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

Android 启动优化(六)- 深入理解布局优化

这几篇文章从 0 到 1,讲解 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的应用。

推荐理由:现在挺多文章一谈到启动优化,动不动就聊拓扑结构,这篇文章从数据结构到算法、到设计都给大家说清楚了,开源项目也有非常强的借鉴意义。

在这里插入图片描述

说在前面

本次推出 Android Architecture Components 系列文章,目前写好了四篇,主要是关于 lifecycle,livedata 的使用和源码分析,其余的 Navigation, Paging library,Room,WorkMannager 等春节结束之后会更新,欢迎关注我的公众号,有更新的话会第一时间会在公众号上面通知。

Android lifecycle 使用详解

Android LiveData 使用详解

Android lifecyle 源码解剖

Android livedata 源码解剖

github sample 地址: ArchiteComponentsSample

徐公码字,一位不羁的码农。


前言

前两篇博客,我们已经讲解了 lifecycle ,liveData, ViewModel 的使用,这一篇博客,让我们一起来看一下 lifecycle 的原理。


从自定义的 lifecycle 说起

首先我们先来复习一下,如果要自定义 lifecycle,我们要这样做。

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
public class CustomLifecycleActivity extends FragmentActivity implements LifecycleOwner {

private LifecycleRegistry mLifecycleRegistry;

private static final String TAG = "CustomLifecycleActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom_lifecycle);
mLifecycleRegistry = new LifecycleRegistry(this);
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
getLifecycle().addObserver(new GenericLifecycleObserver() {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
Log.d(TAG, "onStateChanged: event = " + event);
}
});

}

@Override
protected void onStart() {
super.onStart();
mLifecycleRegistry.markState(Lifecycle.State.STARTED);
}

@Override
protected void onResume() {
super.onResume();
mLifecycleRegistry.markState(Lifecycle.State.RESUMED);
}

@Override
protected void onDestroy() {
super.onDestroy();
mLifecycleRegistry.markState(Lifecycle.State.DESTROYED);
}

@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}

  1. 第一步:先实现 LifecycleOwner 接口,并返回 mLifecycleRegistry
  2. 第二步:在 Activity 生命周期变化的时候,调用 mLifecycleRegistry.markState() 方法标记相应的状态
  3. 如果想添加 observer,调用 addObserver 方法添加观察者,这样会在 activity 生命周期变化的时候,回调 observer 的 onchange 方法。

我们先来看一下 getLifecycle() 方法, getLifecycle() 它返回的是一个 Lifecycle 的实例,sdk 中默认的实现类为 LifecycleRegistry。

接下来,我们一起来看一下它的 observer 方法。

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
public void addObserver(@NonNull LifecycleObserver observer) {
// 判断是否是 DESTROYED,如果是将初始状态置为 DESTROYED,否则为 INITIALIZED
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
// ObserverWithState 包装
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
// 将 observer 作为key,在缓存的 mObserverMap 中查找是否存在
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

// 存在,直接返回回去,证明该 observer 已经添加过了。否则,证明还没有添加过该 observer
if (previous != null) {
return;
}

LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

// 这里 mAddingObserverCounter 为 0 ,mHandlingEvent 为 false
boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}

if (!isReentrance) {
// we do sync only on the top level.
sync();
}
mAddingObserverCounter--;
}

在 addObserver 方法中,它主要干这几件事情

  1. 首先,先初始化状态, 判断当前 mState 是否是 DESTROYED,如果是将初始状态置为 DESTROYED,否则为 INITIALIZED,接着用 ObserverWithState 包装 observer 和 初始化状态 initialState
  2. 将 observer 作为 key,在缓存的 mObserverMap 中查找是否存在,如果存在,证明该 observer 已经添加过,直接返回回去,不必再进行处理。
  3. addObserver 方法中第 21 行 , isReentrance 一般情况下为 false,什么情况 为 true,暂时未想到,

接下来我们先来看 calculateTargetState 方法。

1
2
3
4
5
6
7
8
9
10
11
12
private State calculateTargetState(LifecycleObserver observer) {
// 取出 mObserverMap 的上一个 entry,previous
Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);

// 如果不为空,获取它的状态
State siblingState = previous != null ? previous.getValue().mState : null;
// 判断 mParentStates 是否为 null,不为 null,去最后的一个状态,否则,为 null
State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
: null;
// 取最小的状态
return min(min(mState, siblingState), parentState);
}
  1. 首先,取出 mObserverMap 中上一个的 entry,该 LifecycleRegistry 实例如果是第一次调用 addObserver 实例的话,那么是 null,否则是上一个 observer 的 entry
  2. 根据 previous 是否为 null,设置 siblingState 的值
  3. 判断 mParentStates 是否为 null,不为 null,取 mParentStates 最后一次的状态
  4. 取 mState, siblingState 最小的状态 a,再取 a 与 parentState 的状态 b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum State {

DESTROYED,

INITIALIZED,

CREATED,

STARTED,

RESUMED;

public boolean isAtLeast(@NonNull State state) {
return compareTo(state) >= 0;
}
}

State 中,他们排序的顺序是 DESTROYED < INITIALIZED < CREATED < STARTED < RESUMED。

我们知道,我们在 activity 的 onCreate 方法中初始化 LifecycleRegistry,并标记它的状态为 CREATED。当我们第一次在 onCreate 方法调用 addObserver 的时候,在 calculateTargetState 方法中,若是首次调用 previous 为 null,则 siblingState,parentState 为 null, 而 mState 为 CREATED,所以最终的状态为 CREATED,即 State targetState = calculateTargetState(observer); 中 targetState 为 CREATED

1
2
// 取最小的状态
return min(min(mState, siblingState), parentState);

看完 calculateTargetState 方法,我们回过头再来看一下 addObserver 方法。

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 void addObserver(@NonNull LifecycleObserver observer) {


// 省略若干行

// 这里 mAddingObserverCounter 为 0 ,mHandlingEvent 为 false
boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}

if (!isReentrance) {
// we do sync only on the top level.
sync();
}
mAddingObserverCounter--;
}

这里 statefulObserver.mState 为 DESTROYED 或者 INITIALIZED,肯定比 CREATED 小。而 mObserverMap.contains(observer) 必定为 true,除非我们手动移除掉 mObserverMap。因而,会走进 while循环。

1
2
3
4
5
private void pushParentState(State state) {
mParentStates.add(state);
}

private ArrayList<State> mParentStates = new ArrayList<>();

pushParentState(statefulObserver.mState); 很简单,只是将 statefulObserver 的状态添加到 mParentStates 集合中。

继续往下走,接着会调用 statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static Event upEvent(State state) {
switch (state) {
case INITIALIZED:
case DESTROYED:
return ON_CREATE;
case CREATED:
return ON_START;
case STARTED:
return ON_RESUME;
case RESUMED:
throw new IllegalArgumentException();
}
throw new IllegalArgumentException("Unexpected state value " + state);
}

upEvent 方法也很简单,只是返回它的下一个 event。这里因为他们的 state为 INITIALIZED,所以它会返回 ON_CREATE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void dispatchEvent(LifecycleOwner owner, Event event) {
State newState = getStateAfter(event);
mState = min(mState, newState);
mLifecycleObserver.onStateChanged(owner, event);
mState = newState;
}

static State getStateAfter(Event event) {
switch (event) {
case ON_CREATE:
case ON_STOP:
return CREATED;
case ON_START:
case ON_PAUSE:
return STARTED;
case ON_RESUME:
return RESUMED;
case ON_DESTROY:
return DESTROYED;
case ON_ANY:
break;
}
throw new IllegalArgumentException("Unexpected event value " + event);
}

这里 event 为 ON_CREATE,所以 newState 也为 CREATED。 mState = min(mState, newState); mState newState,两者状态相同,所以 mState 也为 CREATED。接着回调 mLifecycleObserver 的 onStateChanged 方法。所以,这里,会收到我们的 onCreate 事件,与我们的预想相符。

但是我们并没有在 onStart,onResume, onPause , onStop 和 onDestroy 方法中调用 mLifecycleRegistry.handleLifecycleEvent 方法,它又是怎样促发 Observer 的 onStateChanged 方法的。这里先不揭晓,我们先来看一下 26.1.0 以后的 AppCompatActivity,待会你就明白了,会感叹 google 真的牛逼!

从 26.1.0 以后 AppCompatActivity 的设计说起

我们知道,在 26.1.0 以后,如果我们要使用 lifecycle,我们只需要调用以下的方法即可。

SupportActivity

1
2
3
4
5
6
7
8
getLifecycle().addObserver(new GenericLifecycleObserver() {

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
Log.d(TAG, "onStateChanged: event =" + event);
}
});

跟踪 getLifecycle() 方法,它会跳转到 SupportActivity 的 getLifecycle 方法 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SupportActivity extends Activity implements LifecycleOwner, Component {

private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ReportFragment.injectIfNeededIn(this);
}

@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}

}

在 SupportActivity 中,它默认为我们初始化 mLifecycleRegistry,作为一个成员变量。接着,他在
onCreate 方法中调用了 ReportFragment.injectIfNeededIn(this); 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ReportFragment extends Fragment {
private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle"
+ ".LifecycleDispatcher.report_fragment_tag";

public static void injectIfNeededIn(Activity activity) {
// ProcessLifecycleOwner should always correctly work and some activities may not extend
// FragmentActivity from support lib, so we use framework fragments for activities
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
// Hopefully, we are the first to make a transaction.
manager.executePendingTransactions();
}
}

在 injectIfNeededIn 方法中,它会判断我们是否已经添加 ReportFragment,没有的话,添加进去。

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
public class ReportFragment extends Fragment {
private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle"
+ ".LifecycleDispatcher.report_fragment_tag";

private ActivityInitializationListener mProcessListener;

private void dispatchCreate(ActivityInitializationListener listener) {
if (listener != null) {
listener.onCreate();
}
}

private void dispatchStart(ActivityInitializationListener listener) {
if (listener != null) {
listener.onStart();
}
}

private void dispatchResume(ActivityInitializationListener listener) {
if (listener != null) {
listener.onResume();
}
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
dispatchCreate(mProcessListener);
dispatch(Lifecycle.Event.ON_CREATE);
}

@Override
public void onStart() {
super.onStart();
dispatchStart(mProcessListener);
dispatch(Lifecycle.Event.ON_START);
}

@Override
public void onResume() {
super.onResume();
dispatchResume(mProcessListener);
dispatch(Lifecycle.Event.ON_RESUME);
}

@Override
public void onPause() {
super.onPause();
dispatch(Lifecycle.Event.ON_PAUSE);
}

@Override
public void onStop() {
super.onStop();
dispatch(Lifecycle.Event.ON_STOP);
}

@Override
public void onDestroy() {
super.onDestroy();
dispatch(Lifecycle.Event.ON_DESTROY);
// just want to be sure that we won't leak reference to an activity
mProcessListener = null;
}
}


然后,它在 onCreat ,onStart, onResume, onPause, onStop, onDestroy 方法中分别调用 dispatch 方法进行分发生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

private void dispatch(Lifecycle.Event event) {
Activity activity = getActivity();
if (activity instanceof LifecycleRegistryOwner) {
((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
return;
}

if (activity instanceof LifecycleOwner) {
Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
}
}
}

在 dispatch 方法中,会先判断 activity 是不是实现了 LifecycleRegistryOwner ,如果是,直接分发,不过不是,判断是否实现 LifecycleOwner,获取它的 lifecycle,调用它 的 handleLifecycleEvent 进行分发。

1
2
3
public class SupportActivity extends Activity implements LifecycleOwner, Component {

private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this

而很明显,高版本的 SupportActivity 实现了 LifecycleOwner 接口,并写 LifecycleOwner.getLifecycle() 是 LifecycleRegistry

普通的 Activity

对于 26.1.0 以后的版本,你会发现,对于普通的 Activity,如果你想要使用 lifecycle,你只需要实现
LifecycleOwner 接口即可。当生命周期变化的时候,它也可以回调 Observer 的 onStateChanged 方法。

回到我们前面的问题:

我们并没有在 onStart,onResume, onPause , onStop 和 onDestroy 方法中调用 mLifecycleRegistry.handleLifecycleEvent 方法,它又是怎样促发 onStateChanged 方法的

我们猜想它也是通过 ReportFragment 实现的。但是在 Activity 的 onCreate 方法中,我们并没有发现它有添加 ReportFragment,我们在 As 全局搜一下,看哪些地方使用到 ReportFragment。如下图

从图中可以看到,有几个地方使用到他。我们先来看一下 LifecycleDispatcher

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
class LifecycleDispatcher {

private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle"
+ ".LifecycleDispatcher.report_fragment_tag";

private static AtomicBoolean sInitialized = new AtomicBoolean(false);

static void init(Context context) {
if (sInitialized.getAndSet(true)) {
return;
}
// 在 init 方法中,监听全局 activity 的创建,从而来添加 fragment
((Application) context.getApplicationContext())
.registerActivityLifecycleCallbacks(new DispatcherActivityCallback());
}

@SuppressWarnings("WeakerAccess")
@VisibleForTesting
static class DispatcherActivityCallback extends EmptyActivityLifecycleCallbacks {
private final FragmentCallback mFragmentCallback;

DispatcherActivityCallback() {
mFragmentCallback = new FragmentCallback();
}

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (activity instanceof FragmentActivity) {
((FragmentActivity) activity).getSupportFragmentManager()
.registerFragmentLifecycleCallbacks(mFragmentCallback, true);
}
ReportFragment.injectIfNeededIn(activity);
}

@Override
public void onActivityStopped(Activity activity) {
if (activity instanceof FragmentActivity) {
markState((FragmentActivity) activity, CREATED);
}
}

@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (activity instanceof FragmentActivity) {
markState((FragmentActivity) activity, CREATED);
}
}
}

// 省略若干代码

}

可以看到,它 在 init 方法中,通过 context.getApplicationContext() .registerActivityLifecycleCallbacks 监听全局 activity 的创建,在 activity oncreate 的时候,调用 ReportFragment.injectIfNeededIn(activity) ,从而来添加 fragment,进而分发相应的事件。

那 LifecycleDispatcher 的 init 方法又是在哪里调用的呢? 我们全局搜索一下

1
2
3
4
5
6
7
public class ProcessLifecycleOwnerInitializer extends ContentProvider {
@Override
public boolean onCreate() {
LifecycleDispatcher.init(getContext());
ProcessLifecycleOwner.init(getContext());
return true;
}

可以看到它是在 ProcessLifecycleOwnerInitializer 的 onCreate 方法中调用的。而 ProcessLifecycleOwnerInitializer 是一个 ContentProvider。我们知道 ContentProvider 一般是在 AndroidManifest 中生命的。

果然,在 extensions-1.1.1.aar 中,我们惊喜地发现,它在 Manifest 里面注册了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.arch.lifecycle.extensions" >

<uses-sdk android:minSdkVersion="14" />

<application>
<provider
android:name="android.arch.lifecycle.ProcessLifecycleOwnerInitializer"
android:authorities="${applicationId}.lifecycle-trojan"
android:exported="false"
android:multiprocess="true" />
</application>

</manifest>

而 ContentProvider 的 onCreate 方法优先于 Application 的 onCreate 执行,所以在 Application 之前我们就调用了 ProcessLifecycleOwnerInitializer init 方法,监听了 Activity 的创建,当 Actiivty 创建的时候,会尝试为 Activity 添加 ReportFragment。而 ReportFragment 会在 Activity 生命周期变化的时候帮助我们分发生命周期。

ContentProvider 的 onCreate 方法优先于 Application 的 onCreate 执行,可以查看这一篇博客 Android系统中的Application和四大组件一些方法的启动顺序和一些坑


总结

ok,我们来梳理一下。

对于 26.1.0 以后的 SupportActivity

它在 Activity onCreate 的时候添加了 ReportFragment,这个 ReportFragment 相当于一个代理,它在 onActivityCreated 的时候 dispatch(Lifecycle.Event.ON_CREATE) 进行分发生命周期,onStart, onResume, onPause, onStop, onDestroy 的时候也是如此。而 在 dispatch 中 它调用了 LifecycleRegistry handleLifecycleEvent 的方法。而 LifecycleRegistry 方法中经过一系列处理,它又调用了 observer 的 onStateChange 方法,去通知相应的 observer。

对于普通的 Activity

它利用了 ContentProvide 的特征,它是在 Application onCreate 之前初始化的,他在 ProcessLifecycleOwnerInitializer oncreate 的时候监听 Activity 的创建,在 Activity 创建的时候,判断是否已经添加过 ReportFragment,没有的话,添加进去。这是一个很巧妙的设计,隐式初始化了 lifecycle。

用流程图表示如下:

该图片引用自 Android 架构组件(一)——Lifecycle

Lifecycle 设计借鉴

  1. 利用 ProcessLifecycleOwnerInitializer contentProvider 来隐式加载

想一下,如果 ProcessLifecycleOwnerInitializer 不利用 contentProvider 来隐式加载的话,对于 普通的 Activity,旧版本等,如果想使用 lifecycle,那必须在基类中,手动调用 ReportFragment.injectIfNeededIn(activity) 的方法。

  1. 利用 fragment 来分发生命周期

利用 fragment 来分发生命周期有两个优点

  • 将逻辑从 Activity 中剥离出来,减少耦合,方便复用
  • 可以做到在 Activity onCreate 之后才回调 observer 的 CREATED Event 事件。如果是通过 Application registerActivityLifecycleCallbacks 方法来分发生命周期的话,因为 ActivityLifecycleCallbacks 的 onActivityCreated 是在 Activity oncreate 之前调用的。

下一篇:Android livedata 源码解剖

推荐阅读:

java 代理模式详解
观察者设计模式 Vs 事件委托(java)
Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult

说在前面

本次推出 Android Architecture Components 系列文章,目前写好了四篇,主要是关于 lifecycle,livedata 的使用和源码分析,其余的 Navigation, Paging library,Room,WorkMannager 等春节结束之后会更新,欢迎关注我的公众号,有更新的话会第一时间会在公众号上面通知。

Android lifecycle 使用详解

Android LiveData 使用详解

Android lifecyle 源码解剖

Android livedata 源码解剖

github sample 地址: ArchiteComponentsSample

在这里插入图片描述

Architecture Components

lifecycle 是 2107 年 google 大会推出来的,它属于 architecture compoment 里面的一个组件,它可以干什么用呢? 简单得来说,它可以用来检查 Activity 的生命周期,而不必强依赖 Activity。


为什么要引进 lifecycle

举一下我们最常用的 MVP 例子,没引进 lifecycle 之前,我们需要在 Activity 或者 Fragment 销毁的时候,即 onDestroy 的时候手动调用 onDestroy 方法,这里会带来一些问题,每一次在 Activity 或者 Fragment 销毁的烧开后都要调用 presenter.destory() 方法,这样的代码枯燥,毫无意义。

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
class MyPresenter{
public MyPresenter() {
}

void create() {
//do something
}

void destroy() {
//do something
}
}

class MyActivity extends AppCompatActivity {
private MyPresenter presenter;

public void onCreate(...) {
presenter= new MyPresenter ();
presenter.create();
}

public void onDestroy() {
super.onDestroy();
presenter.destory();
}
}

当然我们也可以定义一些 IBasePresenter 的接口,在 BaseActivity 的时候调用 IBasePresenter 的 onDestroy 方法,这样也确实能做到。只不过稍微繁琐一点。

那如果是别的类的呢,比如 MediaCompoment,在 Activity 的时候,我们需要销毁一些资源,按照传统的方法,我们还是需要在 Activity onDestroy 的时候手动调用 onDestroy 方法。那有没有更好的方法呢?当然是有的,lifecycle 就可以解决这个问题。接下来,我们先来看一下 Lifycycle 的基本使用。


Lifycycle 的基本使用

  1. 引入相关的依赖包

Lifecycle 已经是稳定版,它包含在 support library 26.1.0 及之后的依赖包中,如果我们的项目基于这些依赖包,那么不需要额外的引用。

1
2
3
4
5
6
// ViewModel and LiveData
implementation "android.arch.lifecycle:extensions:1.1.0"
// alternatively, just ViewModel
implementation "android.arch.lifecycle:viewmodel:1.1.0"
// alternatively, just LiveData
implementation "android.arch.lifecycle:livedata:1.1.0"

support library在26.1.0 之前,lifecycle 并没有集成进去,需要我们引入另外的包。

1
implementation "android.arch.lifecycle:extensions:1.0.0-alpha4"
  1. 使用

这里同样分为几种情况

  1. support library 26.1.0 之后,且继承 FragmentActivity,那么我们直接调用 getLifecycle().addObserver 方法即可,当 Activity 的生命周期变化的时候,将会回调 onStateChanged 的方法,状态分别是一一对应的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";

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

getLifecycle().addObserver(new GenericLifecycleObserver() {

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
Log.d(TAG, "onStateChanged: event =" + event);
}
});
}

}
  1. support library 26.1.0 之后,不是继承 FragmentActivity,只是简单地继承 Actiivty
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
public class SimpleLifecycleActivity extends Activity implements LifecycleOwner {

private static final String TAG = "SimpleLifecycleActivity";
private LifecycleRegistry mLifecycleRegistry;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_lifecycle);
mLifecycleRegistry = new LifecycleRegistry(this);
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
getLifecycle().addObserver(new GenericLifecycleObserver() {

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
Log.d(TAG, "onStateChanged: event =" + event);
}
});
}

@Override
protected void onStart() {
super.onStart();
mLifecycleRegistry.markState(Lifecycle.State.STARTED);
}

@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}

}

  1. support library 26.1.0 之前

(现在的 support library 基本都在 26.1.0 之后了,这个可以忽略)

第一步:实现 LifecycleOwner 接口,并返回响应的 Lifecycle

1
2
3
4
5
6
7
8
9
public interface LifecycleOwner {
/**
* Returns the Lifecycle of the provider.
*
* @return The lifecycle of the provider.
*/
@NonNull
Lifecycle getLifecycle();
}

第二步:在 Activity 生命周期变化的时候,调用 mLifecycleRegistry.handleLifecycleEvent 方法,分发相应的生命周期。

第三步:调用 Lifecycle 的 addObserver 方法添加相应的 Observer。

代码如下

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

private LifecycleRegistry mRegistry;

private static final String TAG = "MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRegistry = new LifecycleRegistry(this);
mRegistry.markState(Lifecycle.State.CREATED);
getLifecycle().addObserver(new GenericLifecycleObserver() {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
Log.d(TAG, "onStateChanged:event =" + event);
}

@Override
public Object getReceiver() {
return null;
}
});

}

@Override
protected void onStart() {
super.onStart();
mRegistry.markState(Lifecycle.State.STARTED);
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
}

@Override
protected void onResume() {
super.onResume();
mRegistry.markState(Lifecycle.State.RESUMED);
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
}

@Override
protected void onPause() {
super.onPause();
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
}

@Override
protected void onStop() {
super.onStop();
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
}

@Override
protected void onDestroy() {
super.onDestroy();
mRegistry.markState(Lifecycle.State.DESTROYED);
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}

@Override
public Lifecycle getLifecycle() {
return mRegistry;
}
}


总结

我们回过头来看一下我们上面提出的问题?

MediaCompoment 在 Activity ondestroy 的时候,我们需要销毁一些资源,用传统的方法,我们需要在 Activity onDestroy 的时候手动调用 onDestroy 方法。这样会存在一个问题,调用者必须知道比较清楚得知道 MediaCompoment 的设计,否则可能会忘记调用 onDestroy 的方法。

那有没有一种方法, 当 Activity 生命周期变化的时候,MediaCompoment 自身能够检测到 Activity 的 生命周期变化,从而做相应的处理。

答案当然是有的,使用 lifycycle。

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
public class MediaCompoment {
private static final String TAG = "MediaCompoment";

private final LifecycleOwner mLifecycleOwner;

public MediaCompoment(LifecycleOwner lifecycleOwner) {
mLifecycleOwner = lifecycleOwner;
mLifecycleOwner.getLifecycle().addObserver(new GenericLifecycleObserver() {
@Override
public void onStateChanged(LifecycleOwner source, final Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_CREATE) {
onCreate();
} else if (event == Lifecycle.Event.ON_START) {
onStart();
} else if (event == Lifecycle.Event.ON_RESUME) {
onResume();
} else if (event == Lifecycle.Event.ON_PAUSE) {
onPause();
} else if (event == Lifecycle.Event.ON_STOP) {
onStop();
} else if (event == Lifecycle.Event.ON_DESTROY) {
onDestroy();
}
}
});
}

public void onCreate() {
Log.d(TAG, "onCreate:");
}

public void onStart() {
Log.d(TAG, "onStart:");
}

public void onResume() {
Log.d(TAG, "onResume:");
}

public void onPause() {
Log.d(TAG, "onPause:");
}

public void onStop() {
Log.d(TAG, "onStop:");
}

public void onDestroy() {
Log.d(TAG, "onDestroy:");
}
}

小结:

  1. lifycycle 其实是用观察者模式实现的,当 Activity 生命周期变化的时候,通知相应的 Observers 即观察者。
  2. 使用 lifecycle,我们可以将释放资源的动作内聚在自身,减少与调用者之间的耦合。

下一篇博客:Android LiveData 使用详解

推荐阅读

Android 启动优化(一) - 有向无环图

Android 启动优化(二) - 拓扑排序的原理以及解题思路

Android 启动优化(三)- AnchorTask 开源了

Android 启动优化(四)- AnchorTask 是怎么实现的

Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

Android 启动优化(六)- 深入理解布局优化

这几篇文章从 0 到 1,讲解 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的应用。

推荐理由:现在挺多文章一谈到启动优化,动不动就聊拓扑结构,这篇文章从数据结构到算法、到设计都给大家说清楚了,开源项目也有非常强的借鉴意义。

在这里插入图片描述

简介

什么是 Hook

Hook 又叫“钩子”,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。

这样当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)。

Hook 分类

1.根据Android开发模式,Native模式(C/C++)和Java模式(Java)区分,在Android平台上

  • Java层级的Hook;
  • Native层级的Hook;

2.根 Hook 对象与 Hook 后处理事件方式不同,Hook还分为:

  • 消息Hook;
  • API Hook;

3.针对Hook的不同进程上来说,还可以分为:

  • 全局Hook;
  • 单个进程Hook;

常见 Hook 框架

在Android开发中,有以下常见的一些Hook框架:

  1. Xposed

通过替换 /system/bin/app_process 程序控制 Zygote 进程,使得 app_process 在启动过程中会加载 XposedBridge.jar 这个 Jar 包,从而完成对 Zygote 进程及其创建的 Dalvik 虚拟机的劫持。

Xposed 在开机的时候完成对所有的 Hook Function 的劫持,在原 Function 执行的前后加上自定义代码。

  1. Cydia Substrate

Cydia Substrate 框架为苹果用户提供了越狱相关的服务框架,当然也推出了 Android 版 。Cydia Substrate 是一个代码修改平台,它可以修改任何进程的代码。

不管是用 Java 还是 C/C++(native代码)编写的,而 Xposed 只支持 Hook app_process 中的 Java 函数。

  1. Legend

Legend 是 Android 免 Root 环境下的一个 Apk Hook 框架,该框架代码设计简洁,通用性高,适合逆向工程时一些 Hook 场景。大部分的功能都放到了 Java 层,这样的兼容性就非常好。

原理是这样的,直接构造出新旧方法对应的虚拟机数据结构,然后替换信息写到内存中即可。

Hook 必须掌握的知识

  • 反射

如果你对反射还不是很熟悉的话,建议你先复习一下 java 反射的相关知识。有兴趣的,可以看一下我的这一篇博客 Java 反射机制详解

  • java 的动态代理

动态代理是指在运行时动态生成代理类,不需要我们像静态代理那个去手动写一个个的代理类。在 java 中,我们可以使用 InvocationHandler 实现动态代理,有兴趣的,可以查看我的这一篇博客 java 代理模式详解

本文的主要内容是讲解单个进程的 Hook,以及怎样 Hook。有兴趣的可以关注我的微信公众号:程序员徐公
在这里插入图片描述


Hook 使用实例

Hook 选择的关键点

  • Hook 的选择点:尽量静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。

  • Hook 过程:

    • 寻找 Hook 点,原则是尽量静态变量或者单例对象,尽量 Hook public 的对象和方法。
    • 选择合适的代理方式,如果是接口可以用动态代理。
    • 偷梁换柱——用代理对象替换原始对象。
  • Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

简单案例一: 使用 Hook 修改 View.OnClickListener 事件

首先,我们先分析 View.setOnClickListener 源码,找出合适的 Hook 点。可以看到 OnClickListener 对象被保存在了一个叫做 ListenerInfo 的内部类里,其中 mListenerInfo 是 View 的成员变量。ListeneInfo 里面保存了 View 的各种监听事件。因此,我们可以想办法 hook ListenerInfo 的 mOnClickListener 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

static class ListenerInfo {

---

ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}

---
}

接下来,让我们一起来看一下怎样 Hook View.OnClickListener 事件?

大概分为三步:

  • 第一步:获取 ListenerInfo 对象

从 View 的源代码,我们可以知道我们可以通过 getListenerInfo 方法获取,于是,我们利用反射得到 ListenerInfo 对象

  • 第二步:获取原始的 OnClickListener事件方法

从上面的分析,我们知道 OnClickListener 事件被保存在 ListenerInfo 里面,同理我们利用反射获取

  • 第三步:偷梁换柱,用 Hook代理类 替换原始的 OnClickListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void hookOnClickListener(View view) throws Exception {
// 第一步:反射得到 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
// 第二步:得到原始的 OnClickListener事件方法
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
mOnClickListener.setAccessible(true);
View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
// 第三步:用 Hook代理类 替换原始的 OnClickListener
View.OnClickListener hookedOnClickListener = new HookedClickListenerProxy(originOnClickListener);
mOnClickListener.set(listenerInfo, hookedOnClickListener);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HookedClickListenerProxy implements View.OnClickListener {

private View.OnClickListener origin;

public HookedClickListenerProxy(View.OnClickListener origin) {
this.origin = origin;
}

@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "Hook Click Listener", Toast.LENGTH_SHORT).show();
if (origin != null) {
origin.onClick(v);
}
}

}

执行以下代码,将会看到当我们点击该按钮的时候,会弹出 toast “Hook Click Listener”

1
2
3
4
5
6
7
mBtn1 = (Button) findViewById(R.id.btn_1);
mBtn1.setOnClickListener(this);
try {
HookHelper.hookOnClickListener(mBtn1);
} catch (Exception e) {
e.printStackTrace();
}

简单案例二: HooK Notification

发送消息到通知栏的核心代码如下:

1
2
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(id, builder.build());

跟踪 notify 方法发现最终会调用到 notifyAsUser 方法

1
2
3
4
5
public void notify(String tag, int id, Notification notification)
{
notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
}

而在 notifyAsUser 方法中,我们惊喜地发现 service 是一个单例,因此,我们可以想方法 hook 住这个 service,而 notifyAsUser 最终会调用到 service 的 enqueueNotificationWithTag 方法。因此 hook 住 service 的 enqueueNotificationWithTag 方法即可

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
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
//
INotificationManager service = getService();
String pkg = mContext.getPackageName();
// Fix the notification as best we can.
Notification.addFieldsFromContext(mContext, notification);
if (notification.sound != null) {
notification.sound = notification.sound.getCanonicalUri();
if (StrictMode.vmFileUriExposureEnabled()) {
notification.sound.checkFileUriExposed("Notification.sound");
}
}
fixLegacySmallIcon(notification, pkg);
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (notification.getSmallIcon() == null) {
throw new IllegalArgumentException("Invalid notification (no valid small icon): "
+ notification);
}
}
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

private static INotificationManager sService;

static public INotificationManager getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService("notification");
sService = INotificationManager.Stub.asInterface(b);
return sService;
}

综上,要 Hook Notification,大概需要三步:

  • 第一步:得到 NotificationManager 的 sService
  • 第二步:因为 sService 是接口,所以我们可以使用动态代理,获取动态代理对象
  • 第三步:偷梁换柱,使用动态代理对象 proxyNotiMng 替换系统的 sService

于是,我们可以写出如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

public static void hookNotificationManager(final Context context) throws Exception {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

Method getService = NotificationManager.class.getDeclaredMethod("getService");
getService.setAccessible(true);
// 第一步:得到系统的 sService
final Object sOriginService = getService.invoke(notificationManager);

Class iNotiMngClz = Class.forName("android.app.INotificationManager");
// 第二步:得到我们的动态代理对象
Object proxyNotiMng = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
Class[]{iNotiMngClz}, new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG, "invoke(). method:" + method);
String name = method.getName();
Log.d(TAG, "invoke: name=" + name);
if (args != null && args.length > 0) {
for (Object arg : args) {
Log.d(TAG, "invoke: arg=" + arg);
}
}
Toast.makeText(context.getApplicationContext(), "检测到有人发通知了", Toast.LENGTH_SHORT).show();
// 操作交由 sOriginService 处理,不拦截通知
return method.invoke(sOriginService, args);
// 拦截通知,什么也不做
// return null;
// 或者是根据通知的 Tag 和 ID 进行筛选
}
});
// 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 sService
Field sServiceField = NotificationManager.class.getDeclaredField("sService");
sServiceField.setAccessible(true);
sServiceField.set(notificationManager, proxyNotiMng);

}



Hook 使用进阶

Hook ClipboardManager

第一种方法

从上面的 hook NotificationManager 例子中,我们可以得知 NotificationManager 中有一个静态变量 sService,这个变量是远端的 service。因此,我们尝试查找 ClipboardManager 中是不是也存在相同的类似静态变量。

查看它的源码发现它存在 mService 变量,该变量是在 ClipboardManager 构造函数中初始化的,而 ClipboardManager 的构造方法用 @hide 标记,表明该方法对调用者不可见。

而我们知道 ClipboardManager,NotificationManager 其实这些都是单例的,即系统只会创建一次。因此我们也可以认为
ClipboardManager 的 mService 是单例的。因此 mService 应该是可以考虑 hook 的一个点。

1
2
3
4
5
6
7
8
9
10
11
public class ClipboardManager extends android.text.ClipboardManager {
private final Context mContext;
private final IClipboard mService;

/** {@hide} */
public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException {
mContext = context;
mService = IClipboard.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
}
}

接下来,我们再来一个看一下 ClipboardManager 的相关方法 setPrimaryClip , getPrimaryClip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void setPrimaryClip(ClipData clip) {
try {
if (clip != null) {
clip.prepareToLeaveProcess(true);
}
mService.setPrimaryClip(clip, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

/**
* Returns the current primary clip on the clipboard.
*/
public ClipData getPrimaryClip() {
try {
return mService.getPrimaryClip(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

可以发现这些方法最终都会调用到 mService 的相关方法。因此,ClipboardManager 的 mService 确实是一个可以 hook 的一个点。

hook ClipboardManager.mService 的实现

大概需要三个步骤

  • 第一步:得到 ClipboardManager 的 mService
  • 第二步:初始化动态代理对象
  • 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
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
public static void hookClipboardService(final Context context) throws Exception {
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
Field mServiceFiled = ClipboardManager.class.getDeclaredField("mService");
mServiceFiled.setAccessible(true);
// 第一步:得到系统的 mService
final Object mService = mServiceFiled.get(clipboardManager);

// 第二步:初始化动态代理对象
Class aClass = Class.forName("android.content.IClipboard");
Object proxyInstance = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
Class[]{aClass}, new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG, "invoke(). method:" + method);
String name = method.getName();
if (args != null && args.length > 0) {
for (Object arg : args) {
Log.d(TAG, "invoke: arg=" + arg);
}
}
if ("setPrimaryClip".equals(name)) {
Object arg = args[0];
if (arg instanceof ClipData) {
ClipData clipData = (ClipData) arg;
int itemCount = clipData.getItemCount();
for (int i = 0; i < itemCount; i++) {
ClipData.Item item = clipData.getItemAt(i);
Log.i(TAG, "invoke: item=" + item);
}
}
Toast.makeText(context, "检测到有人设置粘贴板内容", Toast.LENGTH_SHORT).show();
} else if ("getPrimaryClip".equals(name)) {
Toast.makeText(context, "检测到有人要获取粘贴板的内容", Toast.LENGTH_SHORT).show();
}
// 操作交由 sOriginService 处理,不拦截通知
return method.invoke(mService, args);

}
});

// 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
Field sServiceField = ClipboardManager.class.getDeclaredField("mService");
sServiceField.setAccessible(true);
sServiceField.set(clipboardManager, proxyInstance);

}


第二种方法

对 Android 源码有基本了解的人都知道,Android 中的各种 Manager 都是通过 ServiceManager 获取的。因此,我们可以通过 ServiceManager hook 所有系统 Manager,ClipboardManager 当然也不例外。

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


/**
* Returns a reference to a service with the given name.
*
* @param name the name of the service to get
* @return a reference to the service, or <code>null</code> if the service doesn't exist
*/
public static IBinder getService(String name) {
try {
IBinder service = sCache.get(name);
if (service != null) {
return service;
} else {
return getIServiceManager().getService(name);
}
} catch (RemoteException e) {
Log.e(TAG, "error in getService", e);
}
return null;
}
}

老套路

  • 第一步:通过反射获取剪切板服务的远程Binder对象,这里我们可以通过 ServiceManager getService 方法获得
  • 第二步:创建我们的动态代理对象,动态代理原来的Binder对象
  • 第三步:偷梁换柱,把我们的动态代理对象设置进去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void hookClipboardService() throws Exception {

//通过反射获取剪切板服务的远程Binder对象
Class serviceManager = Class.forName("android.os.ServiceManager");
Method getServiceMethod = serviceManager.getMethod("getService", String.class);
IBinder remoteBinder = (IBinder) getServiceMethod.invoke(null, Context.CLIPBOARD_SERVICE);

//新建一个我们需要的Binder,动态代理原来的Binder对象
IBinder hookBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
new Class[]{IBinder.class}, new ClipboardHookRemoteBinderHandler(remoteBinder));

//通过反射获取ServiceManger存储Binder对象的缓存集合,把我们新建的代理Binder放进缓存
Field sCacheField = serviceManager.getDeclaredField("sCache");
sCacheField.setAccessible(true);
Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
sCache.put(Context.CLIPBOARD_SERVICE, hookBinder);

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ClipboardHookRemoteBinderHandler implements InvocationHandler {

private IBinder remoteBinder;
private Class iInterface;
private Class stubClass;

public ClipboardHookRemoteBinderHandler(IBinder remoteBinder) {
this.remoteBinder = remoteBinder;
try {
this.iInterface = Class.forName("android.content.IClipboard");
this.stubClass = Class.forName("android.content.IClipboard$Stub");
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("RemoteBinderHandler", method.getName() + "() is invoked");
if ("queryLocalInterface".equals(method.getName())) {
//这里不能拦截具体的服务的方法,因为这是一个远程的Binder,还没有转化为本地Binder对象
//所以先拦截我们所知的queryLocalInterface方法,返回一个本地Binder对象的代理
return Proxy.newProxyInstance(remoteBinder.getClass().getClassLoader(),
new Class[]{this.iInterface},
new ClipboardHookLocalBinderHandler(remoteBinder, stubClass));
}

return method.invoke(remoteBinder, args);
}
}

Hook Activity

关于怎样 hook activity,以及怎样启动没有在 AndroidManifet 注册的 activity,可以查看我的这一篇博客。

Android Hook Activity 的几种姿势

源码下载地址HookDemo


推荐阅读

Android 启动优化(一) - 有向无环图

Android 启动优化(二) - 拓扑排序的原理以及解题思路

Android 启动优化(三)- AnchorTask 开源了

Android 启动优化(四)- AnchorTask 是怎么实现的

Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

Android 启动优化(六)- 深入理解布局优化

这几篇文章从 0 到 1,讲解 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的应用。

推荐理由:现在挺多文章一谈到启动优化,动不动就聊拓扑结构,这篇文章从数据结构到算法、到设计都给大家说清楚了,开源项目也有非常强的借鉴意义。

在这里插入图片描述

使用CoordinatorLayout打造各种炫酷的效果

自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示

NestedScrolling 机制深入解析

一步步带你读懂 CoordinatorLayout 源码

自定义 Behavior -仿新浪微博发现页的实现

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页

重磅消息:小编我开始运营自己的公众号了, 目前从事于 Android 开发,除了分享 Android开发相关知识,还有职场心得,面试经验,学习心得,人生感悟等等。希望通过该公众号,让你看到程序猿不一样的一面,我们不只会敲代码,我们还会。。。。。。

有兴趣的话可以关注我的公众号 徐公码字(stormjun94),或者拿起你的手机扫一扫,期待你的参与

Android 技术人

效果图

我们先来看一下新浪微博发现页的效果:

接下来我们在来看一下我们仿照新浪微博实现的效果

仿新浪微博效果图

实现思路分析

我们这里先定义两种状态,open 和 close 状态。

  • open 状态指 Tab+ViewPager 还没有滑动到顶部的时候,header 还 没有被完全移除屏幕的时候
  • close 状态指 Tab+ViewPager 滑动到顶部的时候,Header 被移除屏幕的时候

从效果图,我们可以看到 在 open 状态下,我们向上滑动 ViewPager 里面的 RecyclerView 的 时候,RecyclerView 并不会向上移动(RecyclerView 的滑动事件交给 外部的容器处理,被被全部消费掉了),而是整个布局(指 Header + Tab +ViewPager)会向上偏移 。当 Tab 滑动到顶部的时候,我们向上滑动 ViewPager 里面的 RecyclerView 的时候,RecyclerView 可以正常向上滑动,即此时外部容器没有拦截滑动事件

同时我们可以看到在 open 状态的时候,我们是不支持下拉刷新的,这个比较容易实现,监听页面的状态,如果是 open 状态,我们设置 SwipeRefreshLayout setEnabled 为 false,这样不会 拦截事件,在页面 close 的时候,设置 SwipeRefreshLayout setEnabled 为 TRUE,这样就可以支持下拉刷新了。

基于上面的分析,我们这里可以把整个效果划分为两个部分,第一部分为 Header,第二部分为 Tab+ViewPager。下文统一把第一部分称为 Header,第二部分称为 Content 。

需要实现的效果为:在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件。

在上一篇博客 一步步带你读懂 CoordinatorLayout 源码 中,我们有提到在 CoordinatorLayout中,我们可以通过 给子 View 自定义 Behavior 来处理事件。它是一个容器,实现了 NestedScrollingParent 接口。它并不会直接处理事件,而是会尽可能地交给子 View 的 Behavior 进行处理。因此,为了减少依赖,我们把这两部分的关系定义为 Content 依赖于 Header。Header 移动的时候,Content 跟着 移动。所以,我们在处理滑动事件的时候,只需要处理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不需要处理滑动事件,只需依赖于 Header ,跟着做相应的移动即可。


Header 部分的实现

Header 部分实现的两个关键点在于

  1. 在页面状态为 open 的时候,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件
  2. 在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

第一个关键点的实现

这里区分页面状态是 open 还是 close 状态是通过 Header 是否移除屏幕来区分的,即 child.getTranslationY() == getHeaderOffsetRange() 。

1
2
3
4
5
private boolean isClosed(View child) {
boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
return isClosed;
}

NestedScrolling 机制深入解析博客中,我们对 NestedScrolling 机制做了如下的总结。

  • 在 Action_Down 的时候,Scrolling child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法。
  • 在 Action_move 的时候,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 Child 一起进行滑动
  • 当 ScrollingChild 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Parent 的 onNestedScroll 方法
  • 在 Action_down,Action_move 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 ChildHelper 询问 Scrolling parent 的 stopNestedScroll 方法。
  • 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 Action_up 的时候,调用 dispatchNestedPreFling 方法,通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作
    在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法

而 RecyclerView 也是 Scrolling Child (实现了 NestedScrollingChild 接口),RecyclerView 在开始滑动的 时候会先调用 CoordinatorLayout 的 startNestedScroll 方法,而 CoordinatorLayout 会 调用子 View 的 Behavior 的 startNestedScroll 方法。并且只有 boolean startNestedScroll 返回 TRUE 的 时候,才会调用接下里 Behavior 中的 onNestedPreScroll 和 onNestedScroll 方法。

所以,我们在 WeiboHeaderPagerBehavior 的 onStartNestedScroll 方法可以这样写,可以确保 只拦截垂直方向上的滚动事件,且当前状态是打开的并且还可以继续向上收缩的时候还会拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
directTargetChild, View target, int nestedScrollAxes) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);
}

boolean canScroll = canScroll(child, 0);
//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
!isClosed(child);

}


拦截事件之后,我们需要在 RecyclerView 滑动之前消耗事件,并且移动 Header,让其向上偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
//dy>0 scroll up;dy<0,scroll down
Log.i(TAG, "onNestedPreScroll: dy=" + dy);
float halfOfDis = dy;
// 不能滑动了,直接给 Header 设置 终值,防止出错
if (!canScroll(child, halfOfDis)) {
child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);
} else {
child.setTranslationY(child.getTranslationY() - halfOfDis);
}
//consumed all scroll behavior after we started Nested Scrolling
consumed[1] = dy;
}


当然,我们也需要处理 Fling 事件,在页面没有完全关闭的 时候,消费所有 fling 事件。

1
2
3
4
5
6
7
8
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
// consumed the flinging behavior until Closed
return !isClosed(child);
}


至于滑动到顶部的动画,我是通过 mOverScroller + FlingRunnable 来实现的 。完整代码如下。

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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
public class WeiboHeaderPagerBehavior extends ViewOffsetBehavior {
private static final String TAG = "UcNewsHeaderPager";
public static final int STATE_OPENED = 0;
public static final int STATE_CLOSED = 1;
public static final int DURATION_SHORT = 300;
public static final int DURATION_LONG = 600;

private int mCurState = STATE_OPENED;
private OnPagerStateListener mPagerStateListener;

private OverScroller mOverScroller;

private WeakReference<CoordinatorLayout> mParent;
private WeakReference<View> mChild;

public void setPagerStateListener(OnPagerStateListener pagerStateListener) {
mPagerStateListener = pagerStateListener;
}

public WeiboHeaderPagerBehavior() {
init();
}

public WeiboHeaderPagerBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
mOverScroller = new OverScroller(BaseAPP.getAppContext());
}

@Override
protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
super.layoutChild(parent, child, layoutDirection);
mParent = new WeakReference<CoordinatorLayout>(parent);
mChild = new WeakReference<View>(child);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
directTargetChild, View target, int nestedScrollAxes) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);
}

boolean canScroll = canScroll(child, 0);
//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
!isClosed(child);

}

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
// consumed the flinging behavior until Closed

boolean coumsed = !isClosed(child);
Log.i(TAG, "onNestedPreFling: coumsed=" +coumsed);
return coumsed;
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
Log.i(TAG, "onNestedFling: velocityY=" +velocityY);
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY,
consumed);

}

private boolean isClosed(View child) {
boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
return isClosed;
}

public boolean isClosed() {
return mCurState == STATE_CLOSED;
}

private void changeState(int newState) {
if (mCurState != newState) {
mCurState = newState;
if (mCurState == STATE_OPENED) {
if (mPagerStateListener != null) {
mPagerStateListener.onPagerOpened();
}

} else {
if (mPagerStateListener != null) {
mPagerStateListener.onPagerClosed();
}

}
}

}

// 表示 Header TransLationY 的值是否达到我们指定的阀值, headerOffsetRange,到达了,返回 false,
// 否则,返回 true。注意 TransLationY 是负数。
private boolean canScroll(View child, float pendingDy) {
int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);
int headerOffsetRange = getHeaderOffsetRange();
if (pendingTranslationY >= headerOffsetRange && pendingTranslationY <= 0) {
return true;
}
return false;
}



@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent
ev) {

boolean closed = isClosed();
Log.i(TAG, "onInterceptTouchEvent: closed=" + closed);
if (ev.getAction() == MotionEvent.ACTION_UP && !closed) {
handleActionUp(parent,child);
}

return super.onInterceptTouchEvent(parent, child, ev);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
//dy>0 scroll up;dy<0,scroll down
Log.i(TAG, "onNestedPreScroll: dy=" + dy);
float halfOfDis = dy;
// 不能滑动了,直接给 Header 设置 终值,防止出错
if (!canScroll(child, halfOfDis)) {
child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);
} else {
child.setTranslationY(child.getTranslationY() - halfOfDis);
}
//consumed all scroll behavior after we started Nested Scrolling
consumed[1] = dy;
}

// 需要注意的是 Header 我们是通过 setTranslationY 来移出屏幕的,所以这个值是负数
private int getHeaderOffsetRange() {
return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
.weibo_header_offset);
}

private void handleActionUp(CoordinatorLayout parent, final View child) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "handleActionUp: ");
}
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(parent, child);
if (child.getTranslationY() < getHeaderOffsetRange() / 6.0f) {
mFlingRunnable.scrollToClosed(DURATION_SHORT);
} else {
mFlingRunnable.scrollToOpen(DURATION_SHORT);
}

}

private void onFlingFinished(CoordinatorLayout coordinatorLayout, View layout) {
changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);
}

public void openPager() {
openPager(DURATION_LONG);
}

/**
* @param duration open animation duration
*/
public void openPager(int duration) {
View child = mChild.get();
CoordinatorLayout parent = mParent.get();
if (isClosed() && child != null) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(parent, child);
mFlingRunnable.scrollToOpen(duration);
}
}

public void closePager() {
closePager(DURATION_LONG);
}

/**
* @param duration close animation duration
*/
public void closePager(int duration) {
View child = mChild.get();
CoordinatorLayout parent = mParent.get();
if (!isClosed()) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(parent, child);
mFlingRunnable.scrollToClosed(duration);
}
}

private FlingRunnable mFlingRunnable;

/**
* For animation , Why not use {@link android.view.ViewPropertyAnimator } to play animation
* is of the
* other {@link CoordinatorLayout.Behavior} that depend on this could not receiving the
* correct result of
* {@link View#getTranslationY()} after animation finished for whatever reason that i don't know
*/
private class FlingRunnable implements Runnable {
private final CoordinatorLayout mParent;
private final View mLayout;

FlingRunnable(CoordinatorLayout parent, View layout) {
mParent = parent;
mLayout = layout;
}

public void scrollToClosed(int duration) {
float curTranslationY = ViewCompat.getTranslationY(mLayout);
float dy = getHeaderOffsetRange() - curTranslationY;
if (BuildConfig.DEBUG) {
Log.d(TAG, "scrollToClosed:offest:" + getHeaderOffsetRange());
Log.d(TAG, "scrollToClosed: cur0:" + curTranslationY + ",end0:" + dy);
Log.d(TAG, "scrollToClosed: cur:" + Math.round(curTranslationY) + ",end:" + Math
.round(dy));
Log.d(TAG, "scrollToClosed: cur1:" + (int) (curTranslationY) + ",end:" + (int) dy);
}
mOverScroller.startScroll(0, Math.round(curTranslationY - 0.1f), 0, Math.round(dy +
0.1f), duration);
start();
}

public void scrollToOpen(int duration) {
float curTranslationY = ViewCompat.getTranslationY(mLayout);
mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY,
duration);
start();
}

private void start() {
if (mOverScroller.computeScrollOffset()) {
mFlingRunnable = new FlingRunnable(mParent, mLayout);
ViewCompat.postOnAnimation(mLayout, mFlingRunnable);
} else {
onFlingFinished(mParent, mLayout);
}
}

@Override
public void run() {
if (mLayout != null && mOverScroller != null) {
if (mOverScroller.computeScrollOffset()) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "run: " + mOverScroller.getCurrY());
}
ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());
ViewCompat.postOnAnimation(mLayout, this);
} else {
onFlingFinished(mParent, mLayout);
}
}
}
}

/**
* callback for HeaderPager 's state
*/
public interface OnPagerStateListener {
/**
* do callback when pager closed
*/
void onPagerClosed();

/**
* do callback when pager opened
*/
void onPagerOpened();
}

}

第二个关键点的实现

在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

在第一个关键点的实现上,我们是通过自定义 Behavior 来处理 ViewPager 里面 RecyclerView 的移动的,那我们要怎样监听整个 Header 的滑动了。

那就是重写 LinearLayout,将滑动事件交给 ScrollingParent(这里是CoordinatorLayout) 去处理,CoordinatorLayout 再交给子 View 的 behavior 去处理。

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
public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild {

private static final String TAG = "NestedLinearLayout";

private final int[] offset = new int[2];
private final int[] consumed = new int[2];

private NestedScrollingChildHelper mScrollingChildHelper;
private int lastY;

public NestedLinearLayout(Context context) {
this(context, null);
}

public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public NestedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
}

private void initData() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
mScrollingChildHelper.setNestedScrollingEnabled(true);
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastY = (int) event.getRawY();
// 当开始滑动的时候,告诉父view
startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
| ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:

return true;
}
return super.onInterceptTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent: ACTION_MOVE=");
int y = (int) (event.getRawY());
int dy =lastY- y;
lastY = y;
Log.i(TAG, "onTouchEvent: lastY=" + lastY);
Log.i(TAG, "onTouchEvent: dy=" + dy);
// dy < 0 下拉, dy>0 赏花
if (dy >0) { // 上滑的时候才交给父类去处理
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
&& dispatchNestedPreScroll(0, dy, consumed, offset)) {//
// 父类进行了一部分滚动
}
}else{
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
&& dispatchNestedScroll(0, 0, 0,dy, offset)) {//
// 父类进行了一部分滚动
}
}
break;
}
return true;
}



private NestedScrollingChildHelper getScrollingChildHelper() {
return mScrollingChildHelper;
}

// 接口实现--------------------------------------------------

@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}

@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}

@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}

@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}

@Override
public boolean hasNestedScrollingParent() {
return getScrollingChildHelper().hasNestedScrollingParent();
}

@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,
int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy,
consumed, offsetInWindow);
}

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY,
boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX,
velocityY, consumed);
}

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX,
velocityY);
}
}


Content 部分的实现

Content 部分的实现也主要有两个关键点

  • 整体置于 Header 之下
  • Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。

第一个关键点的实现

整体置于 Header 之下。这个我们可以参考 APPBarLayout 的 behavior,它是这样处理的。

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
/**
* Copy from Android design library
* <p/>
* Created by xujun
*/
public abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
private final Rect mTempRect1 = new Rect();
private final Rect mTempRect2 = new Rect();

private int mVerticalLayoutGap = 0;
private int mOverlayTop;

public HeaderScrollingViewBehavior() {
}

public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
// If the menu's height is set to match_parent/wrap_content then measure it
// with the maximum visible height

final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
if (ViewCompat.getFitsSystemWindows(header) && !ViewCompat.getFitsSystemWindows(child)) {
// If the header is fitting system windows then we need to also,
// otherwise we'll get CoL's compatible measuring
ViewCompat.setFitsSystemWindows(child, true);

if (ViewCompat.getFitsSystemWindows(child)) {
// If the set succeeded, trigger a new layout and return true
child.requestLayout();
return true;
}
}

if (ViewCompat.isLaidOut(header)) {
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}

final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST);

// Now measure the scrolling view with the correct height
parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);

return true;
}
}
}
return false;
}

@Override
protected void layoutChild(final CoordinatorLayout parent, final View child, final int layoutDirection) {
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);

if (header != null) {
final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = mTempRect1;
available.set(parent.getPaddingLeft() + lp.leftMargin, header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);

final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection);

final int overlap = getOverlapPixelsForOffset(header);

child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
mVerticalLayoutGap = out.top - header.getBottom();
} else {
// If we don't have a dependency, let super handle it
super.layoutChild(parent, child, layoutDirection);
mVerticalLayoutGap = 0;
}
}

float getOverlapRatioForOffset(final View header) {
return 1f;
}

final int getOverlapPixelsForOffset(final View header) {
return mOverlayTop == 0
? 0
: MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),
0, mOverlayTop);

}

private static int resolveGravity(int gravity) {
return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
}

protected abstract View findFirstDependency(List<View> views);

protected int getScrollRange(View v) {
return v.getMeasuredHeight();
}

/**
* The gap between the top of the scrolling view and the bottom of the header layout in pixels.
*/
final int getVerticalLayoutGap() {
return mVerticalLayoutGap;
}

/**
* Set the distance that this view should overlap any {@link AppBarLayout}.
*
* @param overlayTop the distance in px
*/
public final void setOverlayTop(int overlayTop) {
mOverlayTop = overlayTop;
}

/**
* Returns the distance that this view should overlap any {@link AppBarLayout}.
*/
public final int getOverlayTop() {
return mOverlayTop;
}

}

这个基类的代码还是很好理解的,因为之前就说过了,正常来说被依赖的 View 会优先于依赖它的 View 处理,所以需要依赖的 View 可以在 measure/layout 的时候,找到依赖的 View 并获取到它的测量/布局的信息,这里的处理就是依靠着这种关系来实现的.

我们的实现类,需要重写的除了抽象方法 findFirstDependency 外,还需要重写 getScrollRange,我们把 Header
的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.

至于缩放的高度,根据 结果图 得知是 0,得出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int getFinalHeight() {
Resources resources = BaseAPP.getInstance().getResources();

return 0;
}

@Override
protected int getScrollRange(View v) {
if (isDependOn(v)) {
return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
} else {
return super.getScrollRange(v);
}
}

第二个关键点的实现:

Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。

主要的逻辑就是 在 layoutDependsOn 方法里面,判断 dependcy 是不是 HeaderView ,是的话,返回TRUE,这样在 Header 位置发生变化的时候,会回调 onDependentViewChanged 方法,在该方法里面,做相应的偏移。TranslationY 是根据比例算出来的 translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));

完整代码如下:

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
public class WeiboContentBehavior extends HeaderScrollingViewBehavior {
private static final String TAG = "WeiboContentBehavior";

public WeiboContentBehavior() {
}

public WeiboContentBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return isDependOn(dependency);
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onDependentViewChanged");
}
offsetChildAsNeeded(parent, child, dependency);
return false;
}

private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
float dependencyTranslationY = dependency.getTranslationY();
int translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) *
getScrollRange(dependency));
Log.i(TAG, "offsetChildAsNeeded: translationY=" + translationY);
child.setTranslationY(translationY);

}

@Override
protected View findFirstDependency(List<View> views) {
for (int i = 0, z = views.size(); i < z; i++) {
View view = views.get(i);
if (isDependOn(view)) return view;
}
return null;
}

@Override
protected int getScrollRange(View v) {
if (isDependOn(v)) {
return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
} else {
return super.getScrollRange(v);
}
}

private int getHeaderOffsetRange() {
return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
.weibo_header_offset);
}

private int getFinalHeight() {
Resources resources = BaseAPP.getInstance().getResources();

return 0;
}

private boolean isDependOn(View dependency) {
return dependency != null && dependency.getId() == R.id.id_weibo_header;
}
}


题外话

  • NestedScrolling 机制,对比传统的事件分发机制真的很强大。这种仿新浪微博发现页效果, 如果用传统的事件分发机制来做,估计很难实现,处理起来会有一大堆坑。
  • 看完了这种仿新浪微博发现页的效果,你是不是学到了什么?如果让你 模仿 仿 QQ 浏览器首页效果,你能实现话。

最后,特别感谢写这篇博客 自定义Behavior的艺术探索-仿UC浏览器主页 的开发者,没有这篇博客作为参考,这种效果我很大几率是实现 不了的。大家觉得效果还不错的话,顺手到 github 上面给我 star,谢谢。github 地址


参考文章:

自定义Behavior的艺术探索-仿UC浏览器主页

github 地址

最后的最后,卖一下广告,欢迎大家关注我的微信公众号 徐公码字,扫一扫下方二维码或搜索微信号 stormjun94,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。

文章首发地址CSDN:

岁月如水,时间飞逝,转眼间,已经到了年尾,即将引来新的一年,我要赶紧抓住16年的尾巴,写篇文章记录一下我16年的点点滴滴。篇章大概如下,学习&工作室篇,实习篇,盛夏六月, 博客篇,秋招篇,情感篇,展望未来。

学习&工作室篇

春节弹指一瞬间,转眼间已经到了正月18,迷恋着春节家人朋友团聚时候喜悦的气氛,我依依不舍地乘着大巴回到了学校,开始新的学期。在大学的时光里,没有想高三时光一样,三点一线。在大学里,多的是自由。每天,我往返于工作室和宿舍之间,那时每天只想着能争取多点时间 学习自己感兴趣的东西————也就是我现在所从事的职业Android开发。每天为了挤出一个多小时的时间来学习,尝试过中午不睡觉,坚持了两个多星期。结果是中午不睡,晚上崩溃———下午不睡觉,下午学习的时候还是精神蓬勃的,到了晚上,睡意就来了,经常打瞌睡。结果呢,相信你也猜到了,学习的东西反而少了,效率下降了好多。

原因,浅而明显,第一,一个人的精力是有限的,我们要注意劳逸结合;第二,以前我都是有午睡的习惯的,突然改变了习惯,肯定要有一段适应期。

至于说到劳逸结合,高中的时候就深有体会,大学写编程的时候更是深深刻在心里。有时候,写编程,在调bug的时候,在哪里捣鼓了几个小时,终究是被它折服了,被它弄得心浮气躁。这时候不烦放下手头的工作,出去走走,感受一下大自然,放松一下头脑,接着回来工作,许多时候你会发现bug一下子就解决了。这个时候你通常我会感慨,我擦,我是一个傻逼,这么简单的问题竟然弄了这么久,心里头不禁也涌上来一股满足感————那是一种付出辛苦努力而得到的满足。

有许多人说,写编程会让一个人性格变得烦躁。哈哈,有时候确实会,不过,有时候我更想说的是,写编程往往是我们变得更加耐心和细心。每一次我们在跟bug作斗争的时候,我们的耐心正在一点点培养。

许多人说程序员活像闷葫芦,钱多话少死得早。怎么说呢,这句话还是有一点道理的,首先钱多呢,这个就不必详讲了,相对大多数打工族来说,程序猿的工资相对来是还是比较高的。话少呢,确实也有一定的道理,因为我们整天面对的是电脑,比较少与人沟通交流,久而久之,语言表达能力肯定会退化不少的,有时候在与人交谈中,也不知道谁聊什么话题好,这就给了大家一种印象——话少。至于“死得早”,我们知道程序猿加班相对比较多,尤其是项目要上线的时候,经常会加班,而且工作强度相对来说有比较强。确实,如果你不注意锻炼的话,真的对身体伤害很大的。但只要你注意一下,每个星期坚持两三次锻炼,也是照样精神饱满的。

实习篇

说起实习的那段时间,那真的是一段艰辛岁月。每天实习完回到宿舍,有时候身心俱疲,根本就提不起精神来继续学习,我也因此颓废了一段时间,每天回到宿舍后,就开始看电影,看电视剧——后面我调整了自己的状态,在实习完回来的时候继续学习。

如果你问我那段时间累不累?我可以很肯定地告诉你,累成狗。但是我从未后悔过,因为一段岁月过得很充实,正如我们高三备考的那段岁月——只为心中的那一个目标。

盛夏六月

每天的五六月份,都是我们学校的毕业季。送走了一拨人,又即将引来新的一拨人,注入新的血液。在这段时间,对我感触最深的是,应该是我二哥和我社团的几个师兄和师姐牌毕业照的时候,他们说大学时光飞快,要好好珍惜接下来的大学时光。

有时候也在想,一年后的自己会是怎样的呢?

真的很感谢他们,曾今他们在我大学最迷茫的时候引导了我,为我指点迷津。

博客篇

我正式写博客的时候应该是四月底五月初的时候,像大多数人一样,刚开始写博客的时候完全没有思路,写出来的文章条理性差,访问量也很少。我记忆比较深的一篇博客是我在写这篇博客的时候:
二分查找的相关算法题 ,那时候些博客写到深夜12点多,就发布了出去,第二天醒来,访问量竟然超过一千了。一千的访问量对于经常写博客或者有一定知名度的博客来说,根本就是小菜一碟,算不了什么。但对于我之前几篇博客都是几十最多一百多的访问量的人来说,这无疑是意义非凡的。这意味着对我这篇博客质量的认可,正如我们付出的努力得到认可的喜悦一样。

为什么写博客呢

(当然我不是在说我写博客有多了不起,我只是在分享一下自己的经历而已———— 一些人或许会这样想,坚持写博客有什么了不起的,网上一大堆人在写博客,怎么不见他们在说写博客辛苦,或者吹捧。有一些腹黑的人更恶劣的,甚至会骂你,谴责你,不懂得谦虚,骄傲自大。

对于第一类人,分情况讨论一下,如果是那种整天无所事事的人,那我会嗤之以鼻,如果是那种很努力的人,整天辛苦奋斗的人,那我们写博客确实算不了什么。

而对于那些动不动就站在道德制高点的那些人,我真的不知道说写什么说。这种人我们真的不必跟他们太较真,较真你就输了。

总之,说了这么久,只想表达这样的意思,坚持走自己认为正确的路,世界那么大,让别人说去吧。

这里分享一个故事,是几天前发生的,是Android开发者 StormhZhang 的故事,故事概要是这样的,StromZhang 在他的公众号推送了一篇广告,结果有一些人就说他作为一名技术总监,还发广告,差不差这点钱,进而有提升到道德方面,有一些人更过分,甚至流言谩骂。殊不知他做技术分享帮助了多少人?这里就不过多介绍了,欲知详情,请自行搜索。

哈哈,扯蛋了这么久,终于来说我能够坚持下来写博客的原因呢?原因其实很简单,对于经常写博客的人,我相信他们都有一个共同点,写着写着就爱上博客了。即使说没有写博客,也喜欢用笔记将自己认为有价值的东西记录下来,just so
simple。当然,写博客有几个好处,锻炼自己的写作能力,提高自己的思维,更难能可贵的是,你能够在写博客的时候遇到一些志同道合的朋友。目前我还没有遇到,期待img,说一下我的一个经历,之前有一个技术疑问一直解决不了,后面在写相关博客的时候,在博客的最后提了出来,后面有热心的网友帮忙解答了,那时候真的很感动。

秋招篇

说起我的秋招之旅,可能对于身边的人来说,我相对是比较轻松的。可是对于许多大神来说,差的还不是一截半截。记得我经常说过一句话,比上不足,比下有余。

八月中旬的时候,秋招的号角正式吹响了。刚开始是一些BAT之类的公司内推,筛选简历或者笔试,很遗憾,我全部都没有通过。九月中旬的时候,BAT,网易,CVTE等这些知名企业开始校招了,很遗憾笔试也是全部没有通过。一方面是今年校招缩水了,招的人很少,一方面可能自己的笔试成绩也不是十分突出。那时候,心底是有点慌的,因为校招开始了半个多月,竟然一个笔试都没有通过,面试也没有。

于是,我自己独自一人来到腾讯面试的地方——喜来登大酒店,想去霸面。刚开始,想趁着他们在面试的时候跟着他们上去,可是还是被挡在电梯外面了。于是就去霸面区交了简历,后面想“趁水摸鱼” 坐上电梯,直接去找面试官,跟他说想霸面。可是还是被挡在第一外面了。于是就没有继续找机会坐上电梯去了。结果的最后,就是在里面空坐了一天,霸面fail,一天就这样 get over。其实,那时候如果真的下定决心要上去的话,机会还是很大的,等到有房可上去的时候,跟他们一起上去就好了。之所以当时没有那样做,可能自己还没有足够的信心。可是去之前是信心满满要去霸面的,可到现场遇到一点小阻碍却退却了,也许这就是我性格的一个弱点吧。

到了九月底的时候,也开始面试了,陆续收到了美图,久邦数码,步步高等公司的offer,最终签了美图公司,在十月初的时候也结束了我的秋招之旅。

在秋招,对于面试,我也没有一些很好的技巧。对于技术岗位的,我只能说三分口才,七分实力。对于搞技术的人,千万不要忽略语言表达能力方面的培养与提高,一方面在面试的时候你会吃很大亏,另一方面对你以后人生的发展也是很不利的。我在表达这方面就吃过挺多亏的,现在表达能力还是有待提高。

情感篇

说了这么久,来稍微说一点轻松一点的东西呢?那就是说情感方面的呢,其实我的情感篇真的没什么可说的,大学到现在也没谈过恋爱,可能是一直没有遇到合适的人吧,或者是我的情商有点低吧。谁说得清楚呢?

至于亲情方面,我想说的是,有空就多回家看看吧。对于父母来说,子女经常回家就是最好的礼物呢,比得多钱财万贯。

展望未来

旧的一年即将过去了,新的一年即将到来。在新的一年,大概有以下计划

  1. 在毕业前来一次说走你就走的旅行(不过到时候实习不知道有没有时间,尽量争取吧)
  2. CSDN争取申请到博客专家号,现在是准博客专家

截张图记录一下我现在博客的访问量

17年,即将到来的新的一年,希望家人朋友身体健康,实习,工作顺利。最后的最后,为了青春和热血,再次拼搏加油,致我的青春,青春万岁。

文章首发地址CSDN:

文章首发CSDN地址 :

闲聊

View,对我们来说在熟悉不过了,从接触Android开始,我们就一直在接触View,界面当中到处都是 View,比如我们经常用到的TextView,Button,LinearLayout等等,但是我们真的了解View吗?尤其是View的坐标。mLeft,mRight,mY,mX,mTranslationY,mScoollY,相对于屏幕的坐标等等这些概念你真的清楚了吗?如果真的清楚了,那你没有必要读这篇博客,如果你还是有一些模糊,建议花上几分钟的时间读一下。

为什么要写这一篇博客呢?

因为掌握View的坐标很重要,尤其是对于自定义View,学习动画有重大的意义。

这篇博客主要讲解一下问题

  • View 的 getLeft()和get Right()和 getTop() 和getBottom()
  • View 的 getY(), getTranslationY() 和 getTop() 之间的联系
  • View 的 getScroolY 和 View 的 scrollTo() 和 scrollBy()
  • event.getY 和 event.getRawY()
  • 扩展,怎样获取状态栏(StatusBar)和标题栏(titleBar)的高度

基本概念

简单说明一下(上图Activity采用默认Style,状态栏和标题栏都会显示):最大的草绿色区域是屏幕界面,红色次大区域我们称之为“应用界面区域”,最小紫色的区域我们称之为“View绘制区域”;屏幕顶端、应用界面区之外的那部分显示手机电池网络运营商信息的为“状态栏”,应用区域顶端、View绘制区外部显示Activity名称的部分我们称为“标题栏”。

从这张图片我们可以看到
在Android中,当ActionBar存在的情况下,

1
屏幕的 高度=状态栏+应用区域的高度=状态栏的 高度+(标题栏的 高度+View 绘制区域的高度)

当ActionBar不存在的情况下

1
屏幕的高度=状态栏+应用区域的高度=状态栏的 高度+(View 绘制区域的 高度)

View 的 getLeft()和getRight()和 getTop() 和getBottom()

1
2
3
4
View.getLeft() ;
View.getTop() ;
View.getBottom();
View.getRight() ;

top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标,都是相对于它的直接父View而言的,而不是相对于屏幕而言的。这一点要区分清楚。那那个坐标是相对于屏幕而言的呢,以及要怎样获取相对于屏幕的坐标呢?

目前View里面的变量还没有一个是相对于屏幕而言的,但是我们可以获取到相对于屏幕的坐标。一般来说,我们要获取View的坐标和高度 等,都必须等到View绘制完毕以后才能获取的到,在Activity 的 onCreate()方法 里面 是获取不到的,必须 等到View绘制完毕以后才能获取地到View的响应的坐标,一般来说,主要 有以下两种方法。

第一种方法,onWindowFocusChanged()方法里面进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   @Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//确保只会调用一次
if(first){
first=false;
final int[] location = new int[2];
mView.getLocationOnScreen(location);
int x1 = location[0] ;
int y1 = location[1] ;
Log.i(TAG, "onCreate: x1=" +x1);
Log.i(TAG, "onCreate: y1=" +y1);
}
}

第二种方法,在视图树绘制完成的时候进行测量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {

@Override
public void onGlobalLayout() {
// 移除监听器,确保只会调用一次,否则在视图树发挥改变的时候又会调用
mView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
final int[] location = new int[2];
mView.getLocationOnScreen(location);
int x1 = location[0];
int y1 = location[1];
Log.i(TAG, "onCreate: x1=" + x1);
Log.i(TAG, "onCreate: y1=" + y1);
}
});

View 的 getY(), getTranslationY() 和 getTop() 之间的联

getY()

Added in API level 14
The visual y position of this view, in pixels.(返回的是View视觉上的图标,即我们眼睛看到位置的Y坐标,注意也是相对于直接父View而言的默认值跟getTop()相同,别急,下面会解释)

getTranslationY()

Added in API level 14
The vertical position of this view relative to its top position, in pixels.(竖直方向上相对于top的偏移量,默认值为0)

那 getY() 和 getTranslationY() 和 getTop () 到底有什么关系呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
@ViewDebug.ExportedProperty(category = "drawing")
public float getY() {
return mTop + getTranslationY();
}

@ViewDebug.ExportedProperty(category = "drawing")
public float getTranslationY() {
return mRenderNode.getTranslationY();
}
@ViewDebug.CapturedViewProperty
public final int getTop() {
return mTop;
}

从以上的源码我们可以知道 getY()= getTranslationY()+ getTop (),而 getTranslationY() 的默认值是0,除非我们通过 setTranlationY() 来改变它,这也就是我们上面上到的 getY 默认值跟 getTop()相同

那我们要怎样改变 top值 和 Y 值呢? 很明显就是调用相应的set方法 ,即 setY() 和setTop() ,就可以改变他们 的值。

View 的 getScroolY 和 View 的 scrollTo() 和 scrollBy()

getScrollY是一个比较特别的函数,因为它涉及一个值叫mScrollY,简单说,getScrollY一般得到的都是0,除非你调用过scrollTo或scrollBy这两个函数来改变它。

scrollTo() 和 scrollBy()

从字面意思我们可以知道 scrollTo() 是滑动到哪里的意思 ,scrollBy()是相对当前的位置滑动了多少。当然这一点在源码中也是可以体现出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

有几点需要注意的是

  • 不论是scrollTo或scrollBy,其实都是对View的内容进行滚动而不是对View本身,你可以做个小实验,一个LinearLayouy背景是黄色,里面放置一个子LinearLayout背景是蓝色,调用scrollTo或scrollBy,移动的永远是蓝色的子LinearLayout。
  • 还有就是scrollTo和scrollBy函数的参数和坐标系是“相反的”,比如scrollTo(-100,0),View的内容是向X轴正方向移动的,这个相反打引号是因为并不是真正的相反,具体可以看源码,关于这两个函数的源码分析大家可以看Android——源码角度分析View的scrollBy()和scrollTo()的参数正负问题,一目了然。

View 的 width 和 height

1
2
3
4
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}

我们可以看到 Android的 height 是由 mBottom 和 mTop 共同得出的,那我们要怎样设置Android的高度呢?有人会说直接在xml里面设置 android:height=”” 不就OK了,那我们如果要动态设置height的高度呢,怎么办?你可能会想到 setWidth()方法?但是我们找遍了View的所有方法,都没有发现 setWidth()方法,那要怎样动态设置height呢?其实有两种方法

1
2
3
4
5
6
7
8
9
 int width=50;
int height=100;
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if(layoutParams==null){
layoutParams=new ViewGroup.LayoutParams(width,height);
}else{
layoutParams.height=height;
}
view.setLayoutParams(layoutParams);

第二种方法,单独地改变top或者bottom的值,这种方法不推荐使用

至于width,它跟height基本一样,只不过它是有mRight 和mLeft 共同决定而已。

需要注意的是,平时我们在执行动画的过程,不推荐使用LayoutParams来改变View的状态,因为改变LayoutParams会调用requestLayout()方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制,性能较差,源码体现如下:关于requestLayout ()方法的更多分析可以查看这一篇博客Android View 深度分析requestLayout、invalidate与postInvalidate

1
2
3
4
5
6
7
8
9
10
11
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
requestLayout();
}

因此我们如果在api 14 以后 ,在动画执行过程中,要改变View的状态,推荐使用setTranslationY()和setTranslationX(0等方法,而 尽量避免改变LayoutParams.因为性能嫌贵来说较差。

event.getY() 和 event.getRawY()

要区分于MotionEvent.getRawX() 和MotionEvent.getX();,

在public boolean onTouch(View view, MotionEvent event) 中,当你触到控件时,x,y是相对于该控件左上点(控件本身)的相对位置。 而rawx,rawy始终是相对于屏幕的位置。getX()是表示Widget相对于自身左上角的x坐标,而getRawX()是表示相对于屏幕左上角的x坐标值 (注意:这个屏幕左上角是手机屏幕左上角,不管activity是否有titleBar或是否全屏幕)。

扩展,怎样获取状态栏(StatusBar)和标题栏(titleBar)的高度

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 void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);

//屏幕
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
Log.e(TAG, "屏幕高:" + dm.heightPixels);

//应用区域
Rect outRect1 = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(outRect1);
//这个也就是状态栏的 高度
Log.e(TAG, "应用区顶部" + outRect1.top);

Log.e(TAG, "应用区高" + outRect1.height());

// 这个方法必须在有actionBar的情况下才能获取到状态栏的高度
//View绘制区域
Rect outRect2 = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(outRect2);
Log.e(TAG, "View绘制区域顶部-错误方法:" + outRect2.top); //不能像上边一样由outRect2.top获取,这种方式获得的top是0,可能是bug吧
Log.e(TAG, "View绘制区域高度:" + outRect2.height());

int viewTop = getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop(); //要用这种方法
Log.e(TAG, "View绘制区域顶部-正确方法:" + viewTop);

int titleBarHeight=viewTop;

Log.d(TAG, "onWindowFocusChanged: 标题栏高度titleBarHeight=" +titleBarHeight);

}

这里我们需要注意的 是在ActionBar存在的情况下,通过这种方法我们才能够得出titleBar的高度,否则是无法得到的,因为viewTop 为0.


这篇博客到此为止,关于更多自定义View 的一些例子,可以看我以下的博客

常用的自定义View例子一(FlowLayout)

自定义View常用例子二(点击展开隐藏控件,九宫格图片控件)

常用的自定义View例子三(MultiInterfaceView多界面处理)

常用的自定义控件四(QuickBarView)

程序员徐公,希望让你看到程序猿不同的一面,除了分享 Coding,,还有职场心得,面试经验,学习心得,人生感悟等等。希望通过该公众号,我们不只会敲代码,我们还会。。。。。。

在这里插入图片描述

闲聊

在大三的时候,一直就想搭建属于自己的一个博客,但由于各种原因,最终都不了了之,恰好最近比较有空,于是就自己参照网上的教程,搭建了属于自己的博客。

至于为什么要搭建自己的博客了?

哈哈,大概是为了装逼吧,同时自己搭建博客的话,样式的选择也比较自由,可以自己选择,不需要受限于各大平台。

转载请注明原博客地址:手把手教你用Hexo+Github 搭建属于自己的博客

大概可以分为以下几个步骤

  1. 搭建环境准备(包括node.js和git环境,gitHub账户的配置)
  2. 安装Hexo
  3. 配置Hexo
  4. 怎样将Hexo与github page 联系起来
  5. 怎样发布文章
  6. 主题 推荐
  7. 主题Net的简单配置
  8. 添加sitemap和feed插件
  9. 添加404 公益页面

搭建环境准备

大概可以分为以下三步

  • Node.js 的安装和准备
  • git的安装和准备
  • gitHub账户的配置

配置Node.js环境

  1. 下载Node.js安装文件:

根据自己的Windows版本选择相应的安装文件,要是不知道,就安装32-bit的吧- -。 如图所示:

保持默认设置即可,一路Next,安装很快就结束了。 然后我们检查一下是不是要求的组件都安装好了,同时按下Win和R,打开运行窗口:

Windows的运行界面

在这里插入图片描述

在新打开的窗口中输入cmd,敲击回车,打开命令行界面。(下文将直接用打开命令行来表示以上操作,记住哦~) 在打开的命令行界面中,输入

1
2
node -v
npm -v

如果结果如下图所示,则说明安装正确,可以进行下一步了,如果不正确,则需要回头检查自己的安装过程。

在这里插入图片描述

配置Git环境

下载Git安装文件:

GIt官网下载地址:

Git-2.6.3-64-bit.exe

然后就进入了Git的安装界面,如图:

在这里插入图片描述

Git安装界面

和Node.js一样,大部分设置都只需要保持默认,但是出于我们操作方便考虑,建议PATH选项按照下图选择:

Git PATH设置

这是对上图的解释,不需要了解请直接跳过 Git的默认设置下,出于安全考虑,只有在Git Bash中才能进行Git的相关操作。按照上图进行的选择,将会使得Git安装程序在系统PATH中加入Git的相关路径,使得你可以在CMD界面下调用Git,不用打开Git Bash了。
一样的,我们来检查一下Git是不是安装正确了,打开命令行,输入:

1
git --version

如果结果如下图所示,则说明安装正确,可以进行下一步了,如果不正确,则需要回头检查自己的安装过程。

在这里插入图片描述

关于 git的下载即安装,可以参考我的这一篇博客: Git下载及配置环境变量

github账户的注册和配置

如果已经拥有账号,请跳过此步~

第一步: Github注册

打开https://github.com/,在下图的框中,分别输入自己的用户名,邮箱,密码。

在这里插入图片描述

然后前往自己刚才填写的邮箱,点开Github发送给你的注册确认信,确认注册,结束注册流程。

一定要确认注册,否则无法使用gh-pages!

第二步: 创建代码库

登陆之后,点击页面右上角的加号,选择New repository:

新建代码库

进入代码库创建页面:

在Repository name下填写yourname.github.io,Description (optional)下填写一些简单的描述(不写也没有关系),如图所示:

在这里插入图片描述

注意:比如我的github名称是gdutxiaoxu ,这里你就填 gdutxiaoxu.github.io,如果你的名字是xujun,那你就填 xujun.github.io

第三步: . 代码库设置

正确创建之后,你将会看到如下界面:

在这里插入图片描述

接下来开启gh-pages功能,点击界面右侧的Settings,你将会打开这个库的setting页面,向下拖动,直到看见GitHub Pages,如图:

Github pages

点击Automatic page generator,Github将会自动替你创建出一个gh-pages的页面。 如果你的配置没有问题,那么大约15分钟之后,yourname.github.io这个网址就可以正常访问了~ 如果yourname.github.io已经可以正常访问了,那么Github一侧的配置已经全部结束了。

到此搭建hexo博客的相关环境配置已经完成,下面开始讲解Hexo的相关配置


安装Hexo

在自己认为合适的地方创建一个文件夹,这里我以E:/hexo 为例子讲解,首先在E盘目录下创建Hexo文件夹,并在命令行的窗口进入到该目录

在这里插入图片描述

在命令行中输入:

1
npm install hexo-cli -g

然后你将会看到:

在这里插入图片描述

可能你会看到一个WARN,但是不用担心,这不会影响你的正常使用。 然后输入

1
npm install hexo --save

然后你会看到命令行窗口刷了一大堆白字,下面我们来看一看Hexo是不是已经安装好了。 在命令行中输入:

1
hexo -v

如果你看到了如图文字,则说明已经安装成功了。


hexo的相关配置

初始化Hexo

接着上面的操作,输入:

1
hexo init

然后输入:

1
npm install

之后npm将会自动安装你需要的组件,只需要等待npm操作即可。

首次体验Hexo

继续操作,同样是在命令行中,输入:

1
hexo g

在这里插入图片描述

然后输入:

1
hexo s

然后会提示:

INFO Hexo is running at http://0.0.0.0:4000/. Press Ctrl+C to stop.

在浏览器中打开http://localhost:4000/,你将会看到:

在这里插入图片描述

到目前为止,Hexo在本地的配置已经全都结束了。

下面会讲解怎样将Hexo与github page 联系起来


怎样将Hexo与github page 联系起来

大概分为以下几步

  • 配置git个人信息
  • 配置Deployment

配置Git个人信息

如果你之前已经配置好git个人信息,请跳过这一个 步骤,直接来到

1、设置Git的user name和email:(如果是第一次的话)

1
2
git config --global user.name "xujun"
git config --global user.email "gdutxiaoxu@163.com"

2、生成密钥

1
ssh-keygen -t rsa -C "gdutxiaoxu@163.com"

配置Deployment

同样在_config.yml文件中,找到Deployment,然后按照如下修改:

1
2
3
4
deploy:
type: git
repo: git@github.com:yourname/yourname.github.io.git
branch: master

比如我的仓库的地址是git@github.com:gdutxiaoxu/gdutxiaoxu.github.io.git,所以配置如下

1
2
3
4
deploy:
type: git
repo: git@github.com:gdutxiaoxu/gdutxiaoxu.github.io.git
branch: master

写博客、发布文章

新建一篇博客,执行下面的命令:

1
hexo new post "article title"

在这里插入图片描述

这时候在我的 电脑的目录下 F:\hexo\source\ _posts 将会看到 article title.md 文件

用MarDown编辑器打开就可以编辑文章了。文章编辑好之后,运行生成、部署命令:

1
2
hexo g   // 生成
hexo d // 部署

当然你也可以执行下面的命令,相当于上面两条命令的效果

1
hexo d -g #在部署前先生成

在这里插入图片描述

部署成功后访问 你的地址,https://yourName.github.io(这里输入我的地址: https://gdutxiao.github.io ),将可以看到生成的文章。

踩坑提醒

  • 1)注意需要提前安装一个扩展:
1
npm install hexo-deployer-git --save

如果没有执行者行命令,将会提醒

deloyer not found:git

  • 2)如果出现下面这样的错误,

    Permission denied (publickey).
    fatal: Could not read from remote repository.
    Please make sure you have the correct access rights
    and the repository exists.

则是因为没有设置好public key所致。
在本机生成public key,不懂的可以参考我的这一篇博客Git ssh 配置及使用


主题推荐

每个不同的主题会需要不同的配置,主题配置文件在主题目录下的_config.yml。有两个比较好的主题推荐给大家。

Yilia

Yilia 是为 hexo 2.4+制作的主题。
崇尚简约优雅,以及极致的性能。

在这里插入图片描述

Yilia地址

NexT

我的网站就是采用这个主题,简洁美观。
目前Github上Star最高的Hexo主题,支持几种不同的风格。
作者提供了非常完善的配置说明。

在这里插入图片描述


Net主题的配置

在 Hexo 中有两份主要的配置文件,其名称都是 _config.yml。 其中,一份位于站点根目录下,主要包含 Hexo 本身的配置;另一份位于主题目录下,这份配置由主题作者提供,主要用于配置主题相关的选项。

为了描述方便,在以下说明中,将前者称为 站点配置文件, 后者称为 主题配置文件

比如我的电脑下的 F:\hexo 目录下的成为 站点配置文件,F:\hexo\themes\next 目录下的成为主题配置文件。

1)安装 NexT

Hexo 安装主题的方式非常简单,只需要将主题文件拷贝至站点目录的 themes 目录下, 然后修改下配置文件即可。具体到 NexT 来说,安装步骤如下。

下载主题

如果你熟悉 Git, 建议你使用 克隆最新版本 的方式,之后的更新可以通过 git pull 来快速更新, 而不用再次下载压缩包替换。

克隆最新版本
下载稳定版本
在终端窗口下,定位到 Hexo 站点目录下。使用 Git checkout 代码:

1
2
cd your-hexo-site
git clone https://github.com/iissnan/hexo-theme-next themes/next

2)启用主题

与所有 Hexo 主题启用的模式一样。 当 克隆/下载 完成后,打开 站点配置文件, 找到 theme 字段,并将其值更改为 next。

启用 NexT 主题

1
theme: next

到此,NexT 主题安装完成。下一步我们将验证主题是否正确启用。在切换主题之后、验证之前, 我们最好使用 hexo clean 来清除 Hexo 的缓存。

3)验证主题

首先启动 Hexo 本地站点,并开启调试模式(即加上 –debug),整个命令是 hexo s –debug。 在服务启动的过程,注意观察命令行输出是否有任何异常信息,如果你碰到问题,这些信息将帮助他人更好的定位错误。 当命令行输出中提示出:

INFO Hexo is running at http://0.0.0.0:4000/. Press Ctrl+C to stop.

此时即可使用浏览器访问 http://localhost:4000 ,检查站点是否正确运行。

当你看到站点的外观与下图所示类似时即说明你已成功安装 NexT 主题。这是 NexT 默认的 Scheme —— Muse

现在,你已经成功安装并启用了 NexT 主题。下一步我们将要更改一些主题的设定,包括个性化以及集成第三方服务。

4)主题设定

选择 Scheme

Scheme 是 NexT 提供的一种特性,借助于 Scheme,NexT 为你提供多种不同的外观。同时,几乎所有的配置都可以 在 Scheme 之间共用。目前 NexT 支持三种 Scheme,他们是:

1
2
3
4
Muse - 默认 Scheme,这是 NexT 最初的版本,黑白主调,大量留白
Mist - Muse 的紧凑版本,整洁有序的单栏外观
Pisces - 双栏 Scheme,小家碧玉似的清新
Scheme 的切换通过更改 主题配置文件,搜索 scheme 关键字。 你会看到有三行 scheme 的配置,将你需用启用的 scheme 前面

注释 # 即可。

选择 Pisce Scheme

1
2
3
#scheme: Muse
#scheme: Mist
scheme: Pisces

5)设置语言

编辑 站点配置文件, 将 language 设置成你所需要的语言。建议明确设置你所需要的语言,例如选用简体中文,配置如下:

1
language: zh-Hans

目前 NexT 支持的语言如以下表格所示:

语言 代码 设定实例
English en language: en
简体中文 zh-Hans language: zh-Hans
Français fr-FR language: fr-FR
Português pt language: pt
繁體中文 zh-hk 或者 zh-tw language: zh-hk
Русский язык ru language: ru
Deutsch de language: de
日本語 ja language: ja
Indonesian id language: id

6)设置 菜单

菜单配置包括三个部分,第一是菜单项(名称和链接),第二是菜单项的显示文本,第三是菜单项对应的图标。 NexT 使用的是 Font Awesome 提供的图标, Font Awesome 提供了 600+ 的图标,可以满足绝大的多数的场景,同时无须担心在 Retina 屏幕下 图标模糊的问题。

编辑主题配置文件,修改以下内容:

设定菜单内容,对应的字段是 menu。 菜单内容的设置格式是:item name: link。其中 item name 是一个名称,这个名称并不直接显示在页面上,她将用于匹配图标以及翻译。

菜单示例配置

1
2
3
4
5
6
7
menu:
home: /
archives: /archives
#about: /about
#categories: /categories
tags: /tags
#commonweal: /404.html

若你的站点运行在子目录中,请将链接前缀的 / 去掉

NexT 默认的菜单项有(标注 的项表示需要手动创建这个页面):

键值 设定值 显示文本(简体中文)
home home: / 主页
archives archives: /archives 归档页
categories categories: /categories 分类页
tags tags: /tags 标签页
about about: /about 关于页面
commonweal commonweal: /404.html 公益 404

设置菜单项的显示文本。在第一步中设置的菜单的名称并不直接用于界面上的展示。Hexo 在生成的时候将使用 这个名称查找对应的语言翻译,并提取显示文本。这些翻译文本放置在 NexT 主题目录下的 languages/{language}.yml ({language} 为你所使用的语言)。

以简体中文为例,若你需要添加一个菜单项,比如 something。那么就需要修改简体中文对应的翻译文件 languages/zh-Hans.yml,在 menu 字段下添加一项:

1
2
3
4
5
6
7
8
9
menu:
home: 首页
archives: 归档
categories: 分类
tags: 标签
about: 关于
search: 搜索
commonweal: 公益404
something: 有料

设定菜单项的图标,对应的字段是 menu_icons。 此设定格式是 item name: icon name,其中 item name 与上一步所配置的菜单名字对应,icon name 是 Font Awesome 图标的 名字。而 enable 可用于控制是否显示图标,你可以设置成 false 来去掉图标。

菜单图标配置示例

1
2
3
4
5
6
7
8
9
menu_icons:
enable: true
# Icon Mapping.
home: home
about: user
categories: th
tags: tags
archives: archive
commonweal: heartbeat

在菜单图标开启的情况下,如果菜单项与菜单未匹配(没有设置或者无效的 Font Awesome 图标名字) 的情况下,NexT 将会使用 作为图标。

请注意键值(如 home)的大小写要严格匹配

7)** 侧栏**

默认情况下,侧栏仅在文章页面(拥有目录列表)时才显示,并放置于右侧位置。 可以通过修改 主题配置文件 中的 sidebar 字段来控制侧栏的行为。侧栏的设置包括两个部分,其一是侧栏的位置, 其二是侧栏显示的时机。

设置侧栏的位置,修改 sidebar.position 的值,支持的选项有:

1
2
left - 靠左放置
right - 靠右放置

目前仅 Pisces Scheme 支持 position 配置。影响版本5.0.0及更低版本。

1
2
sidebar:
position: left

设置侧栏显示的时机,修改 sidebar.display 的值,支持的选项有:

1
2
3
4
5
6
post - 默认行为,在文章页面(拥有目录列表)时显示
always - 在所有页面中都显示
hide - 在所有页面中都隐藏(可以手动展开)
remove - 完全移除
sidebar:
display: post

已知侧栏在 use motion: false 的情况下不会展示。 影响版本5.0.0及更低版本。

8)设置 头像

编辑 站点配置文件, 新增字段 avatar, 值设置成头像的链接地址。其中,头像的链接地址可以是:

地址
完整的互联网 URI http://example.com/avtar.png
站点内的地址 将头像放置主题目录下的 source/uploads/ (新建uploads目录若不存在) 配置为:avatar: /uploads/avatar.png 或者 放置在 source/images/ 目录下 , 配置为:avatar: /images/avatar.png

头像设置示例

1
avatar: http://example.com/avtar.png

9)设置 作者昵称

编辑 站点配置文件, 设置 author 为你的昵称。

10)站点描述

编辑 站点配置文件, 设置

字段为你的站点描述。站点描述可以是你喜欢的一句签名:)

net主题的官方文档地址


添加插件

添加sitemap和feed插件

切换到你本地的hexo 目录CIA,在命令行窗口,属兔以下命令

1
2
npm install hexo-generator-feed -save
npm install hexo-generator-sitemap -save

修改_config.yml,增加以下内容

1
2
3
4
5
6
7
8
9
10
11
12
# Extensions
Plugins:
- hexo-generator-feed
- hexo-generator-sitemap
#Feed Atom
feed:
type: atom
path: atom.xml
limit: 20
#sitemap
sitemap:
path: sitemap.xml

再执行以下命令,部署服务端

1
hexo d -g

配完之后,就可以访问 https://gdutxiaoxu.github.io/atom.xmlhttps://gdutxiaoxu.github.io/sitemap.xml ,发现这两个文件已经成功生成了。


添加404 页面

GitHub Pages有提供制作404页面的指引:Custom 404 Pages
直接在根目录下创建自己的404.html或者404.md就可以。但是自定义404页面仅对绑定顶级域名的项目才起作用,GitHub默认分配的二级域名是不起作用的,使用hexo server在本机调试也是不起作用的。

推荐使用腾讯公益404

我的404页面配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8;"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="robots" content="all" />
<meta name="robots" content="index,follow"/>
</head>
<body>

<script type="text/javascript" src="https://www.qq.com/404/search_children.js"
charset="utf-8" homePageUrl="gdutxiaoxu.github.io"
homePageName="回到我的主页">
</script>

</body>
</html>

参考博客

Hexo主页

史上最详细的Hexo博客搭建图文教程

我的git系列参考教程

转载请注明原博客地址:手把手教你用Hexo+Github 搭建属于自己的博客

欢迎关注我的微信公众号程序员徐公,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。
在这里插入图片描述

前言:前几天在写博客 手把手教你用Hexo + github 搭建自己博客的时候,经常需要用到一些git操作,截了好多图,于是就想干脆整理成一系列的git 教程,总结如下


本篇博客主要讲解以下问题:

  • Git 常用命令
    • 创建新仓库
    • 检出仓库
    • 添加与提交
    • 推送改动
    • 分支
    • 更新与合并
    • 标签
    • 替换本地改动
  • Git实例教程
  • 操作小技巧

Git 常用命令常用命令

创建新仓库

创建新文件夹,打开,然后执行

1
git init

以创建新的 git 仓库。

检出仓库

执行如下命令以创建一个本地仓库的克隆版本:

1
git clone /path/to/repository 

如果是远端服务器上的仓库,你的命令会是这个样子:

1
git clone username@host:/path/to/repository

工作流
你的本地仓库由 git 维护的三棵“树”组成。第一个是你的 工作目录,它持有实际文件;第二个是 缓存区(Index),它像个缓存区域,临时保存你的改动;最后是 HEAD,指向你最近一次提交后的结果。

添加与提交

你可以计划改动(把它们添加到缓存区),使用如下命令:

1
2
3
4
5
git add <filename>
git add *

# 添加所有文件
git add .

这是 git 基本工作流程的第一步;使用如下命令以实际提交改动:

1
git commit -m "代码提交信息"

现在,你的改动已经提交到了 HEAD,但是还没到你的远端仓库。

推送改动

你的改动现在已经在本地仓库的 HEAD 中了。执行如下命令以将这些改动提交到远端仓库:

1
2
git push origin master

可以把 master 换成你想要推送的任何分支。

如果你还没有克隆现有仓库,并欲将你的仓库连接到某个远程服务器,你可以使用如下命令添加:

1
2
3
4
# 注意 server必须是存在的仓库
git remote add origin <server>
git remote add origin https://github.com/gdutxiaoxu/test2.git

该命令是移除本地缓存已有的remote信息

1
git remote remove origin 

如此你就能够将你的改动推送到所添加的服务器上去了。

分支

分支是用来将特性开发绝缘开来的。在你创建仓库的时候,master 是“默认的”。在其他分支上进行开发,完成后再将它们合并到主分支上。

创建一个叫做“feature_x”的分支,并切换过去:

1
git checkout -b feature_x

切换回主分支:

1
git checkout master

切换回分支:

1
2
git checkout <branch>
git checkout feature_x

再把新建的分支删掉:

1
git branch -d feature_x

除非你将分支推送到远端仓库,不然该分支就是 不为他人所见的:

1
2
git push origin <branch>
git push origin feature_x

更新与合并

要更新你的本地仓库至最新改动,执行:

1
git pull

以在你的工作目录中 获取(fetch) 并 合并(merge) 远端的改动。
要合并其他分支到你的当前分支(例如 master),执行:

1
2
git merge <branch>
git merge feature_x

两种情况下,git 都会尝试去自动合并改动。不幸的是,自动合并并非次次都能成功,并可能导致 冲突(conflicts)。 这时候就需要你修改这些文件来人肉合并这些 冲突(conflicts) 了。改完之后,你需要执行如下命令以将它们标记为合并成功:

1
git add <filename>

在合并改动之前,也可以使用如下命令查看:

1
git diff <source_branch> <target_branch>

标签

在软件发布时创建标签,是被推荐的。这是个旧有概念,在 SVN 中也有。可以执行如下命令以创建一个叫做 1.0.0 的标签:

1
git tag 1.0.0 1b2e1d63ff

1b2e1d63ff 是你想要标记的提交 ID 的前 10 位字符。使用如下命令获取提交 ID:

1
git log

你也可以用该提交 ID 的少一些的前几位,只要它是唯一的。

替换本地改动

假如你做错事(自然,这是不可能的),你可以使用如下命令替换掉本地改动:

1
git checkout -- <filename>

此命令会使用 HEAD 中的最新内容替换掉你的工作目录中的文件。已添加到缓存区的改动,以及新文件,都不受影响。

假如你想要丢弃你所有的本地改动与提交,可以到服务器上获取最新的版本并将你本地主分支指向到它:

1
2
git fetch origin
git reset --hard origin/master

有用的贴士

内建的图形化 git:

1
gitk

彩色的 git 输出:

1
git config color.ui true

显示历史记录时,只显示一行注释信息:

1
git config format.pretty oneline

交互地添加文件至缓存区:

1
git add -i

到此 git常用的命令已经 讲解完毕,下面开始讲解Git 实例教程


Git实例教程

大概分为以下两步

  • github账号的注册与Repo的创建
  • 实例教程

github账号的注册与Repo的创建

  1. Github注册

打开https://github.com/,在下图的框中,分别输入自己的用户名,邮箱,密码。

然后前往自己刚才填写的邮箱,点开Github发送给你的注册确认信,确认注册,结束注册流程。

一定要确认注册,否则无法使用gh-pages!

  1. 创建代码库

登陆之后,点击页面右上角的加号,选择New repository:

新建代码库

进入代码库创建页面:

到此我们就创建好了repo,地址 为:https://github.com/gdutxiaoxu/test.git

实例教程

这里我们把仓库建在 G://test 目录下

  1. 首先打开命令行,进入G 盘,输入以下命令
1
2
# 在 test目录下创建 README.md 文件
echo "# test" >> README.md
  1. 接着初始化仓库
1
git init

可以看到如下图片的效果

  1. 将 README.md 文件添加到版本控制

    1
    git add README.md
  2. 提交文件到本地缓存,并添加说明

1
git commit -m "first commit"

  1. 将本地仓库与远程仓库 https://github.com/gdutxiaoxu/test.git 联系起来
1
git remote add origin https://github.com/gdutxiaoxu/test.git
  1. 将本地仓库缓存的文件提交到远程仓库中
1
git push -u origin master

如果你没有配置ssh ,那么在这里需要输入你的github 账户的用户名和密码

正确输入你的用户名和密码后,可以看到

同时我们登陆我们的github 仓库 : https://github.com/gdutxiaoxu/test.git ,可以看到:

说明已经提交成功了。

注意事项:

  • 如果我们本地已经存在仓库了,那我们只需要执行以下命令就可以将我们本地仓库与远程绑定起来
1
2
3
4
git remote add origin https://github.com/gdutxiaoxu/test.git
git push -u origin master

git pull https://github.com/gdutxiaoxu/test.git master
  • 如果本地仓库已经绑定别的远程仓库,我们可以用以下命令将其删除相应的仓库信息
1
2
# 该命令是移除本地缓存已有的remote信息
git remote remove origin
  • 如果我们remote repo (即远端仓库已经存在了),那么我们只需要执行以下命令就OK了
1
git clone https://github.com/gdutxiaoxu/test.git  "you path"

比如我们想储存在 G://test 目录下,那么我们可以输入一下命令

1
git clone https://github.com/gdutxiaoxu/test.git  G://test

效果图如下



操作小技巧

有时候在cmd 窗口中,你会发现复制,粘贴的快捷键失效了,对我们开发者来说很不方便,拿我们有什么解决方法你? 哈哈,就是开启快速插入模式。

右键点击,点击cmd 窗口

选择快速插入模式,在Cmd 窗口,按右键,就能实现粘贴了。

同理,在git bash 窗口也是这样,这样就不在阐述了。

前言:前几天在写博客 手把手教你用Hexo + github 搭建自己博客
的时候,经常需要用到一些git操作,截了好多图,于是就想干脆整理成一系列的git 教程,总结如下


下载Git安装文件:

GIt官网下载地址:

Git-2.6.3-64-bit.exe

然后就进入了Git的安装界面,如图:

Git安装界面

和Node.js一样,大部分设置都只需要保持默认,但是出于我们操作方便考虑,建议PATH选项按照下图选择:

Git PATH设置

这是对上图的解释,不需要了解请直接跳过 Git的默认设置下,出于安全考虑,只有在Git Bash中才能进行Git的相关操作。按照上图进行的选择,将会使得Git安装程序在系统PATH中加入Git的相关路径,使得你可以在CMD界面下调用Git,不用打开Git Bash了。
一样的,我们来检查一下Git是不是安装正确了,打开命令行,输入:

1
git --version

如果结果如下图所示,则说明安装正确,可以进行下一步了,如果不正确,则需要回头检查自己的安装过程。

Git安装界面

大部分设置都只需要保持默认,但是出于我们操作方便考虑,建议PATH选项按照下图选择:

Git PATH设置

这是对上图的解释,不需要了解请直接跳过 Git的默认设置下,出于安全考虑,只有在Git Bash中才能进行Git的相关操作。按照上图进行的选择,将会使得Git安装程序在系统PATH中加入Git的相关路径,使得你可以在CMD界面下调用Git,不用打开Git Bash了。
一样的,我们来检查一下Git是不是安装正确了,打开命令行,输入:

1
git --version

如果结果如下图所示,则说明安装正确,可以进行下一步了,如果不正确,则需要回头检查自己的安装过程。