一、前言
最近用ViewPager做一个用户引导界面,对于为什么在setAdapter后,再对数据更改时要加notifyDataSetChanged()产生了疑惑。在不加notifyDataSetChanged()时,程序就会崩溃,并提醒你要加上notifyDataSetChanged。通过查找源码找到了答案,并且学习了一下设计模式中的观察者模式,不对的地方,欢迎指教和艾特我出来挨打。
二、源码解析
初学android的时候,学到ListView的时候,会先将要展示的数据准备好放到adapter中,再调用ListView中的setAdapter,并将adapter传入,从而实现一个简单的ListView。同理ViewPager也可以这么实现。但最近看到一些代码是将顺序颠倒,先setAdapter将adapter传,再设置adapter中设置数据,并加一个notifyDataSetChanged()。
vp.setAdapter(adapter);
ArrayList<Integer> datas = new ArrayList<>();
datas.add(R.drawable.guide1);
datas.add(R.drawable.guide2);
datas.add(R.drawable.guide3);
adapter.setDatas(datas);
//adapter中setDatas代码
public void setDatas(List<T> data){
if(data!=null&&data.size()>0){
datas.clear();
datas.addAll(data);
notifyDataSetChanged();
}
}
在不加notifyDataSetChanged(),程序直接崩溃。在崩溃点找到如下源码,是在populate函数里,关于这个函数,网上说的是跟View页面绘制有关,这里我也不太懂,但可以知道是在页面绘制时出的问题。
void populate(int newCurrentItem) {
///
final int pageLimit = mOffscreenPageLimit;
final int startPos = Math.max(0, mCurItem - pageLimit);
final int N = mAdapter.getCount();
final int endPos = Math.min(N-1, mCurItem + pageLimit);
if (N != mExpectedAdapterCount) {
String resName;
try {
resName = getResources().getResourceName(getId());
} catch (Resources.NotFoundException e) {
resName = Integer.toHexString(getId());
}
throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
" contents without calling PagerAdapter#notifyDataSetChanged!" +
" Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
" Pager id: " + resName +
" Pager class: " + getClass() +
" Problematic adapter: " + mAdapter.getClass());
}
}
可以看到final int N = mAdapter.getCount();getCount()是一个抽象函数,在使用adapter时,需要我们自己实现,用来返回数据的个数。在获取到数据个数后会与一个成员变量mExpectedAdapterCount做比较,不相等的话,就会抛出异常,异常里的信息就是要我们加上notifyDataSetChanged。看来问题就是出在这个mExpectedAdapterCount上,接下来要看看这个成员变量是什么。
/**
* Set a PagerAdapter that will supply views for this pager as needed.
*
* @param adapter Adapter to use
*/
public void setAdapter(PagerAdapter adapter) {
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
mExpectedAdapterCount = 0;
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.registerDataSetObserver(mObserver);
mPopulatePending = false;
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount();
}
}
在setAdapter中可以看到首先将mExpectedAdapterCount赋值为0,接着判断适配器是否为空,在最后我们又一次看到了熟悉的代码,mExpectedAdapterCount = mAdapter.getCount(),又是获取adapter数据的个数。这时候,我们就知道为什么不加notifyDataSetChanged会崩溃,因为在先使用setAdapter的时候,会对mExpectedAdapterCount赋一次值,值为数据的个数,这时候,我们并没有在adapter中传入数据,所以数组的大小为0,我们接着往adapter里入传数据,然后View开始绘制页面时,执行到populate时再次获取适配器里的数据,这时候数据已经发生变化了,所以发生了崩溃。知道了崩溃的原因后,我们就可以知道加notifyDataSetChanged肯定是将mExpectedAdapterCount的值修正了,但是我们还是要看一下源码,看它是在哪里将值修正的,毕竟眼见为实。
/**
* This method should be called by the application if the data backing this adapter has changed
* and associated views should update.
*/
public void notifyDataSetChanged() {
mObservable.notifyChanged();
}
找到PagerAdapter里的notifyDataSetChanged函数,看到是调用了mObservable的notifyChanged函数,mObservable下面会介绍是什么,这里先看notifyChanged函数。
/**
* Invokes {@link DataSetObserver#onChanged} on each observer.
* Called when the contents of the data set have changed. The recipient
* will obtain the new contents the next time it queries the data set.
*/
public void notifyChanged() {
synchronized(mObservers) {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
notifyChanged里的代码很少,重点是其中的for循环,遍历了mObservers中的数据,调用它们的onChanged函数。。
private class PagerObserver extends DataSetObserver {
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
在ViewPager里看到 onChanged的实现,调用了dataSetChanged函数。
void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null.
final int adapterCount = mAdapter.getCount();
mExpectedAdapterCount = adapterCount;
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
mItems.size() < adapterCount;
int newCurrItem = mCurItem;
}
在这个函数的开头我们就可以看到mExpectedAdapterCount被adapterCount更新,adapterCount 是mAdapter.getCount()里得到的,现在mExpectedAdapterCount已经被更新为最新的值,所以就不会产生崩溃。至此崩溃的原因和解决办法已经明了,但是在我初学的时候,对类之间的关系和函数调用看的云里雾里,但在看到设计模式中的观察者模式后,对整体的脉络更加清晰了,下面介绍一下观察者模式。
3、观察者模式
观察者模式,在生活中随处可见。比如你关注了一位博主,在有新的内容更新或者发布的时候,就会通知你。在这里订阅者就是一个观察者,博主就是一个被观察者。android里的广播是不是就有种似曾相识的感觉,下面我自己实现了个简单的观察者模式。
public interface MyObserver {
public void myupdate(MyObservable myObservable);
}
首先是定义了一个观察者的接口,里面只有一个myupdate函数,参数为MyObservable,是一个自己定义的一个被观察者。
import java.util.ArrayList;
import java.util.List;
public class MyObservable {
private String sendInfo;
private List<MyObserver> mMyObservers;
public MyObservable(){
mMyObservers = new ArrayList<>();
}
protected void MyNotificationDataChanged(){
for(MyObserver myObserver : mMyObservers){
myObserver.myupdate(this);
}
}
public void AddObservrt(MyObserver myObserver){
mMyObservers.add(myObserver);
}
protected void setInfo(String string){
this.sendInfo = string;
}
public String getInfo(){
return "接受广播:"+ this.sendInfo;
}
}
这是一个被观察者类,其中定义了一个MyObserver数组,接着看AddObserver,代码很简单,将传进来的myObserver添加到数组里,最后看MyNotificationDataChanged函数,是不是和android源码中的notifyChanged很像?遍历mMyObservers数组,调用它们的myupdate函数,这里myupdate还没实现,下面来实现它。
import javafx.beans.Observable;
public class Coder implements MyObserver {
public String name;
public Coder(String aName){
name = aName;
}
@Override
public void myupdate(MyObservable myObservable) {
System.out.println(this.name + myObservable.getInfo());
}
}
接着实现了MyObserver接口,并重写了myupdate,这里打印了下从被观察者中得到的数据,接着我们再写一个继承被观察者的类。
import java.util.Observable;
public class MyBroadcast extends MyObservable {
public MyBroadcast(){}
public void sendBroadcast(String content){
setInfo(content);
MyNotificationDataChanged();
}
}
定义了一个MyBroadcast继承MyObservable,定义了一个成员函数sendBroadcast,setInfo用来更新内容,然后调用函数MyNotificationDataChanged来通知订阅观察者。终于写好了,可以写demo测试下。
public class Test {
public static void main(String[] args){
MyBroadcast myBroadcast = new MyBroadcast();
Coder coder_1 = new Coder("coder_1");
Coder coder_2 = new Coder("coder_2");
Coder coder_3 = new Coder("coder_3");
Coder coder_4 = new Coder("coder_4");
myBroadcast.AddObserver(coder_1);
myBroadcast.AddObserver(coder_2);
myBroadcast.AddObserver(coder_3);
myBroadcast.AddObserver(coder_4);
myBroadcast.sendBroadcast("我是新消息\n");
}
}
-----------------------------------------------------------
Test
coder_1接受广播:我是新消息
coder_2接受广播:我是新消息
coder_3接受广播:我是新消息
coder_4接受广播:我是新消息
总结下,观察者加入到被观察者中的数组中,完成了注册,被观察者通过SendBroiadcast->MyNotificationDataChanged遍历数组中的所有数据,并调用它们的myupdate函数从而完成通知。到此我们就可拿这个demo和前面的adnroid源码中的代码作比较,会发现原理是一样的。,PageView是根据PagerAdapter中的数据来显示的,所以说PagerAdapter是一个被观察者,ViewPager是一个观察者。我们可以回头来看下源码中的ViewPager中的setAdapter函数。
/**
* Set a PagerAdapter that will supply views for this pager as needed.
*
* @param adapter Adapter to use
*/
public void setAdapter(PagerAdapter adapter) {
///
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.registerDataSetObserver(mObserver);
mPopulatePending = false;
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount();
if (mRestoredCurItem >= 0) {
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
setCurrentItemInternal(mRestoredCurItem, false, true);
mRestoredCurItem = -1;
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) {
populate();
} else {
requestLayout();
}
}
}
可以看到mAdapter.registerDataSetObserver(mObserver),其中mAdapter是一个PagerAdapter,mObserver是一个观察者,可以看一下它的数据类型和实现。
private class PagerObserver extends DataSetObserver {
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
再次翻到前面的解析,它重写了观察者的onChanged方法,跟我们demo里的myupdate类似。这时候观察者有了,被观察者也有了,demo里是用AddObserver函数联结它们,将观察者加入到数组,这里可以猜到registerDataSetObserver应该做了同样的事。
/**
* Register an observer to receive callbacks related to the adapter's data changing.
*
* @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
*/
public void registerDataSetObserver(DataSetObserver observer) {
mObservable.registerObserver(observer);
}
这里调用了mObservable的registerObserver函数,mObservable是一个DataSetObservable类。
/**
* Adds an observer to the list. The observer cannot be null and it must not already
* be registered.
* @param observer the observer to register
* @throws IllegalArgumentException the observer is null
* @throws IllegalStateException the observer is already registered
*/
public void registerObserver(T observer) {
if (observer == null) {
throw new IllegalArgumentException("The observer is null.");
}
synchronized(mObservers) {
if (mObservers.contains(observer)) {
throw new IllegalStateException("Observer " + observer + " is already registered.");
}
mObservers.add(observer);
}
}
最关键的是最后一句mObservers.add(observers),来看一下mObservers的数据类型。
protected final ArrayList<T> mObservers = new ArrayList<T>();
是一个数组!!是不是发现跟我们的demo相似度高的离谱。这说明了ViewPager和PageDapter之间使用了观察者模式。
四、总结
总结分析完整个流程后,可以直观的得到,在adapter中更改了数据,而不受用notifyDataSetChanged就相当于被观察者更新了内容,却不通知你,这样的订阅毫无意义。观察者模式应该做到被观察者更改了数据,就需要通知订阅者,至于订阅者怎么处理,这就不管它的事了。