「Learn」开发记录

本文最后更新于:2020年2月20日 上午

开发App过程中遇到的一些问题和解决办法。临时记录一些解决方案。

音频

Android MediaPlayer基础。
在线音频播放,使用MediaPlayer。
下载在线音频到本地,使用URLConnection。

自定义ViewGroup

继承自LinearLayout,自定义子View的排布方式。

crash ViewGroup.resetResolvedLayoutDirection

给LinearLayout addView的时候报错

1
2
E/AndroidRuntime:     at android.view.ViewGroup.resetResolvedLayoutDirection(ViewGroup.java:7291)
at android.view.ViewGroup.resetResolvedLayoutDirection(ViewGroup.java:7291)

检查代码,发现addView的时候把LinearLayout自己添加进去了。

1
2
final LinearLayout wordCube = new LinearLayout(this);
wordCube.addView(wordCube, chTvParams);

改成

1
2
final LinearLayout wordCube = new LinearLayout(this);
wordCube.addView(tv, chTvParams); // 要加的是tv

Bugly热更新

Bugly热更新方案集成了腾讯的tinker,自带了补丁包发布平台。

文档

https://bugly.qq.com/docs/user-guide/instruction-manual-android-hotfix/?v=20181014122344#_3

tinker不支持热更新新增四大组件,不能修改Manifest文件。但是可以修改四大组件里面的逻辑。
可以修改layout文件和资源文件。

Assets遍历文件

assets里存放着四千多个文件,红米6A遍历一次要2秒多。

1
String[] list = context.getAssets().list("folder"); // 执行这一句要两千多毫秒

语音识别方案

主力方案为百度语音识别。

综合价格考虑,将科大讯飞的语音听写作为备用方案。

将百度语音识别与讯飞听写的SDK一起引入到App中。由后台控制用户使用哪一个语音引擎。

下载文件

项目采用的是mvvm架构。有2个页面要用到同一个数据源。把这个数据源单独抽出来,设计监听器。

原框架的下载文件功能有一个bug。如果下载时抛出了异常,也会调用success回调。
这里是在下载时记录目标文件的长度,在success回调中检查本地文件大小与这个长度是否一致。

限速下载

在io流那里进行延时操作。用Thread.sleep方法。
阻塞的是socket的操作。

下载安装apk

下载了新版本apk后,调用代码进行安装。根据手机系统版本的不同选择不同的安装方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static  void installApk(Context context,String downloadApk) {
Intent intent = new Intent(Intent.ACTION_VIEW);
File file = new File(downloadApk);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri apkUri = FileProvider.getUriForFile(context, "com.iNTGO.nndc.fileprovider", file);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
Uri uri = Uri.fromFile(file);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}
context.startActivity(intent);
}

对于小米4c android 5.1.1(API 22),如果把apk放在app内部存储,packageInstaller是无法安装的。要把apk放在公共存储中才能安装。

修改Android系统镜像(img)

装一个VMware Workstation Pro,下载一个Ubuntu 16的镜像(iso)。用的是阿里云的资源,比较快。
一系列的mount,打包后,刷机一直不成功。找到个ROM助手,尝试一下。修改了system.img后,线刷进去,卡米(卡在开机的MI logo界面)。
查一下发现,是selinux处于enforcing状态,没法装。小米4c用的是MIUI8,商家说没法root。MiUI7可以root。
还没找到非root情况下关闭selinux的方法。

动画效果

加一些动效会让界面更加生动有活力。

属性动画 - 星星飞行

控制星星飞入,定位,飞出。

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
        final int flyInTime = 100;
final int stayTime = 520;
final int flyOutTime = 250;
final int totalTime = flyInTime + stayTime + flyOutTime;
binding.starIv.setX(inX);
binding.starIv.setY(yStay);
binding.starIv.setVisibility(View.VISIBLE);
setStarIvSize(0, 0);
final ValueAnimator animator = ValueAnimator.ofInt(1, totalTime);
animator.setDuration(totalTime);
animator.setInterpolator(new AccelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (int) valueAnimator.getAnimatedValue();
ImageView starIv = binding.starIv;
if (value < flyInTime) {
float inProgress = value / (1.0f * flyInTime);
float x = inProgress * (xStay - inX) + inX;
float y;
if (value < flyInTime / 2) {
y = yStay + 20 - inProgress * 60;
} else {
y = yStay - 20 + inProgress * 60;
}
starIv.setX(x);
starIv.setY(y);
setStarIvSize((int) (mStarIvOriginWid * (inProgress)), (int) (mStarIvOriginHeight * (inProgress)));
// Log.d(TAG, "onAnimationUpdate: value: " + value + ", progress: " + inProgress + ", x: " + x);
} else if (value < flyInTime + stayTime) {
starIv.setX(xStay);
starIv.setY(yStay);
setStarIvSize(mStarIvOriginWid, mStarIvOriginHeight);
} else {
if (valueAnimator.isRunning()) {
float progress = (value - flyInTime - stayTime) / (1.0f * flyOutTime);
starIv.setX(xStay + (progress * Math.abs(endX - xStay)));
starIv.setY(yStay - (progress * Math.abs(endY - yStay)));
setStarIvSize((int) (mStarIvOriginWid * (1 - progress)), (int) (mStarIvOriginHeight * (1 - progress)));
}
}
if (value >= totalTime - 10) {
binding.starIv.setVisibility(View.GONE);
}
}
});
animator.start();

// 改变星星ImageView的大小,LayoutParams不能弄错了
private void setStarIvSize(int wid, int height) {
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) binding.starIv.getLayoutParams();
lp.width = wid;
lp.height = height;
binding.starIv.setLayoutParams(lp);
}

如果一个View在LinearLayout里,它的坐标没有那么容易获取。

下面的代码获取到的坐标是[0,0]。

1
2
3
4
5
6
7
imageView.post(new Runnable() {
@Override
public void run() {
int[] location = new int[2];
imageView.getLocationOnScreen(location);
}
});

创建自定义过渡动画 - Google
自动为布局更新添加动画 - Google

退出App

在登录界面,点击返回键即退出整个App。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private boolean mExitApp = false;

@Override
public void onBackPressed() {
super.onBackPressed();
mExitApp = true;
}

@Override
protected void onDestroy() {
super.onDestroy();
if (mExitApp) {
System.exit(0);
}
}

crash

为了提高程序的健壮性,很多时候并不能过于相信服务器返回的结果。该加判空就判空。
如果服务器返回空的数据或者字段,要有对应的措施。

gc超时

该异常表示调用超时。

解决方案:一般是系统在gc时,调用对象的finalize超时导致

解决办法:
1.检查分析finalize的实现为什么耗时较高,修复它;
2.检查日志查看GC是否过于频繁,导致超时,减少内容开销,防止内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
android.content.res.XmlBlock$Parser.finalize() timed out after 10 seconds

android.content.res.AssetManager.xmlBlockGone(AssetManager.java:500)

android.content.res.AssetManager.xmlBlockGone(AssetManager.java:500)
android.content.res.XmlBlock.decOpenCountLocked(XmlBlock.java:63)
android.content.res.XmlBlock.access$1600(XmlBlock.java:34)
android.content.res.XmlBlock$Parser.close(XmlBlock.java:448)
android.content.res.XmlBlock$Parser.finalize(XmlBlock.java:454)
java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:191)
java.lang.Daemons$FinalizerDaemon.run(Daemons.java:174)
java.lang.Thread.run(Thread.java:818)

RuntimeException: Cannot create an instance of class

使用了MVVM的框架,创建viewModel时报错。检查发现忘记复写方法initViewModel

1
2
3
4
5
@Override
public MyViewModel initViewModel() {
MyAppViewModelFactory factory = MyAppViewModelFactory.getInstance(getApplication());
return ViewModelProviders.of(this, factory).get(MyViewModel.class);
}

ANR

死循环导致的ANR

之前业务逻辑中,有一个随机添加不重复字符串的功能。

1
2
3
4
5
6
7
8
while (needCheckList.size() < 3) {
Random rd = new Random();
String word = wordList.get(rd.nextInt(wordList.size()));
if(!needCheckList.contains(word)) {
needCheckList.add(word);
}
// .....
}

这段代码的效率不高。伪随机数不能保证高效地不重复地取到新的下标。
在某些性能较差的手机上,陷入多次循环后有可能导致anr。 anr message 表明此时CPU占用率超过100%。
我们使用Collections.shuffle(wordList);来代替伪随机数,也能实现随机取出字符串的效果。提高健壮性。

gradle

想在Terminal里使用gradlew命令,还得先在电脑上安装jdk。
现在(2019-11-18)想在官网下载个jdk,还得登录oracle账号。网速很慢,去别的地方下载jdk-8u181-windows-x64。

多渠道自动打包

假设我们有很多种渠道,每个渠道的manifestPlaceholders的内容都不同。

1
2
3
4
5
6
productFlavors {
xxx {
manifestPlaceholders = [XX_ID : "123",
XX_KEY : "key_key"]
}
}

之前渠道少的时候,可以点击gradle task一个个来打包。
现在渠道种类多了(比如二十多个),再一个个点击就很累。想要一键打包或者一行命令打包,有什么成熟好用的多渠道打包方式呢?
我们尝试了美团点评的walle,号称是Android Signature V2 Scheme签名下的新一代渠道包打包神器
试着接入walle的姿势可能不对,打包不成功。此时看到有人说不支持多渠道不同的包名和配置manifestPlaceholders,暂时先不使用walle。

已经使用了Bugly,有很多形如assembleXxxRelease的任务。
我们可以在终端命令行里执行gradlew命令来打包。
Windows环境下就是

1
gradlew assembleXxxRelease

那么写一个bat脚本,把这几十个渠道包按顺序一个个打包出来。

1
gradlew assembleXxxRelease && gradlew assembleYyyRelease && gradlew assembleZzzRelease

这个方法非常“暴力”,仅仅是替代了手动执行的过程。

框架

BindingCommand 问题

由于历史原因,App使用了一个MVVM框架。layout中可以绑定BindingCommand。
不知道是不是开发姿势不对,快速点击某个按钮时,对应的BindingCommand并不能立即响应。连续点击会错过点击事件。

1
2
3
4
5
6
public BindingCommand myCommand = new BindingCommand(new BindingAction() {
@Override
public void call() {
// my logic
}
});

而换用setOnClickListener可以立刻监听到每一个点击事件。
为了追求响应速度,在某些地方采用设置监听器的方式了。

界面UI

android 跑马灯重复抖动的解决方法

解决的方法,在跑马灯控件外层,再嵌套一个布局控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="55"
android:orientation="horizontal">

<com.rustfisher.view.MarqueeTextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:maxWidth="150dp"
android:singleLine="true"
android:text=""
android:textSize="10sp" />

</LinearLayout>

EditText划词选词弹出菜单

et可选,弹出了系统的菜单。

et不可选,弹出了自定义的菜单。

1
2
3
4
5
6
7
8
registerForContextMenu(mEt);
mEt.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
Log.d(TAG, "onCreateContextMenu: selected: " + mEt.getSelectionStart() + ", " + mEt.getSelectionEnd()); // 都是0
getMenuInflater().inflate(R.menu.et_menu, menu);
}
});

et可选,弹出了自定义菜单。

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
mEt.setCustomSelectionActionModeCallback(genActionModeCallback1());

private ActionMode.Callback genActionModeCallback1() {
return new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Log.d(TAG, "onCreateActionMode:"
+ " selected: " + mEt.getSelectionStart() + ", " + mEt.getSelectionEnd());
return true;
}

@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Log.d(TAG, "onPrepareActionMode: " + " selected: " + mEt.getSelectionStart() + ", " + mEt.getSelectionEnd());
menu.clear();
mode.getMenuInflater().inflate(R.menu.et_menu, menu);
return true;
}

@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Log.d(TAG, "onActionItemClicked: " + " selected: " + mEt.getSelectionStart() + ", " + mEt.getSelectionEnd());
mode.finish();
return false;
}

@Override
public void onDestroyActionMode(ActionMode mode) {
Log.d(TAG, "onDestroyActionMode: " + " selected: " + mEt.getSelectionStart() + ", " + mEt.getSelectionEnd());
}
};
}


1+手机可以用,但小米手机无法弹出自定义菜单。此法不能通用。

竖直的进度条

https://stackoverflow.com/questions/3926395/android-set-a-progressbar-to-be-a-vertical-bar-instead-of-horizontal

获取statusbar高度

在Activity中获取DecorView。通过DecorView的位置来判断statusBar的高度。Activity别设置成全屏的就好。

1
2
3
4
5
6
private void getStatusBarHeight() {
Rect rectangle = new Rect();
Window window = getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
Log.d(TAG, "getStatusBarHeight: " + rectangle.top);
}

让statusbar不占位置,并设置成透明背景。
底下的虚拟系统按键(Home,back,menu)不能受影响。

1
2
3
4
5
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>

drawable - vector assets

通过引入svg文件得到的drawable,layout中直接设置src使用drawable。小米4c和红米6A手机屏幕上图像错乱。
改变ImageView的大小不起作用。清楚as缓存也不起作用。

如果不在layout中设置,而是在代码中setImageResource则显示正常。

设计界面

去花瓣网上找灵感。
比如设计列表界面,可以给每个项目增加一个小背景。可以是颜色,可以是背景图。

网络请求

设计接口获取数据

项目里用OKHttp框架来进行网络请求。返回结果被转化成对象Entity
同一个服务器返回里装有相同结构的A,B,C对象。它们的名字不一样,GsonFormat的时候是分开成3个类的。
为了让代码更简洁,把这3个对象进行抽象。
一开始是做了一个抽象类,让这3个类继承。但是OKHttp那边会报错。

然后改用了接口的方式。设计的接口里有一些通用方法。在Entity里让那3个类都实现这个接口,然后在方法中返回我们要的数据。

AsyncTask

资源分配

AsyncTask背后有一个线程池。调用了execute()并不能保证任务立刻被执行。
换用Thread。

App设置

分屏设置

如果不进行设置,默认是允许分屏的。这里我们把分屏给禁止。

1
2
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="false"

添加在application标签里。

1
2
3
4
5
6
7
<application
android:name=".app.MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme">

adb

系统App

把apk放进root后的手机里,当做是系统app。

1
2
3
4
5
6
7
8
9
10
11
F:\IE\MyApp0826\app\libs>adb root
F:\IE\MyApp0826\app\libs>adb remount
remount succeeded

F:\IE\MyApp0826\app\libs>adb shell mkdir /system/priv-app/MyApp
F:\IE\MyApp0826\app\libs>adb shell chmod 755 /system/priv-app/MyApp
F:\IE\MyApp0826\app\libs>adb shell chmod 644 /system/priv-app/MyApp/MyApp.apk
F:\IE\MyApp0826\app\libs>adb shell chmod 755 /system/priv-app/MyApp/lib
F:\IE\MyApp0826\app\libs>adb shell chmod 755 /system/priv-app/MyApp/lib/arm
F:\IE\MyApp0826\app\libs>adb shell sync
F:\IE\MyApp0826\app\libs>adb shell reboot

范围内随机数

1
2
Random rand = new Random(seed);
int random_integer = rand.nextInt(upperbound-lowerbound) + lowerbound;

内存泄漏

Handler与单例

单例模式加上Handler。有人把handler直接交给单例。长生命周期的一直持有短生命周期的对象,没法回收造成内存泄漏。

viewModel中有一个handler,而handler被单例持有。handler是直接实例化的。

1
2
3
4
5
6
aHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
handleResult(msg);
}
};

handleResult方法中使用了viewModel的数据列表。

这样在新建一个viewModel的时候,单例持有旧的handler,handler持有的还是旧的那个数据列表。
内存中就有2份不一样的数据列表。

修复方案:
首先不能让单例持有这个handler。
其次退出viewModel的时候,把handler中的消息清空。


「Learn」开发记录
https://blog.rustfisher.com/2018/09/01/Dev-note/dev-note-app-learn/
作者
Rust Fisher
发布于
2018年9月1日
许可协议