本文最后更新于: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 )
STYLUS
检查代码,发现addView的时候把LinearLayout自己添加进去了。
1 2 final LinearLayout wordCube = new LinearLayout (this ); wordCube.addView(wordCube, chTvParams);
JAVA
改成
1 2 final LinearLayout wordCube = new LinearLayout (this ); wordCube.addView(tv, chTvParams);
JAVA
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" );
JAVA
语音识别方案主力方案为百度语音识别。
综合价格考虑,将科大讯飞的语音听写 作为备用方案。
将百度语音识别与讯飞听写的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); }
JAVA
对于小米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))); } 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(); 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); }
JAVA
如果一个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); } });
JAVA
创建自定义过渡动画 - 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 ); } }
JAVA
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 )
STYLUS
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); }
JAVA
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); } }
JAVA
这段代码的效率不高。伪随机数不能保证高效地不重复地取到新的下标。
在某些性能较差的手机上,陷入多次循环后有可能导致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
之前渠道少的时候,可以点击gradle task一个个来打包。 现在渠道种类多了(比如二十多个),再一个个点击就很累。想要一键打包或者一行命令打包,有什么成熟好用的多渠道打包方式呢? 我们尝试了美团点评的walle ,号称是Android Signature V2 Scheme签名下的新一代渠道包打包神器
。 试着接入walle的姿势可能不对,打包不成功。此时看到有人说不支持多渠道不同的包名和配置manifestPlaceholders ,暂时先不使用walle。
已经使用了Bugly,有很多形如assembleXxxRelease
的任务。 我们可以在终端命令行里执行gradlew命令来打包。 Windows环境下就是
1 gradlew assembleXxxRelease
BAT
那么写一个bat脚本,把这几十个渠道包按顺序一个个打包出来。
1 gradlew assembleXxxRelease && gradlew assembleYyyRelease && gradlew assembleZzzRelease
BAT
这个方法非常“暴力”,仅仅是替代了手动执行的过程。
框架 BindingCommand 问题由于历史原因,App使用了一个MVVM框架。layout中可以绑定BindingCommand。 不知道是不是开发姿势不对,快速点击某个按钮时,对应的BindingCommand
并不能立即响应。连续点击会错过点击事件。
1 2 3 4 5 6 public BindingCommand myCommand = new BindingCommand (new BindingAction () { @Override public void call () { } });
JAVA
而换用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 >
XML
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()); getMenuInflater().inflate(R.menu.et_menu, menu); } });
JAVA
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()); } }; }
JAVA
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); }
JAVA
让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 >
XML
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"
XML
添加在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" >
XML
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
CMD
范围内随机数1 2 Random rand = new Random (seed);int random_integer = rand.nextInt(upperbound-lowerbound) + lowerbound;
JAVA
内存泄漏 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); } };
JAVA
handleResult方法中使用了viewModel的数据列表。
这样在新建一个viewModel的时候,单例持有旧的handler,handler持有的还是旧的那个数据列表。 内存中就有2份不一样的数据列表。
修复方案: 首先不能让单例持有这个handler。 其次退出viewModel的时候,把handler中的消息清空。