常见问题总结初稿

常见问题总结

在广告机和开发版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官方指定按照下列标准进行区分:

名称像素密度范围
mdpi120dpi~160dpi
hdpi160dpi~240dpi
xhdpi240dpi~320dpi
xxhdpi320dpi~480dpi
xxxhdpi480dpi~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. 寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.
  1. 使用开源项目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之后重启应用实现

  1. 原理
    Java API提供了一个全局异捕获处理器,Android应用在Java层捕获Crash依赖的就是ThreadUncaughtExceptionHandler处理接口,通常情况下,我们只需要实现这个接口,并重写其中的uncaughtException方法,在该方法中可以读取Crash的堆栈信息。

  2. 举例

    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背压使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值