Flutter app stuck at installing with white screen

Problem Background and Symptoms:

A while ago, I tried setting up the Flutter environment and had a brief experience with it. After a long break, I recently became interested again and wanted to see what changes Flutter has undergone.

  1. I followed the official tutorial to set up the Flutter environment and ran flutter doctor without any issues.
  2. I downloaded a demo app project and tried running it on iOS. After the compilation completed, the app was installed on the simulator, but it got stuck on a white screen.
  3. After killing the app, I manually opened it, and it worked fine.

Attempts and Solutions:

  1. I tried installing it on an Android emulator, but encountered the same issue ❌.
  2. I attempted to run flutter clean to clear the cache, but the problem persisted ❌.
  3. I tried running flutter pub cache clean to clear the dependency cache, but the problem remained ❌.
  4. Running flutter run -v to view the output, I noticed that it got stuck at the line Connecting to service protocol:http://127.0.0.1:57071/0dqLN-ZPpFk=/. I suspected an issue with port 57071, so I ran lsof -i:57071 to check the process ID occupying that port and then killed the process using kill <pid>. However, the issue persisted, and I realized that the service changed ports every time, so the problem wasn’t related to that ❌.
  5. Suddenly, I noticed that every time I ran a Flutter command, the first line was Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!. I remembered that I had changed the source due to a poor network environment in the past. Could it be an issue with this source? I quickly checked the ~/.zshrc file and found the following configuration:
    1
    2
    3
    export PUB_HOSTED_URL=https://pub.flutter-io.cn
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
    export FLUTTER_GIT_URL=https://gitee.com/mirrors/Flutter.g
    I deleted these lines, restarted VSCode, and ran the command again. The problem was resolved. In retrospect, it seems that the content of this source was outdated or incorrect! ✅

In summary, one should not easily trust third-party sources. Configuring the network environment correctly is always the first step in setting up the environment.

After searching on Stack Overflow for a while, I couldn’t find any answers pointing to the source. I’m documenting this experience here, hoping it can help others facing the same issue. ♥️

If the above solution doesn’t resolve your problem, you can refer to:

https://stackoverflow.com/questions/75665760/flutter-run-stuck-on-launching

https://stackoverflow.com/questions/68698437/flutter-app-stuck-at-installing-with-white-screen

flutter运行后白屏卡住无法启动 / Flutter app stuck at installing with white screen

问题背景和症状:

很久之前尝试配置过一次flutter环境,体验了一下。很久没用了,最近有了兴趣,又想看下flutter有什么变化。

  1. 按照官方教程装好了flutter环境,运行flutter doctor无异常项
  2. 下载了一个demo app工程,点击运行(ios),编译完成后,安装到了模拟器上,app自动打开了,但是卡在白屏。
  3. 杀死app后,手动打开能正常进入。

尝试和解决

  1. 尝试安装到android模拟器上,也是同样的症状❌
  2. 尝试运行flutter clean清除缓存,问题没有解决❌
  3. 尝试运行flutter pub cache clean清除依赖缓存,问题没有解决❌
  4. 运行flutter run -v查看输出信息,发现最后是卡在Connecting to service protocol:http://127.0.0.1:57071/0dqLN-ZPpFk=/这一行了,怀疑是57071这个端口有问题,运行lsof -i:57071查看占用这个端口的进程的pid,然后运行kill <pid>杀掉这个进程。再次运行发现还是不行,并且发现这个service每次都会换个端口,问题应该不是在这。❌
  5. 忽然留意到每次运行flutter命令的时候,第一句都是Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!,想起来以前因为网络环境不好,换过源,不会是这个源有问题吧,于是赶紧查看~/.zshrc文件,果然在里面发现了这个配置
    1
    2
    3
    export PUB_HOSTED_URL=https://pub.flutter-io.cn
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
    export FLUTTER_GIT_URL=https://gitee.com/mirrors/Flutter.g
    删掉这几行,然后重启vscode,再次运行,问题就解决了。回顾一下应该就是这个源的内容不够新甚至不正确导致的!✅

总结,不能轻易相信第三方源,配置好网络环境永远是配置环境的第一步

stackoverflow上查了半天,没有一个将问题指向源的,在此做下记录,希望能帮助到遇到同样问题的人。♥️

如果上面没有解决你的问题,可以参考:

https://stackoverflow.com/questions/75665760/flutter-run-stuck-on-launching

https://stackoverflow.com/questions/68698437/flutter-app-stuck-at-installing-with-white-screen

每日单词计划

前言

工作后很难过的发现自己学了那么多年的英语正在一点点的还给老师。日常工作之余会每天记录几个遇到的生疏单词,本篇主要记录下我平时背过的单词,以便日后查询。本文不会给出释义。

由于19年的单词都是抄在纸上的,未能按天归类,20年2月26号开始按天归类。

Read more

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/

记一次overloadaggressively造成的崩溃

将Kotlin版本从1.1.2-4升级到1.3.11后,发现打的release包开机就会崩溃:崩溃日志为

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
java.lang.RuntimeException: Unable to instantiate application com.tencent.sigma.patch.HotPatchApplication: java.lang.ClassNotFoundException: Didn't find class "com.tencent.sigma.patch.HotPatchApplication" on path: DexPathList[[zip file "/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk"],nativeLibraryDirectories=[/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/lib/arm, /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk!/lib/armeabi, /system/lib, /vendor/lib]]
at android.app.LoadedApk.makeApplication(LoadedApk.java:993)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5801)
at android.app.ActivityThread.-wrap1(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1683)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:173)
at android.app.ActivityThread.main(ActivityThread.java:6650)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:818)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.tencent.sigma.patch.HotPatchApplication" on path: DexPathList[[zip file "/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk"],nativeLibraryDirectories=[/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/lib/arm, /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk!/lib/armeabi, /system/lib, /vendor/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:125)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at android.app.Instrumentation.newApplication(Instrumentation.java:1088)
at android.app.LoadedApk.makeApplication(LoadedApk.java:987)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5801) 
at android.app.ActivityThread.-wrap1(Unknown Source:0) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1683) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loop(Looper.java:173) 
at android.app.ActivityThread.main(ActivityThread.java:6650) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:818) 
Suppressed: java.io.IOException: Failed to open dex files from /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk because: Failure to verify dex file '/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk': Out-of-order annotation_element name_idx: 7150 then 7150
at dalvik.system.DexFile.openDexFileNative(Native Method)
at dalvik.system.DexFile.openDexFile(DexFile.java:353)
at dalvik.system.DexFile.<init>(DexFile.java:100)
at dalvik.system.DexFile.<init>(DexFile.java:74)
at dalvik.system.DexPathList.loadDexFile(DexPathList.java:374)
at dalvik.system.DexPathList.makeDexElements(DexPathList.java:337)
at dalvik.system.DexPathList.<init>(DexPathList.java:157)
at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:65)
at dalvik.system.PathClassLoader.<init>(PathClassLoader.java:64)
at com.android.internal.os.ClassLoaderFactory.createClassLoader(ClassLoaderFactory.java:73)
at com.android.internal.os.ClassLoaderFactory.createClassLoader(ClassLoaderFactory.java:88)
at android.app.ApplicationLoaders.getClassLoader(ApplicationLoaders.java:69)
at android.app.ApplicationLoaders.getClassLoader(ApplicationLoaders.java:35)
at android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:695)
at android.app.LoadedApk.getClassLoader(LoadedApk.java:729)
at android.app.LoadedApk.getResources(LoadedApk.java:956)
at android.app.ContextImpl.createAppContext(ContextImpl.java:2282)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5723)
... 8 more

乍一看,以为是HotPatchApplication被混淆的原因导致Class Not Found,于是尝试keep住HotPatchApplication这个类,但是发现问题依旧。于是又怀疑是前不久升级gradle对分包造成了影响,对apk中的dex文件进行分析,发现HotPatchApplication这个类稳稳的躺在第一个dex文件中。

后来又进行了以下的尝试:

​ 1.尝试按顺序更改Kotlin版本,最后,当Kotlin版本为1.3.0时,发现问题出现了。

​ 2.另一个线索是当我打开Proguard时,那就是minifyEnable true,崩溃就出现了。

经过以上排查,基本确定了问题出在Kotlin和混淆的身上。最后发现上述日志中有一行:

1
Suppressed: java.io.IOException: Failed to open dex files from /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk because: Failure to verify dex file '/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk': Out-of-order annotation_element name_idx: 7150 then 7150

这个报错是第一次见到,上网搜了下,最终定位在混淆时的一个参数身上:-overloadaggressively

ProGuard官网解释:

-overloadaggressively Specifies to apply aggressive overloading while obfuscating. Multiple fields and methods can then get the same names, as long as their arguments and return types are different, as required by Java bytecode (not just their arguments, as required by the Java language). This option can make the processed code even smaller (and less comprehensible). Only applicable when obfuscating.

可以看出,开启该选项可能把不同的方法或者变量混淆成相同的名字,这样会尽可能的减小代码体积。但是由于过于激进,造成了dex文件中有重复annotation_element,这个问题在编译和打包时并不会报错,只有在开启软件时才会发现。

至于为什么升级了Kotlin才有这个问题,猜测原因是新版本的kotlin中加入了Keep注释与Java的Keep注释同名造成的。

解决办法:

去掉混淆文件(proguard.txt)中的-overloadaggressively

参考:

https://stackoverflow.com/questions/56458360/app-crashed-when-r8-enabled-with-existing-proguard-settings

https://issuetracker.google.com/issues/129241209#comment11

https://stackoverflow.com/questions/51948250/failure-to-verify-dex-file-out-of-order-annotation-element-name-idx

初识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

Your browser is out-of-date!

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

×