Fragment的创建和管理

我们很多应用都涉及到fragment的创建和管理,比如:下面n个tab,上面n个fragment的组合,activity管理多个fragment或者一个fragment等等。

很多应用在MainActivity里的onCreate里去 一 一 实例化fragment,然后在tab切换的时候使用FragmentTransaction去添加或者替换fragment。
大概是这么写的

1
2
3
4
5
6
7
8
9
10
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e("StateActivity", "---------------onCreate");
setContentView(R.layout.activity_main_for_state);
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.state_fragment_container, Fragment1.newInstance("", ""));
ft.commit();
}

这种有没有问题呢?看着好像没有问题,但是如果你打开不保存状态然后再调试或者打印log你就会会发现问题了。

我们有A,B两个Activity,A里有一个fragment,fragment里有一个按钮,可以跳转到Activity B。

我们打开不保存状态,点击fragment里的按钮跳转到B,然后返回,看看A以及fragment的哪些生命周期调用了
看看log的打印信息:

(test是在onSavedInstanceState中保存的字符串)

我们可以看到:

1. Fragment被创建两次。
2. 第二次创建的fragment获取不到我们保存的状态。

想一想也是有道理的:
在不保存状态的情况下,跳转到B之后,A和A里面的fragment都被销毁了,回到A的时候,log中打印的第一个fragment是系统保存状态然后恢复的fragment,所以保存的状态都能获取到,但是第二个fragment是在恢复activity运行生命周期onCreate方法的时候我们代码new出来的,所以第二个fragment里面是没有我们之前保存的状态的。

这种问题要怎么解决呢?
如果只是解决问题1,重复创建fragment的情况(不想有两个fragment实例,因为有的如果是透明fragment可能还会出现重叠),很好解决使用replace,或者不调用fragment的super.onSavedInsance()等等。

但是如果我们想要在fragment中保存状态要怎么办呢?

1如果是一个fragment的activity。

我们可以先从fragmentManager里去取,如果取到了,那么说明这个fragment已经有了,那么我们就什么也不做,如果没取到,我们再去创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e("StateActivity", "---------------onCreate");
setContentView(R.layout.activity_main_for_state);
FragmentManager fm = getSupportFragmentManager();
Fragment1 f = (Fragment1) fm.findFragmentByTag(TAG_FRAGMENT1);
if (f == null) {
FragmentTransaction ft = fm.beginTransaction();
f = Fragment1.newInstance("", "");
ft.add(R.id.state_fragment_container, f, TAG_FRAGMENT1);
ft.commit();
}
}

再来运行一下看看log:

Fragment只被创建了一次,并且也能获取到他所保存的状态。
(其实也可以根据savedInstanceState是否为空来判断是否需要重新创建fragment。)

如果一个activity里需要创建多个fragment并且以tab的方式来切换,我们应该怎么做?

借鉴fragmentTabHost里的做法,用一个TabInfo实体类来保存对应的tab和fragment之间的关系。对于fragment的创建,还是先从fragmentManager里去取,如果没有再去创建。

使用一个TabInfo实体类来保存fragment和每个tab之间的关系。
比如:

1
2
3
4
5
6
7
8
9
10
11
public class TabInfo {
public final String tag;
public final Class<? extends Fragment> clazz;
public final int viewId;

public TabInfo(String tag, Class<? extends Fragment> clazz, int viewId) {
this.tag = tag;
this.clazz = clazz;
this.viewId = viewId;
}
}

然后用一个list来保存所有的tab信息:

1
2
3
4
private ArrayList<TabInfo> tabInfos = new ArrayList<TabInfo>(2);
//在onCreate()中初始化tab
tabInfos.add(new TabInfo(Fragment1.class.getSimpleName(), Fragment1.class, R.id.tab_right));
tabInfos.add(new TabInfo(Fragment2.class.getSimpleName(), Fragment2.class, R.id.tab_left));

当然这里做的比较粗糙,使用两个按钮做tab,也可以使用其他的自定义view。

切换的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void switchTab(int tabIndex) {
TabInfo tabInfo = tabInfos.get(tabIndex);
FragmentManager fm = getSupportFragmentManager();
Fragment f = fm.findFragmentByTag(tabInfo.tag);
FragmentTransaction transaction = fm.beginTransaction();
if (f == null) {
f = Fragment.instantiate(this, tabInfo.clazz.getName());
transaction.add(R.id.tab_fragment_container, f, tabInfo.tag);
}
if (currentFragment != null && currentFragment != f) {
transaction.hide(currentFragment);
}
transaction.show(f);
transaction.commit();
currentFragment = f;
this.tabIndex = tabIndex;
}

这里替换fragment用的是show/hide(不销毁fragment和视图),当然你也可以使用detach/attach(只销毁视图不销毁fragment实例)
http://www.voidcn.com/blog/u013168615/article/p-5794851.html (这篇文章讲show/hide,detach/attach等等比较详细)

再来看看我们的log:

一切正常!

java线程池和对象池

java线程池

原文链接:(讲的比较详细)
http://www.infoq.com/cn/articles/java-threadPool#anch97718

线程池的创建

我们可以通过ThreadPoolExecutor来创建一个线程池。

1
new  ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

创建一个线程池需要输入几个参数:

  1. corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  2. runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。

    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  3. maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

  4. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  5. RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
  6. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

  7. TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

向线程池提交任务

  • 我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

    1
    2
    3
    4
    5
    6
    threadsPool.execute(new Runnable() {
    @Override
    public void run() {
    // TODO Auto-generated method stub
    }
    });
  • 我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Future<Object> future = executor.submit(harReturnValuetask);
    try {
    Object s = future.get();
    } catch (InterruptedException e) {
    // 处理中断异常
    } catch (ExecutionException e) {
    // 处理无法执行任务异常
    } finally {
    // 关闭线程池
    executor.shutdown();
    }

线程池的关闭

我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

线程池的分析

  • 流程分析:线程池的主要工作流程如下图:

从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:

  1. 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
  2. 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
  3. 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。
  • 源码分析。上面的流程分析让我们很直观的了解了线程池的工作原理,让我们再通过源代码来看看是如何实现的。线程池执行任务的方法如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public void execute(Runnable command) {
    if (command == null)
    throw new NullPointerException();
    //如果线程数小于基本线程数,则创建线程并执行当前任务
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
    //如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。
    if (runState == RUNNING && workQueue.offer(command)) {
    if (runState != RUNNING || poolSize == 0)
    ensureQueuedTaskHandled(command);
    }
    //如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,
    则创建一个线程执行任务。
    else if (!addIfUnderMaximumPoolSize(command))
    //抛出RejectedExecutionException异常
    reject(command); // is shutdown or saturated
    }
    }

工作线程。线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。我们可以从Worker的run方法里看到这点:

1
2
3
4
5
6
7
8
9
10
11
12
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}

如何复用线程:

从上面也可以看出来:
是通过无限循环获取工作队列里的任务来执行,并且在自己的run方法中直接调用Runnable.run()方法,这样并没有开启新的线程,而是处理了我们提交的线程里需要执行的任务,这样就达到了复用线程的目的了。


对象池:

参考资料:

http://www.cnblogs.com/rubylouvre/p/3314775.html

对象池的作用:

限制一个类的对象的个数,(单例是限制了一个类只能有一个实例,对象池则是限制一个类实例的个数)

实现方式:

也是跟线程池差不多,在创建新对象的时候去判断是重新创建还是去池里取还是等待,从而达到复用对象的功能。这里重新创建和等待都很好理解,那么如何判断一个已经创建的对象是否可用呢?这就需要使用方在用完这个对象的时候去将这个对象设置为空闲,也就是相当于手动释放。理解了这个,对象池还是很好理解的。

android插件化开发

昨天一不小心搞出一个bug,而且还是崩溃,但是很快我就找到了原因,并且修复了bug提交了代码,一会儿qa马上热发了一版,这样我的崩溃问题就解决了,这也太快了!Android更新不是要先发应用市场,然后等待好几周的审核,然后有的用户还不肯更新吗?这里面到底用到了什么黑科技?于是我想知道android热发的原理,开始查android热发,没有找到什么有价值的东西,后来搜android 组件化开发,在知乎上找到一篇,回答很简单,但是有一个链接到他详细博客的地址http://www.trinea.cn/android/android-plugin/ ,根据博客里面提供的信息,我了解了几个开源框架是如何实现热加载,或者说是热发的。


热发的过程:

  1. apk插件更新:通过差分(bsdiff),来更新需要更新的apk插件,最小化用户的下载量。
  2. 宿主程序动态加载更新之后的apk插件,完成热发。

组件开发的概念:

宿主程序通过ClassLoader去动态加载apk文件并将其放在自己的进程中执行。


基本概念理解资料:


组件开发的好处:

  • 动态更新。不用频繁卸载安装apk程序。
  • 可以多个项目同时开发,减少apk的大小,并且每个项目可以同时进行,减少编译时间等等。

组件开发需要解决的问题:

https://github.com/singwhatiwanna/dynamic-load-apk 这个很好地解释了为什么会有这两个问题

  1. Activity生命周期的管理。

    apk被宿主程序调起以后,apk中的activity其实就是一个普通的对象,不具有activity的性质 ,因为系统启动activity是要做很多初始化工作的,而我们在应用层通过反射去启动activity是很难完成系统所做的初始化工作的,所以activity的大部分特性都无法使用包括activity的生命周期管理,这就需要我们自己去管理。

  2. 资源的访问。

    我们知道,宿主程序调起未安装的apk,一个很大的问题就是资源如何访问,具体来说就是,凡是以R开头的资源都不能访问了,因为宿主程序中并没有apk中的资源,所以通过R来加载资源是行不通的,程序会报错:无法找到某某id所对应的资源。


几种动态加载开源项目的实现:

原文链接:http://www.trinea.cn/android/android-plugin/

  1. DynamicLoadApk
    GitHub:https://github.com/singwhatiwanna/dynamic-load-apk
    这个项目实现了一部分的动态加载,原理是 DexClassLoader 加 Activity 代理,可以看看。即在容器中注册几个代理的 Activity,启动插件的 Activity 时实际启动的都是代理的 Activity,这样就解决了 Activity 必须注册的问题。
    当然这个项目里也有不少问题没解决,有兴趣可以加入他们。

  2. AndroidDynamicLoader GitHub:https://github.com/mmin18/AndroidDynamicLoader
    这是点评一个工程师介绍的方式,和上面不同的是:他不是用代理 Activity 的方式实现而是用 Fragment 以及 schema 的方式实现

  3. Android PluginManager GitHub:https://github.com/houkx/android-pluginmgr
    这个项目的原理实际也是 DexClassLoader 加 Activity 代理,不同的是上面的 dynamic-load-apk 项目中,插件需要依赖框架的 lib,插件组件继承框架 lib 的 Base 组件。而这个框架通过字节码操作动态生成一个子类去继承插件组件解决插件必须依赖框架的问题,从而达到插件无需做任何改动(理论上)即可加载的效果。

最近的:

RadioGroup.check()的坑

问题描述:

我使用RadioGroup.check()方法来初始化radioGroup,然后我在onCheckedChanged()里去根据选择的radioButton来发送网络请求,并且新建fragment。这时候我发现一个问题:这个radiobutton下的网络请求会发送两次,而且fragment也会创建两次,而且那个新建的fragment里还有很多的初始化过程和网络请求,所以会让用户一直处在等待的状态。

开始时候各种找原因,以为是我在哪里调用了太多次,还是共用layout文件有相同的id所以被别的地方的调用影响了?都不是,最后在stackoverflow上找到了答案:使用RadioGroup.check()会调用onCheckedChanged()方法三次,而我在onCheckedChanged()方法里判断了调用的是哪个id,然后发送请求创建fragmeng,所以只调用了两次。

statckoverflow原文链接:

http://stackoverflow.com/questions/10263778/radiogroup-calls-oncheckchanged-three-times

RadioGroup.check()源码如下:

http://www.grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/widget/RadioGroup.java#RadioGroup.check%28int%29

解决方法 :

使用radioBotton.setChecked()方法来解决

java io

java io继承关系图:

inputStream vs Reader

http://stackoverflow.com/questions/4367539/what-is-the-difference-between-reader-and-inputstream
An InputStream is the raw method of getting information from a resource. It grabs the data byte by byte without performing any kind of translation. If you are reading image data, or any binary file, this is the stream to use.
A Reader is designed for character streams. If the information you are reading is all text, then the Reader will take care of the character decoding for you and give you unicode characters from the raw input stream. If you are reading any type of text, this is the stream to use.
You can wrap an InputStream and turn it into a Reader by using the InputStreamReader class.
Reader reader = new InputStreamReader(inputStream);

solution:

如果是读取字符类文件,可以使用Reader和Writer,不用担心编码问题。
如果是读取二进制文件,图片等等可以使用InputStream和OutputStream。

some useful git command

不提交一个文件(已经在仓库中,并且不想将远程仓库中的文件删除)在本地的改变

GIT: ignoring changes in tracked files29/01/2009

There may be times when you want to edit some variables in for example a database connection file, to run an application right from within your GIT repo. Of course you don’t wont those changes to be commited, so you add the file the .gitignore.
However adding tracked files to .gitignore won’t work because GIT will still track the changes and commit the file if you use the -a parameter.
Fortunately GIT has a very easy solution for this, just run the following command on the file or path you want to ignore the changes of:

git update-index --assume-unchanged <file>

If you wanna start tracking changes again run the following command:

git update-index --no-assume-unchanged <file>

参考资料:

http://blog.pagebakers.nl/2009/01/29/git-ignoring-changes-in-tracked-files/

AysncTask源码解析

AsyncTask的类图

  • 其中,如果使用默认的Executor其实AsyncTask中的任务是一个一个执行的。
    其中灰色的是接口
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
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}

protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}

从这段代码中可以看出来。
如果使用Thread_pool_executor那么就会使用线程池来执行你的task。
注释中也有:

Order of execution

When first introduced, AsyncTasks were executed serially on a single background
thread. Starting with {android.os.Build.VERSION_CODES#DONUT}, this was changed
to a pool of threads allowing multiple tasks to operate in parallel. Starting with
android.os.Build.VERSION_CODES#HONEYCOMB, tasks are executed on a single
thread to avoid common application errors caused by parallel execution

If you truly want parallel execution, you can invoke
executeOnExecutor(java.util.concurrent.Executor, Object[])} with
THREAD_POOL_EXECUTOR

  • 从类图可以看出来AsyncTask主要使用了组合的方式来使用代码。

  • 理解代码的一个方式是画类图,然后找一个入口然后看下去,看他是如何调用的。

idea上一些实用的快捷键

这里列出的是我自认为使用比较多比较实用的一些,并不全,如果想看全的,可以推荐这一篇:
http://www.cnblogs.com/tonycody/p/3257601.html

  • ctrl + P
    列出参数列表(使用比较多 )

  • ctrl + shift + enter
    当在括号里输入完最后一个参数时候他会直接光标跳到最后并且添加分号,不需要自己按向左键移动光标。(使用比较多 )
    ctrl + enter 向下插入一行,光标位置不变

  • shift + enter
    向下插入一行。光标位置进入下一行(可以在一行的任何位置换行,而不必将光标移动到行尾,使用比较多)

  • ctrl+left/right
    在单词间跳动光标,基本在所有的编辑器中都是适用 (使用比较多 )

  • ctr+shitf+left/right
    快速选中,基本在所有的编辑器中都是适用(shift选中,ctrl+left/right在单词中快速跳动,使用比较多 )

  • Alt+left/right,切换代码视图(使用比较多 )

  • Alt+Up/Down,在方法间快速移动定位(使用比较多 )

  • F2 或 Shift+F2,高亮错误或警告快速定位(使用比较多)

  • Ctrl+Alt+T
    可以把代码包在一个块内,例如:try/catch(使用比较多 )

  • Ctrl+F,处于查找状态下按down向下查找,up向上查找(使用比较多)

  • Ctrl+F12,可以显示当前文件的结构(快速查找当前类中某个方法,使用比较多)

  • Ctrl+Shift+W,取消选择光标所在词(使用比较多)

  • Ctrl+[ OR ],可以跑到大括号的开头与结尾
  • Ctrl+Shift +[ OR ],选择大括号中的内容

  • alt + 1 同上(在工程目录和编辑页面间切换,使用比较多)

  • Ctrl+Alt+O,优化导入的类和包(去掉没有使用的import,使用比较多 )

  • Shift+F6,重构 - 重命名(使用比较多 )

  • Ctrl+G,定位行

  • Ctrl+H,显示类结构图(类的继承层次)

  • Ctrl+U,转到父类

  • Ctrl+”+/-“,当前方法展开、折叠

  • Ctrl+Shift+”+/-“,全部展开、折叠

  • Ctrl+Shift+J,整合两行

  • ctrl + shift + space
    当new 一个变量的时候,已经写了变量类型,打了等号之后按这个可以快速完成。

gralde 循环依赖的问题

今天遇到一个问题,添加一个EventBus依赖包之后运行一直报:‘java.exe finished with non-zero exit value 2 ’

在statckoverflow上找到了问题的答案:

First of all you should try to list your dependencies with gradle :MODULE:dependencies Check if there are libraries conflicts ( same library but different version ). In this case i supose you should exclude support library module from Facebook SDK.

compile ('com.facebook.android:facebook-android-sdk:3.23.1'){    
    exclude group: 'com.google.android', module: 'support-v4'    
}

解决方法

在添加EventBus的依赖的地方添加如下代码:

compile ('de.greenrobot:eventbus:2.0.1'){
    exclude group: 'com.google.android', module: 'support-v4'
    exclude group: 'com.google.android', module: 'annotations'
    exclude group: 'com.google.android', module: 'android-test'
}

listView 闪动的问题

我仿做一个网易新闻客户端,因为每一行有不同的内容,所以需要使用getItemViewType来区分不同的行的类型

然而我的东西出现了一个很奇怪的问题:

在滑动的时候会出现一行快速的闪动(快速变换不同的内容)

分析问题原因:

一开始以为我是因为在viewpager里嵌套viewpager惹的祸,
或者是使用开源的下拉刷新框架惹的祸,
或者是自己使用Viewholder的姿势不对,一开始主要是怀疑getView里面的东西用的不对。

求证过程:

我新写一个Activity并没用用到ViewPager,而且我也将原来的adapter的GetView方法重写,由于类型比较多,所以还将变量的名字改变了怕自己哪里写错了。但是依然还是闪动。

真实的原因:

然后我就发现我自己的Viewholder里面的组件也是static,因为Viewholder是要复用的,所以需要为static的,但是如果把里面的元素也写成是static的就会有问题。就像上面那样的闪动的问题。


本站总访问量次,本站访客数人次,本文总阅读量