常见问题总结
在广告机和开发版Android项目开发过程中的个人经验总结,分为以下几个栏目(需要结合工作以来的问题解决案例):
- 广告机屏幕适配
- 图片展示模糊、图片占用内存过高问题
- 动画抖动问题
- 白屏问题优化
- 常见OOM解决方案
- ANR问题解决方案
- 大量图片帧动画实现
- 动画流畅度经验总结
- APP开机启动方案
- Crash 日志收集、重启
- Rxjava背压使用
- 软键盘遮挡的问题
- 架构技术栈制作
- 内存采集打印框架
(查看热门书籍、博客优化目录)
广告机屏幕适配
广告机的参数
density:1.0
densityDpi:160
width:1080
height:1920
小米6的参数
density:3.0
densityDpi:480
width:1080
height:1920
根据以上参数可知,若使用dp来适配屏幕,根据公式px = dp x density 可知在小米6上,整个屏幕的宽度是360dp,在广告机上,整体屏幕的宽度达到1080dp。所以即使使用dp,依然不能解决屏幕分辨率的适配问题,现在提供以下几种方案供参考:
1.屏幕适配之dimen适配
我们可以使用尺寸限定符,针对不同的屏幕创建不同的dimen值
关于尺寸限定符
values-sw600dp
这里的sw代表smallwidth的意思,当你的屏幕的绝对宽度大于600dp时,屏幕就会自动调用layout-sw600dp文件夹里面的布局。
layout-w600dp
当你的屏幕的相对宽度大于600dp时,屏幕就会自动调用layout-w600dp文件夹里面的布局。
==注意:这里的相对宽度是指手机相对放置的宽度;即当手机竖屏时,为较小边的长度;当手机横屏时,为较长边的长度。==
对不同的屏幕创建不同的dimen值。
- res/values/dimens.xml
<resources>
<dimen name="button_length_1">180dp</dimen>
<dimen name="button_length_2">160dp</dimen>
</resources>
- res/values-sw600dp/dimens.xml
<resources>
<dimen name="button_length_1">540dp</dimen>
<dimen name="button_length_2">480dp</dimen>
</resources>
2.屏幕适配之百分比布局
示例代码:
<android.support.percent.PercentRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/top_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentTop="true"
android:background="#ff44aacc"
app:layout_heightPercent="20%"
app:layout_widthPercent="70%" />
<View
android:id="@+id/top_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/top_left"
android:background="#ffe40000"
app:layout_heightPercent="20%"
app:layout_widthPercent="30%" />
<View
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_below="@+id/top_left"
android:background="#ff00ff22"
app:layout_heightPercent="80%" />
</android.support.percent.PercentRelativeLayout>
3.屏幕适配之LayoutParams动态适配
原理和百分比布局相似,根据控件占据屏幕的尺寸的百分比,在代码中对控件的宽高进行动态设置。
- 获取手机屏幕宽高:
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
int screenWidth = dm.widthPixels;
int screenHeight = dm.heightPixels;
- 根据屏幕宽度为控件设置动态设置宽高:
imageView.setImageResource(R.drawable.newscar);
LayoutParams params = imageView.getLayoutParams();
params.height=screenWidth/10;
params.width =screenHeight/10;
示例代码:
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) logoIV.getLayoutParams();
params.width = 375 * mGlobalData.mScreenWidth / 1080;
params.height = 394 * mGlobalData.mScreenWidth / 1080;
params.topMargin = 470 * mGlobalData.mScreenHeight / 1920;
图片展示模糊、图片占用内存过高问题
Google官方指定按照下列标准进行区分:
名称 | 像素密度范围 |
---|---|
mdpi | 120dpi~160dpi |
hdpi | 160dpi~240dpi |
xhdpi | 240dpi~320dpi |
xxhdpi | 320dpi~480dpi |
xxxhdpi | 480dpi~640dpi |
需要注意的是:
- 对于高密度的手机,如果资源文件存放于低密度的文件夹中,那么高密度的手机会认为这张图片的像素密度不够,所以解压的时候会自动填充更多的像素信息。
- 对于低密度的手机,如果资源文件存放与高密度的文件夹中,那么低密度的手机会认为图片密度过高,而自己的密度达不到那么高,所以就会降采样,最终得到的位图也会变小。
所以,针对上面出现的问题我们应该注意以下几问题
1.避免将高分辨率的图放入低密度的资源文件中,否则高密度的手机加载图片时会认为这张图是一张低密度的图,进而填充更多像素点,浪费内存。
2.如果低分辨率手机加载高分辨率文件夹中的图片时,解压的图片会自动降采样,对于一些图片,清晰度会下降,有些线条多的图片会变得非常模糊,所以最好在低分辨率文件夹中放一套图,或者给那些不清晰的图片单独放。
我们可以通过几个工具对PNG图片进行压缩来达到瘦身的目的。
无损压缩 [ImageOptim]
ImageOptim是一个无损的压缩工具,它通过优化PNG压缩参数,移除冗余元数据以及非必需的颜色配置文件等方式,在不牺牲图片质量的前提下,既减少了PNG图片占用的空间,又提高了加载的速度。
有损压缩 [ImageAlpha]
ImageAlpha是ImageOptim作者开发的一个有损的PNG压缩工具,相比较而言,图片大小得到极大的降低,当然图片质量同事也会受到一定程度的影响,经过该工具压缩的图片,需要经过设计师的确认才能最终上线,否则可能回影响整个APP的视觉效果。
有损压缩 [TinyPNG]
TinyPNG也是比较知名的有损PNG压缩工具,它以Web站点的形式提供,没有独立的APP安装包,同所有的有损压缩工具一样,经过压缩的图片,需要经过设计师的确认才能最终上线,否则可能回影响整个APP的视觉效果。
还有很多无损压缩工具,例如JPEGMini、MozJPEG等,大家自行选择适合自己项目的一个就行,主要是在图片大小和图片质量之间找到一个折中点。
白屏问题优化
https://github.com/DanluTeam/ColdStart?utm_source=androidweekly.io&utm_medium=website
因为广告机开发过程中,为了使展示图片更加清晰,使用的都是高清大图,通常在加载高清大图的时候会出现短暂的白屏现象。我们常用的解决方案有:
1.预加载,在上一个页面,把图片预加载到内存中,这样在展示图片的时候直接从内存中获取,解决白屏的问题。
动画抖动问题
常见OOM解决方案
ANR问题解决方案
这里推荐两种解决ANR问题的办法:
1. ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:
$adb pull data/anr/traces.txt
获取到的tracs.txt文件一般如下:
----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp // 最新的ANR发生的进程(包名)
...
DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
| sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
| state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
| stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应.
at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
- locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166) // 产生ANR的那个函数调用
- locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点
at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5254)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
1. 文件最上的即为最新产生的ANR的trace信息.
2. 前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
3. 寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.
- 使用开源项目ANR-WatchDog来检测ANR,Github地址
该ANR-WatchDog实现原理:
ANR-WatchDog创建一个监测线程,该线程不断往UI线程post一个任务,然后睡眠固定时间,等该线程重新起来后检测之前post的任务是否执行了,如果任务未被执行,则生成ANRError,并终止进程。
大量图片帧动画实现
用普通方法实现帧动画用到普通场景是没问题的,如果碰到几十甚至几百帧图片,而且每张图片几百K的情况,例如,做一个很炫的闪屏帧动画,要保证高清且动作丝滑,就需要至少几十张高清图片。这时,OOM问题就出来了。
先分析下普通方法为啥会OOM,查看AnimationDrawable的源代码,发现为了良好的展示效果,它似乎将所有帧一次加载到内存中。然而一次拿出这么多图片,而系统都是以Bitmap位图形式读取的,大量Bitmap就排好队等待播放然后释放,然而这个排队的地方空间是很有限的,图片过多就会导致OOM问题的产生。
然而,在一般的情况中,我们所使用的帧动画在每一帧之间是设置有时间间隔的,没有时间间隔的情况很少,所以我们完全可以改变帧动画的加载方式,通过一次只加载一张图片展示,而不是把所有图片都加载到内存中去,经过试用证明这种方式,节省了很大的内存,而帧动画的效果完全没有受到影响。
代码示例:
public synchronized void start() {
mShouldRun = true;
if (mIsRunning)
return;
Runnable runnable = new Runnable() {
@Override
public void run() {
ImageView imageView = mSoftReferenceImageView.get();
if (!mShouldRun || imageView == null) {
mIsRunning = false;
if (mOnAnimationStoppedListener != null) {
mOnAnimationStoppedListener.AnimationStopped();
}
return;
}
mIsRunning = true;
//新开线程去读下一帧
mHandler.postDelayed(this, mDelayMillis);
if (imageView.isShown()) {
int imageRes = getNext();
if (mBitmap != null) { // so Build.VERSION.SDK_INT >= 11
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions);
} catch (Exception e) {
e.printStackTrace();
}
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(imageRes);
mBitmap.recycle();
mBitmap = null;
}
} else {
imageView.setImageResource(imageRes);
}
}
}
};
mHandler.post(runnable);
}
APP开机启动方案总结
1. 广播启动
写一个广播接收器,用来接收手机开机广播
public class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.e("broadCastReceiver","onReceiver...");
try {
Intent mBootIntent = new Intent(context, MainActivity.class);
// 下面这句话必须加上才能开机自动运行app的界面
mBootIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(mBootIntent);
} catch (Exception e) {
e.printStackTrace();
}
}
}
manifest中静态注册广播接收器
<!--开机广播接受者-->
<receiver android:name=".Receiver">
<intent-filter>
<!--注册开机广播地址-->
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
2. 制作为Launcher应用
下面是开机直接把app Launcher页面当成手机桌面,完成一开机就直接启动app,不需要等待。目前中控三合一启动模式就是采用的这种方案。
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.HOME"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.MONKEY"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
Android Crash 日志收集,Crash之后重启应用实现
原理
Java API提供了一个全局异捕获处理器,Android应用在Java层捕获Crash依赖的就是ThreadUncaughtExceptionHandler处理接口,通常情况下,我们只需要实现这个接口,并重写其中的uncaughtException方法,在该方法中可以读取Crash的堆栈信息。举例
if (e == null) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(t, null);
} else {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
return;
}
final String time = FORMAT.format(new Date(System.currentTimeMillis()));
final StringBuilder sb = new StringBuilder();
final String head = "************* Log Head ****************" +
"\nTime Of Crash : " + time +
"\nDevice Manufacturer: " + Build.MANUFACTURER +
"\nDevice Model : " + Build.MODEL +
"\nAndroid Version : " + Build.VERSION.RELEASE +
"\nAndroid SDK : " + Build.VERSION.SDK_INT +
"\nApp VersionName : " + versionName +
"\nApp VersionCode : " + versionCode +
"\n************* Log Head ****************\n\n";
sb.append(head);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
Throwable cause = e.getCause();
while (cause != null) {
cause.printStackTrace(pw);
cause = cause.getCause();
}
pw.flush();
sb.append(sw.toString());
final String crashInfo = sb.toString();
final String fullPath = (dir == null ? defaultDir : dir) + time + ".txt";
if (createOrExistsFile(fullPath)) {
input2File(crashInfo, fullPath);
} else {
Log.e("CrashUtils", "create " + fullPath + " failed!");
}
if (sOnCrashListener != null) {
sOnCrashListener.onCrash(crashInfo, e);
}
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(t, e);
}
接入Bugly
Bugly是腾讯旗下的一个移动端异常上报和运营统计平台,选择Bugly主要有几个原因,第一,接入简单快捷;第二,每一个Crash都有相应的帮助;第三,每天早上都可以收到Crash日报,用户奔溃率,影响用户数,发生次数,联网用户数
自动集成
详情请参考Bugly Android SDK 使用指南,推荐自动集成。
Bugly SDK分为两部分:SDK和NDK(需要同时集成Bugly SDK),按需添加。自动集成只需要在module中添加相应的依赖即可:
android {
defaultConfig {
ndk {
// 设置支持的SO库架构
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
dependencies {
compile 'com.tencent.bugly:crashreport:latest.release' //其中latest.release指代最新Bugly SDK版本号,也可以指定明确的版本号,例如2.1.9
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新Bugly NDK版本号,也可以指定明确的版本号,例如3.0
}
配置权限
在AndroidManifest.xml中添加权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
混淆配置
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
初始化
在Application类的onCreate方法中添加一行代码:
CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false);
====crash 重启
1. 接入重启功能
在APP Crash之后,监听到uncaughtException被调用,则进行APP重启
public void uncaughtException(final Thread t, final Throwable e) {
// 重启APP
PackageManager packageManager = Utils.getApp().getPackageManager();
Intent intent = packageManager.getLaunchIntentForPackage(Utils.getApp().getPackageName());
if (intent == null) return;
ComponentName componentName = intent.getComponent();
Intent mainIntent = Intent.makeRestartActivityTask(componentName);
Utils.getApp().startActivity(mainIntent);
System.exit(0);
}
Rxjava背压使用
在