Git保命大全

💥我不小心删除了分支 或者 删除了Stash 或者 丢失了提交(reset —hard) 怎么办!

操作前,建议备份一下当前的分支,或者切换到新的分支进行操作。

1/ 确定正确的SHA-1值

首先,你需要找到分支或者提交的SHA-1值,如果你已经知道丢失分支的SHA-1值,可跳过这个步骤。

通过git reflog命令查找对git作出过的修改,可以查询到HEAD的变更记录,如commit, rebase, checkout, reset等。示例如下:

1
2
3
4
5
6
7
8
># git reflog
a55a2725e HEAD@{846}: checkout: moving from calvinche/3650/voiceopt to calvinche/3650/bugfix
5271cecfc HEAD@{847}: commit: --story=855394891 【语音控制】语音控制功能优化
5413bd7ce HEAD@{848}: checkout: moving from calvinche/3650/bugfix to calvinche/3650/voiceopt
a55a2725e HEAD@{849}: commit: --bug=74100899 【畅听页卡】活动弹窗遮挡播放列表的问题
5413bd7ce HEAD@{850}: checkout: moving from calvinche/3650/voiceopt to calvinche/3650/bugfix
5413bd7ce HEAD@{851}: reset: moving to 5413bd7ce1a1ddfbd12406175a83c863f9d68da6
ff1200963 HEAD@{852}: commit: --story=855394891 【语音控制】语音控制功能优化

第一列就是所在提交的SHA-1值

如果通过reflog无法找到丢失的分支或者提交,那你还可以通过git fsck命令查找git数据库中所有丢失的分支信息:

1
git fsck --full --no-reflogs --unreachable --lost-found | grep commit | cut -d\  -f3 | xargs -n 1 git log -n 1 --pretty=oneline > ~/lost-found.txt

由于输出的结果通常较大,我们一般将命令的输出重定向到一个文本文件中进行查看,其输出的结果如下所示:

1
2
3
4
5
6
e005fc257bb96252e9bc84deb255bba28f829e63 --story=854880635 【技术优化】升级gradle: 修复打包无产物
b90bace85ba53c9a229f84a458935aeb6858bc6c --story=854880635 【技术优化】升级gradle: 修改build_patch失败
9b51c859899d0699a73c699fb349d8ff952cf7ae WIP on master: c5312d039 Merge branch 'calvinche/dev/car_adapt' into 'master'
aa0c44873d24bc07487e4e5b8883189573c55029 --story=64610857 【语音交互】播放控制功能:UI走查
d40e8c2ed3122a30164c82696a72bf4d5fbd8c58 --story=854880635 【技术优化】升级gradle: 修复打包无产物
ed0f98b6ebd7bb9bbb6da21467fdea2931bab598 --story=64610857 【语音交互】播放控制功能:实现语音识别和命令执行

然后我们就可以通过提交的名字或者通过git log -p <commit>查看提交的内容或git cat-file -p <commit>查看提交的信息来确定我们要恢复的提交,确定了要恢复的提交后,复制它的SHA-1值备用。

2/ 恢复

如果你要恢复的是分支,使用类似于下面的命令:

1
git checkout -b <new branch name> <SHA-1>

如果你要恢复的是提交,那么可以直接:

1
git reset --hard <SHA-1>

💥我的提交历史重复臃肿,杂乱无章怎么办!

有时候,在开发过程中,可能被产品或设计打断,或者前期考虑不够严谨导致后面补充了过多的修补提交,这时候可以通过Rebase的交互模式,对提交历史进行“美化”。

例如下面一个提交记录:

1
2
3
4
5
--story=854849047(d01f70f39)【技术优化】fresco图片库更新 hypertian* 2019-05-23 13:12
--story=64746051 【个人中心】增加开机兴趣选择修改: UI走查修改1 calvinche 2019-05-20 22:25
--story=64746051 【个人中心】增加开机兴趣选择修改: 产品补充逻辑1 calvinche 2019-05-23 15:59
--story=64746051 【个人中心】增加开机兴趣选择修改: UI走查修改2 calvinche 2019-05-21 14:50
--story=64746051 【个人中心】增加开机兴趣选择修改:产品补充逻辑2 calvinche 2019-05-23 18:25

可以看出,UI修改和逻辑修改提交了多次,这个时候如果把两个UI走查的提交合并,把两个补充逻辑的提交合并,提交历史看起来会清楚干净很多,于是,我们使用rebase操作来整理提交记录:

1/ 进入Rebase交互模式

找到要修改提交的上一条提交的SHA值,如上面例子,要对下面四个提交进行修改,则需要找到”fresco图片库更新”这个提交的SHA值:d01f70f39,然后运行:

1
git rebase -i d01f70f3

就进入到下面的页面:

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
pick 39741ee1a --story=64746051 【个人中心】增加开机兴趣选择修改:  UI走查修改1
pick 8078fa8f9 --story=64746051 【个人中心】增加开机兴趣选择修改: 产品补充逻辑1
pick 6f830afa3 --story=64746051 【个人中心】增加开机兴趣选择修改: UI走查修改2
pick 5efe76189 --story=64746051 【个人中心】增加开机兴趣选择修改:产品补充逻辑2

# Rebase d01f70f39..5efe76189 onto d01f70f39 (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

2/ 使用合适的命令对提交记录进行修改

每次提交按照时间升序排列,也就是最上面的提交是最早的提交。每个提交前面是要进行修改的命令,下面列出了所有可用的命令的名字及其功能,默认是pick,也就是保留这条提交,什么也不做的意思。这里解释一下几个常用的命令:

reword:重命名提交信息

edit:编辑提交的内容,可以用于添加或移出提交的文件

squash:将当前提交和前一条提交合并,使用后,会跳到另一个页面,编辑合并后的提交信息

fixup:将当前提交和前一条提交合并,使用后,丢弃当前的提交信息,直接以上一条提交信息为合并后的提交信息

drop:删除当前的提交

⚠️ 需要注意的是这句话:

These lines can be re-ordered; they are executed from top to bottom.

意思是说,改变上面一行行的提交记录的顺序是可以改变最终的提交顺序的,不过需要注意的是,改变顺序可能会带来conflict,如果遇到冲突,需要解决冲突后才能继续Rebase。

可以看下视频中的演示消化理解

rebase interactively
3/ 恢复到Rebase前

如果你rebase结束后发现并不是自己想要的结果,这个时候怎么恢复到rebase之前呢?很简单,运行下下面这个命令即可:

git reset --hard ORIG_HEAD

In case ORIG_HEAD is no longer useful, you can also use the branchName@{n} syntax, where n is the nth prior position of the branch pointer. So for example, if you rebase featureA branch onto your master branch, but you don’t like the result of the rebase, then you can simply do git reset --hard featureA@{1} to reset the branch back to exactly where it was before you did the rebase. You can read more about the branch@{n} syntax at the official Git docs for revisions.

⚠️⚠️⚠️ Rebase的最基本原则

既然Rebase命令可以删除和编辑历史提交,那同样也有一个重要的原则:

永远不要对已经推到主干分支服务器或者团队其他成员的提交进行Rebase,我们选择Rebase范围应该在自己本地工作范围内。

否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

参考:

[1] https://www.internalpointers.com/post/squash-commits-into-one-git

[2] https://ohshitgit.com/

初识ViewPager2

ViewPager2是Google于2019年2月7号发布的ViewPager的升级版本,目前最新版本是1.0.0-alpha01。ViewPager2主要解决了传统ViewPager中的几个问题:

  • 不支持从右到左布局
  • 不支持纵向滚动
  • notifyDataSetChanged()有时不起作用的Bug

实现原理

查看ViewPager2内部代码可以看出,其实现原理是在其内部封装了一个RecyclerView,利用LinearLayoutManager+PagerSnapHelper实现了ViewPager的滚动效果以及对纵向滚动的支持。

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

private void initialize(Context context, AttributeSet attrs) {
mRecyclerView = new RecyclerView(context) {
...
};

mLayoutManager = new LinearLayoutManager(context);
mRecyclerView.setLayoutManager(mLayoutManager);
setOrientation(context, attrs);

new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
...
}
}

目前存在的问题:

由于直接使用的RecycleView实现,一些ViewPager的特性没有得到完全实现,官方指出了下面几个问题:

  • 不支持设置offscreen limit
  • 与TabLayout的集成存在问题
  • FragmentStateAdapter还有稳定性问题
  • 不支持设置pageWitch,默认100%/100%
  • 不支持clipToPadding
  • 不支持fakeDrag
  • 设置page transformer时,不支持设置绘制顺序

使用方法:

其使用方法与传统ViewPager类似:

1.在模块gradle中添加依赖:

1
implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha01'

2.在Layout文件中布局,这里可以设置滚动方向:

1
2
3
4
5
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />

3.创建Adapter,ViewPager2使用FragmentStateAdapter来实现Fragment的管理:

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

public ViewPager2Adapter(@NonNull FragmentManager fragmentManager) {
super(fragmentManager);
}

@NonNull
@Override
public Fragment getItem(int position) {
return new SimpleFragment();
}

@Override
public int getItemCount() {
return 10;
}
}

4.应用Adapter:

1
2
3
viewPager2 = findViewById(R.id.viewpager2);
ViewPager2Adapter adapter = new ViewPager2Adapter(getSupportFragmentManager());
viewPager2.setAdapter(adapter);

总结

ViewPager2具有原生支持垂直方向滑动的特性,但是由于目前FragmentStateAdapter还有稳定性问题,以及设置page transformer时,不支持设置绘制顺序,在实际应用时还需要多进行修改和测试。建议等官方推出稳定版本后,再考虑接入。

参考:

[1] https://developer.android.com/jetpack/androidx/releases/viewpager2

[2] https://developer.android.google.cn/reference/androidx/viewpager2/adapter/FragmentStateAdapter

[3] https://developer.android.google.cn/reference/androidx/viewpager2/widget/ViewPager2

[4] https://www.journaldev.com/26148/android-viewpager2

Android Studio的三种类型的模版(Templates)创建

Android Studio的三种类型的模版(Templates)创建

如果说使用快捷键是程序员的刀🔪,那灵活的使用代码模版就应该是程序员的剑。

这里说的模版(Templates),是指在使用开发创建类文件,甚至是某些代码块时,IDE自动按照规定的格式创建出类或代码的功能。如果类中有大量相似代码,使用模版可以极大的提高开发效率,降低出错概率。

下面我们看一下如何在Android Studio上使用模版,我将讲述三种模版的创建,分别是:

  • 使用Live Templates创建代码块模版
  • 使用File and Code Templates创建类模版
  • 基于FreeMarker创建多文件模版

1.代码块模版

java中经常需要定义这样的静态常量:

1
private static final int DEFAULT_VALUE = 1;

使用Live Templates后,就可以直接输入const就可以快速定义静态常量:

其设置方法是,打开File->Setting(⌘+,),搜索“Live Templates”,打开如下的界面:

其中,1指的是缩写和该缩写的描述;2中输入的是模版的内容;3中可以指定该模版应用生效的语言和场所,例如,可以限制该模版只应用到Java语言定义变量(declaration)的时候; 4可以将模版中变化的部分定义为变量,如上图中的${name}${value}

上面例子中的const是默认的模版,你可以点击加号添加自己的模版,例如为kotlin定义一个tag常量的模版:

Read more

踩坑-复用RemoteView导致内存泄漏总结

复用RemoteView导致内存泄漏总结

关于RemoteView

RemoteView是一个提供跨进程控制的View,主要用在通知栏或者小部件的开发上。例如音乐类APP自定义的通知栏样式就是通过RemoteView实现的。如果你之前没有听说过RemoteView,可以在这里简单了解一下:

https://www.jianshu.com/p/23041852bd85

RemoteView中可以使用的布局和控件是受限制的,能用的布局有:

  • AdapterViewFlipper
  • FrameLayout
  • GridLayout
  • GridView
  • LinearLayout
  • ListView
  • RelativeLayout
  • StackView
  • ViewFlipper

可以用的控件有:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextClock
  • TextView

遇到的问题

最近开发了一个音乐类的App,播放音乐时会在通知栏常驻一个自定义样式的通知,其中通知栏有一个头像是通过setImageViewBitmap(int viewId, Bitmap bitmap)方法进行设置的:

1
2
3
4
5
6
RemoteViews mRemoteView;  // 全局变量

mRemoteView = new RemoteViews(Application.getInstance().getPackageName(), resId);

// 每次调用setImageViewBitmap来更新通知栏的头像
mRemoteView.setImageViewBitmap(R.id.cover, bitmap);

后来发现如果App连续放歌在3个小时左右时就会OOM崩掉,通过Profile检查内存后,发现是头像的bitmap没有销毁导致的,但是这里的bitmap对象每次使用完都会recycle掉,为什么还会内存泄漏呢?经过一番排查,发现是使用同一个RemoteVIew对象setImageViewBitmap(R.id.cover, bitmap)导致的。

RemoteView的源码中我们可以看到一个mActions变量,这是一个Action的列表:

1
2
3
4
5
/**
* An array of actions to perform on the view tree once it has been
* inflated
*/
private ArrayList<Action> mActions;

而Action是其内部定义的一个可序列化的类:

1
private abstract static class Action implements Parcelable 

通过跟踪setImageViewBitmap(int viewId, Bitmap bitmap)中bitmap的去向,发现最终调用了setBitmap()方法:

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 setBitmap(int viewId, String methodName, Bitmap value) {
addAction(new BitmapReflectionAction(viewId, methodName, value));
}

private class BitmapReflectionAction extends Action {
int bitmapId;
Bitmap bitmap;
String methodName;

BitmapReflectionAction(int viewId, String methodName, Bitmap bitmap) {
this.bitmap = bitmap;
...
}
...

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(TAG);
dest.writeInt(viewId);
dest.writeString(methodName);
dest.writeInt(bitmapId);
}

...
}

可以看出,每次setImageViewBitmap(),都会将Bitmap做成一个BitmapReflectionAction,并添加到mActions列表里,这里的BitmapReflectionAction是继承Action的一个可序列化的类,Bitmap在里面作为被序列化成了一组值最终存到了mActions列表中。在源码中,mActions列表只看到有添加操作,并没有看到remove或者clear操作,导致了内存泄漏。

解决

不要复用RemoteView,更新通知栏icon时,new一个新的RemoteView给NotificationCompat.Builder

参考

So internally RemoteViews is simply a set of actions that are “serialized” and sent to another process. Each time you make a call to something like setDouble(), you’re adding an additional action to RemoteViews’ internal list.

Because there isn’t a way of clearing these actions from a RemoteViews object, all of your successive setImageViewBitmap() calls, along with their Bitmaps, remain in the internal list, and are actually “serialized” and applied each time your send it. :( In this case it’s best to just create a new RemoteViews object every time.

https://github.com/rojdes/AngryDict/blob/master/app/src/main/java/me/rds/angrydictionary/widget/BinaryClockWidget.java

https://blog.csdn.net/u013989732/article/details/78501462

对使用ViewPager的一点理解

大家都用过ViewPager吧😄,使用ViewPager时,需要给它配一个Adapter,通常我们要继承下面三个Adapter来,分别是:

———>最基本的PagerAdapter

——————>继承PagerAdapter的FragmentPagerAdapter

——————>继承PagerAdapter的FragmentStatePagerAdapter

那么这三个Adapter有什么区别,我们应该怎么选择呢?

1.关于PagerAdapter

首先,我们知道如果继承了PagerAdapter,需要我们实现下面两个方法:

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

/**
* Return the number of views available.
*/
public abstract int getCount();


/**
* Create the page for the given position. The adapter is responsible
* for adding the view to the container given here, although it only
* must ensure this is done by the time it returns from
* {@link #finishUpdate(ViewGroup)}.
*
* @param container The containing View in which the page will be shown.
* @param position The page position to be instantiated.
* @return Returns an Object representing the new page. This does not
* need to be a View, but can be some other container of the page.
*/
public Object instantiateItem(ViewGroup container, int position) {
return instantiateItem((View) container, position);
}

第一个是要告诉ViewPager一共有几个页面,第二个则是ViewPager需要加载页面了,你需要根据position创建不同的页面对象,这里通常是View对象。

当然,如果你的ViewPager中内容比较复杂,需要用Fragment来自动管理其生命周期,那么可以使用FragmentPagerAdapter和FragmentStatePagerAdapter中的一种,那么他俩有什么区别呢?

首先我们看一下FragmentPagerAdapter的instantiateItemdestroyItem方法:

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
 	@Override
public Object instantiateItem(ViewGroup container, int position) {
...
final long itemId = getItemId(position);

// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
...
return fragment;
}


@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);
}

public long getItemId(int position) {
return position;
}


private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}

可以看出instantiateItem方法在添加Fragment时,会带一个Fragment的名字,其实就是Tag:

1
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

这个Tag是根据一定规则从makeFragmentName中获得的,这里唯一变化的就是id,默认的id可从getItemId(position)中看到,就是当前的position,保证了每个fragment的tag是唯一的。

当ViewPager通过instantiateItem获取Fragment的时候,会先根据之前设置的Tag找一下这个Fragment存不存在:

1
mFragmentManager.findFragmentByTag(name)

根据获取到的Fragment有两种添加方式,如果根据Tag找到了之前存在的Fragment,就attach上去:

1
mCurTransaction.attach(fragment);

如果没找到,那就把新创建的Fragment add上去:

1
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

当不用这个Fragment的时,会调用destroyItem方法,可以看到这里是detach掉了Fragment,并没有销毁。

综上,一个页面只会创建一次,创建时根据当前的position给Fragment设置一个Tag,当不需要时只是把Fragmetn detach掉,并不会销毁,下次需要时通过Tag复用Fragment。

所以,如果你有大量的Fragment要展示,FragmentPagerAdapter会持有每一个Fragment不释放,最终走向OOM。

所以,无论使用PagerAdapter还是FragmentPagerAdapter,有多少页面,就会创建多少页面对象,页面很多的情况下,会非常占用内存,虽然这样,但是它们也有各自的应用场景,例如,App的首次安装的启动引导页面个数是固定的,而且如果比较复杂,通常做成Fragment,这时使用FragmentPagerAdapter较为合适;而如果页面中有需要自动轮播卡片的地方,则可以使用PagerAdapter实现,因为其页面一般都是View,结构简单,数量也是固定的。

那如果我需要做一个可以无限滑动的卡片,就需要用到FragmentStatePagerAdapter了,我们看一下FragmentStatePagerAdapter的instantiateItemdestroyItem方法:

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

/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);

@Override
public Object instantiateItem(ViewGroup container, int position) {
...

if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}

Fragment fragment = getItem(position);
while (mFragments.size() <= position) {
mFragments.add(null);
}

...
mCurTransaction.add(container.getId(), fragment);

return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
...
mCurTransaction.remove(fragment);
}

可以看到,FragmentStatePagerAdapter在初始化页面时,通过getItem这个抽象方法来获得Fragment,并将其add到容器里,而不用的时候直接remove掉,这样就不会一直占用Fragment,可以实现无限滑动,唯一关心的就是在getItem中创建一个Fragment就可以了。

以上就是我对三种PagerAdapter的简单理解。在使用ViewPager的过程中,除了adapter的选择,还需要到了下面的问题。

2.常见的两个问题小析

1.notifyDataSetChanged()后页面没有刷新的问题

这个问题主要是由于当数据更新时,ViewPager会调用dataSetChanged方法:

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

void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null.

...
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
final int newPos = mAdapter.getItemPosition(ii.object);

if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}

if (newPos == PagerAdapter.POSITION_NONE) {
mItems.remove(i);
i--;

if (!isUpdating) {
mAdapter.startUpdate(this);
isUpdating = true;
}

mAdapter.destroyItem(this, ii.position, ii.object);
needPopulate = true;

if (mCurItem == ii.position) {
// Keep the current item in the valid range
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
needPopulate = true;
}
continue;
}

...
}

...
}

这个方法会调用adapter的getItemPosition()来获得object的位置情况是否发生变化,如果不发生变化就不更新了,如果而这个方法默认是返回的就是POSITION_UNCHANGED:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 /**
* Called when the host view is attempting to determine if an item's position
* has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
* item has not changed or {@link #POSITION_NONE} if the item is no longer present
* in the adapter.
*
* <p>The default implementation assumes that items will never
* change position and always returns {@link #POSITION_UNCHANGED}.
*
* @param object Object representing an item, previously returned by a call to
* {@link #instantiateItem(View, int)}.
* @return object's new position index from [0, {@link #getCount()}),
* {@link #POSITION_UNCHANGED} if the object's position has not changed,
* or {@link #POSITION_NONE} if the item is no longer present.
*/
public int getItemPosition(Object object) {
return POSITION_UNCHANGED;
}

所以要想每次刷新都让页面更新,需要在adapter中重写getItemPosition方法,并返回POSITION_NONE就可以了:

1
2
3
4
5

@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
2.初始化数据时不会调用OnPageChangeListener

相信大家都遇到过这种情况,就是ViewPager初始化时并不会调用OnPageChangeListener,所以很多时候我们都是手动调用第一页的onPageSelected方法:

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

private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

@Override
public void onPageSelected(int position) {

}

@Override
public void onPageScrollStateChanged(int state) {

}
};

public void OnCreate(){
...
mViewPager.setOnPageChangeListener(onPageChangeListener);
onPageChangeListener.onPageSelected(0); // 手动调用第一页
...
}

但是如果如果你使用的是FragmentPagerAdapter时,会发现在onPageSelected(0)时,会发现Fragment还没有创建成功,这时候会出现NPE(NullPointerException)。所以这时候可以这样写:

1
2
3
4
5
6
mViewPager.post(new Runnable(){
@Override
public void run() {
onPageChangeListener.onPageSelected(0); // 手动调用第一页
}
});

这样就会在viewpager的UI事件队列完成后处理onPageSelected方法的内容,这个时候Fragment已经好了。

但是,虽然这样,讲要进行的工作交给post会打乱同步时序,让要做的事充满了不确定性,我不知道它什么时候能调用onPageSelected方法。而且这样做也存在另外一个问题,当使用notifyDataSetChanged()刷新数据时,还是不会调用当前页的onPageSelected来更新当前页面,所以我更倾向于下面这种方法:

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
public class MyPagerAdapter extends FragmentStatePagerAdapter {
...

private Object lastPrimaryItem;

private OnPageSwitchListener onPageSwitchListener;
public MyPagerAdapter(FragmentManager fm, OnPageSwitchListener onPageSwitchListener) {
super(fm);
this.onPageSwitchListener = onPageSwitchListener;
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.finishUpdate(container);
super.setPrimaryItem(container, position, object);
if (object == lastPrimaryItem) {
return; // setPrimaryItem会多次调用,过滤掉多余的调用
}
lastPrimaryItem = object;
MyFragment fragment = (MyFragment)object;
if (onPageSwitchListener != null && fragment != null) {
onPageSwitchListener.onPageSwitch(position, fragment);
}
}

@Override
public void finishUpdate(ViewGroup container) {
// 这里将finishUpdate放到setPrimaryItem之前执行,见上面的方法👆
// 原因是finishUpdate后的Fragment才算正式可以用了
}

public interface OnPageSwitchListener{
void onPageSwitch(int position, MyFragment fragment);
}
}

class MyActivity extends Activity implements MyPagerAdapter.OnPageSwitchListener{

@Override
public void onPageSwitch(int position, MyFragment fragment) {
// Do whatever you like here. :)
}
}

简单说就是,PagerAdapter中有一个重要的方法叫setPrimaryItem,当一个页面显示的时候都会调用这个方法(多次),通过一些简单的处理,可以实现与mViewPager.setOnPageChangeListener()类似的功能,这样就不用每次调用onPageChangeListener.onPageSelected(0)或者使用Post操作了。

以上分析较为肤浅,有问题欢迎指正、补充,谢谢🙏

参考

[1] https://stackoverflow.com/questions/16074058/onpageselected-doesnt-work-for-first-page

[2] https://stackoverflow.com/questions/11794269/onpageselected-isnt-triggered-when-calling-setcurrentitem0

正则表达式上手

正则表达式是计算机世界里的基础技能,很多东西都是建立在正则表达式之上。学会使用正则表达式会帮助你理解更多,之前对正则表达式总是非常敬畏,感觉很难上手,直到看了老姚的《JavaScript 正则表达式迷你书》,才打开了新世界的大门,有时间的朋友可以去读一下,不要被书名中的JavaScript吓到,这本书其实跟某种语言关系不大。任意门

下面讲我认为书中较为重要的点记在下面:

首先,正则是什么?正则是匹配模式,要么匹配字符,要么匹配位置。(匹配可以理解为搜索的意思)

匹配字符:

一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。

其实现的方式是使用字符组。譬如[abc],表示该字符是可以字符“a”、“b”、“c”中的任何一个,中括号里可以写任意字符,包括空格,问号等。

匹配位置:

一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。

其实现的方式是使用量词。表示譬如{m,n},表示连续出现最少m次,最多n次

其实只要明白了匹配位置的方法{m,n}和匹配字符的方法[这里可以写任意多的字符],就可以实现99.999%的正则表达式问题。

至于其他的你看到关于正则表达式的知识都是建立在这两个基础能力之上的,要么是为了方便,创建了几个简单的写法,例如[0-5]=[012345],又例如\d=[0-9],这样就不用在方括号里写太多的字符,同时也增强了可读性。

下面就介绍一些为了方便实现上面两种匹配而产生的各种简写:

1.表示字符

如果字符组里的字符特别多的话,怎么办?假如我想匹配所有的字符,总不能把所有的字符都输入到中括号内吧?

所以,为了解决这个问题,正则表达式也可以写成这样的缩写:

  • 比如[123456abcdefGHIJKLM],可以写成**[1-6a-fG-M]*。用连字符”-“来省略和简写。(那么要匹配“a”、“-”、“z”这三者中任意一个字符,该怎么做呢?可以写成如下的方式:[-az]或[az-]或[a-z]。即要么放在开头,要么放在结尾,要么转义。)*

  • **[^abc]**,表示是一个除”a”、”b”、”c”之外的任意一个字符。字符组的第一位放”^”(脱字符),表示求反的概念

  • \d 就是[0-9]。表示是一位数字。记忆方式:其英文是digit(数字)。

  • \D 就是[^0-9]。表示除数字外的任意字符。

  • \w 就是[0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式:w是word的简写,也称单词字符。

  • \W 就是[^0-9a-zA-Z_]。非单词字符。

  • \s 就是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s是space character的首字母。

  • \S 就是[^ \t\v\n\r\f]。 非空白符。

  • . 就是[^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式:想想省略号…中的每个点,都可以理解成占位符,表示任何类似的东西。

    如果要匹配任意字符怎么办?可以使用[\d\D]、[\w\W]、[\s\S]和[^]中任何的一个。

2.表示位置(数量)

  • {m,} 表示至少出现m次。

  • {m} 等价于{m,m},表示出现m次。

  • ? 等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?

  • + 等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。
  • *** ** 等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。

举例说明🌰

明白了基本用法,就需要几个例子来实验一下了。如果要在目录中搜索文件夹名字中有Util的文件,而且只显示DreamReader文件夹中的结果,那我可以这样搜:

1
find ./* | grep -e ".*Dream.*StringUtil"

坑--TextView同时使用maxLines=1和ellipsize时偶发崩溃

今天发现项目的报错记录中排名第一的是ArrayIndexOutOfBoundsException错误,影响用户84,发生次数129。

报错堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1 android.text.StaticLayout.getLineTop(StaticLayout.java:878)
2 android.widget.TextView.getExtendedPaddingTop(TextView.java:1966)
3 android.widget.TextView.bringTextIntoView(TextView.java:7291)
4 android.widget.TextView.onPreDraw(TextView.java:5107)
5 android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:944)
6 android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2348)
7 android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1292)
8 android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6598)
9 android.view.Choreographer$CallbackRecord.run(Choreographer.java:800)
10 android.view.Choreographer.doCallbacks(Choreographer.java:603)
11 android.view.Choreographer.doFrame(Choreographer.java:572)
12 android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:786)
13 android.os.Handler.handleCallback(Handler.java:815)
14 android.os.Handler.dispatchMessage(Handler.java:104)
15 android.os.Looper.loop(Looper.java:194)
16 android.app.ActivityThread.main(ActivityThread.java:5682)
17 java.lang.reflect.Method.invoke(Native Method)
18 java.lang.reflect.Method.invoke(Method.java:372)

从错误堆栈里并没有找到特别有用的信息。

于是去看了下报错的设备信息:

| 机型设备 Top 5 | 系统版本 Top 5 | | ——————— | ——————————- | | 华为 TIT-TL00 11.26% | Android 5.1,level 22 31.40% | | 三星 GT-I908 26.48% | Android 4.1.2,level 16 20.48% | | 荣耀 CUN AL00 6.14% | Android 4.0.4,level 15 10.92% | | 华为 TAG-TL00 5.46% | Android 7.0 8.19% | | HTC T327T 4.44% | Android 4.0.3,level 15 7.85% |

可谓是五花八门,看来也不是特定机型或者特定系统的问题。

从日志和设备信息都分析不出问题,只能靠google了,通过不断的搜索,渐渐发现类似的崩溃跟TextView的两个属性有关系,分别是:

  • Android:maxLines
  • Android:ellipsize

根据查询的信息,得出了下面的结论:

如果同时设置了ellipsize和maxLine=1,就会在某些机型上产生崩溃(不是必先),报ArrayIndexOutOfBoundsException的异常。解决办法就是将maxLine=1换成已经废弃的singleLine=true。这是Android系统的一个bug,目前还没有解决。

下面查询到的相关记录:

If you try to use maxLines=1 with ellipsize, you shall get the following lint error (refer to this discussion)

Combining ellipsize and maxLines=1 can lead to crashes on some devices. Earlier versions of lint recommended replacing singleLine=true with maxLines=1 but that should not be done when using ellipsize

来源:https://code.luasoftware.com/tutorials/android/android-textview-singleline-with-ellipsis/


问题 Crash when using ellipsize=”start” (Jelly Bean) 的回复

ri…@gmail.com ri...@gmail.com #3Jul 12, 2012 08:17AM

I do notice that changing android:lines=”1” to android:singleLine=”true” causes the crash not to happen.

来源:https://issuetracker.google.com/issues/36950033


I faced what I suspect is the same problem in my own app. For me, it was happening because I was using android:ellipsize="start" without also using android:singleLine="true".

We had switched all of our android:singleLine="true" attributes to the recommended android:maxLines="1", but it turns out that there’s a bug in how the system calculates the ellipsis that is triggered if singleLine isn’t present.

So I believe you can solve this issue by simply adding android:singleLine="true" to your TextViews that are using ellipsize attrs.

来源:https://stackoverflow.com/questions/45487427/crashlytics-reporting-multiples-issues-for-textview-makesinglelayout

基于MainFramer进行远程编译(以Android开发为例)

一、故事背景(可略过)

以前,世界上只有两种电脑:笔记本和台式机。

上帝说,要有高性能的笔记本。于是,就有了MainFramer。

我就遇到了这种情况,上班后公司配的是MacBookPro的笔记本和一个Windows的台式机(PC),我曾尝试过使用MacBook作为主力机工作,MacOS的体验非常优秀,但是唯一的不足就是其在处理大型复杂任务时的速度远不如我那台高配的台式机,编译一次工程的时间差在3分钟左右,而我每天编译上20次就能差出一个小时,有这个时间提前让我下班多好。最重要的是,我只要一点击Build,整个机器就开始满负荷运行,除了温度飙升外,CPU也被占满,这时候开个网页都卡成了幻灯片。

为了提高工作效率,我尝试一直用台式机工作,于是我的MBP吃灰了很久,这么优秀的笔记本放在那里吃灰实在是心疼。最关键的是,台式机让我完全没有工作流的感觉,当我周末无聊想充电的时候,我发现我所有想看的东西都在公司,当我一个个软件打开,一个个网页从历史记录中找到后,我发现我已经没有了学习的欲望。这时候我非常想念用笔记本时那种“合盖走人”后,回家打开盖子发现我所有的思路都还在的感觉。

于是我急切的找一种方法,让简单的任务在笔记本上来做,复杂的任务能够利用台式机完成,并且在体验上做到“无差别”。终于,我在浏览全球最大的同性社交网站时惊喜的发现了mainframer,这是一个工具,可以将编译这件占内存和CPU的事从本地电脑放到远程电脑上来做。

简单的说,你只需要一台本地机器(Local Machine:例如我的性能不咋地的MacBook)和一台远程机器(Remote Machine:性能强悍的台式机或者云主机),通过mainframer,就可以实现在本地机器上写代码,在远程机器上编译代码。你可以在笔记本上写代码,到了需要调试的时候,mainframer会快速同步代码到远程机器并进行编译,并将编译的结果返回到你的笔记本,这一切仿佛都是在你的笔记本上进行的,你可以正常的build和调试程序。这样一来,就可以享受笔记本的便捷,又能享受台式机的高性能了,美滋滋。

下面就是我配置成功后,录制的在MacBookPro上写代码,在Linux发行版Manjaro上进行Build的视频:

传送门

Read more

数据结构之二叉树相关


二叉树的相关术语:

树的结点:包含一个数据元素及若干指向子树的分支; 孩子结点:结点的子树的根称为该结点的孩子; 双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲; 兄弟结点:同一双亲的孩子结点; 堂兄结点:同一层上结点; 祖先结点: 从根到该结点的所经分支上的所有结点子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙 结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推; 树的深度:树中最大的结点层 结点的度:结点子树的个数 树的度: 树中最大的结点度。 叶子结点:也叫终端结点,是度为 0 的结点; 分枝结点:度不为0的结点; 有序树:子树有序的树,如:家族树; 无序树:不考虑子树的顺序; 参考

Read more
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×