今年安卓开发中碰到的几个稀奇古怪的问题

简介: 如果你也遇到了,请保持淡定~1.SIGBUS和SIGSEGV首先是这两个名词的说明: SIGBUS(Bus error)意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。

如果你也遇到了,请保持淡定~

1.SIGBUS和SIGSEGV

首先是这两个名词的说明:

  1. SIGBUS(Bus error)意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。通常是未对齐的数据访问所致。

  2. SIGSEGV(Segment fault)意味着指针所对应的地址是无效地址,没有物理内存对应该地址。

有人一看,什么指针不指针的,对于大多数开发人员来说,不涉及NDK这方面的开发。所以可以想到的就是我们使用的so库。

我这里碰到的SIGBUS相关问题主要集中在集成的极光推送,在极光社区的这篇帖子和我的问题一样。我收集到的信息集中在CPU架构为arm64-v8a,android 5.x 的 OPPO R9MOPPO R7SMOPPO A59MOPPO A59S等OPPO手机。如下图:

问题起因是这样,为了瘦身我们的apk文件,我只添加了armeabi-v7a 架构的相关so文件。因为现在绝大部分的设备都已经是 armeabi-v7a 和 arm64-v8a,虽然我也可以使用armeabi,但是性能关系我最终只保留了armeabi-v7a 。

按道理arm64-v8a设备可以兼容arm64-v8aarmeabi-v7aarmeabi。但结果在oppo的这些手机上没有兼容,或者说更加的严格,导致了未对齐的数据访问。为什么这么说,因为后来有观察再升级极光的sdk后,发现这类问题有所下降。当然如果你直接添加上arm64-v8a,则不会有这个问题。

导致这个问题有多方面的因素,有我们使用的三方sdk的问题,也有手机问题。但在手机不可变的基础上,只能我们去解决,所以尽量不要通过这种方法瘦身APK。(实在不行可以用折中方案,保留armeabi-v7a 和 arm64-v8a)。

SIGSEGV问题排除掉架构兼容问题,相对于集中在5.0以下及机子。这块问题相对比较复杂,我碰到了这样一个问题:

搜索了一下相关问题,找到一篇解决方法:三星 Android 4.3 机型上 webview crash 问题

有兴趣的可以去看看,这里就不赘述了。导致这类问题的情况比较多,只能是经验积累,碰到一个解决一个。不涉及NDK这方面的开发人员,很难规避掉此类问题。

2.TimeoutException

这个问题真的“无法避免”。从buyly的统计看主要集中在oppo 5.0~6.0及个别华为5.0机型。好吧又是oppo手机,oppo真的是很严格,我都快成黑粉了。。。 (当然了7,8,9看来挺不错的)

反馈上来的远比截图看的多,我只取了截取了一小部分。新版本已经“解决了”这个问题,所以现在报上来的主要都是老版本。

bugly异常信息如下:

 

错误堆栈信息:

FinalizerWatchdogDaemon
java.util.concurrent.TimeoutException
android.os.BinderProxy.finalize() timed out after 120 seconds
android.os.BinderProxy.destroy(Native Method)
android.os.BinderProxy.finalize(Binder.java:547)
java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:214)
java.lang.Daemons$FinalizerDaemon.run(Daemons.java:193)
java.lang.Thread.run(Thread.java:818)

首先来说明一下发生问题的原因,在GC时,为了减少应用程序的停顿,会启动四个GC相关的守护线程。FinalizerWatchdogDaemon就是其中之一,它是用来监控FinalizerDaemon线程的执行。

FinalizerDaemon:析构守护线程。对于重写了成员函数finalize的对象,它们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用它们的成员函数finalize,然后再被回收。

一旦检测到执行成员函数finalize时超出一定的时间,那么就会退出VM。我们可以理解为GC超时了。这个时间默认为10s,我通过翻看oppo、 华为的Framework源码发现这个时间在部分机型被改为了120s和30s。

虽然时间加长了,但还是一样的超时了,具体在oppo手机上为何这么慢,暂时无法得知,但是可以肯定的是Finalizer对象过多导致的。知道了原因,所以要模拟这个问题也很简单了。也就是引用一个重写finalize方法的实例,同时这个finalize方法有耗时操作,这时我们手动GC就行了。刚好前几天,在我订阅的张绍文老师的《Android开发高手课中》,老师提到了这个问题,同时分享了一个模拟问题并解决问题的 Demo。有兴趣的可以试试。

那么解决问题的方法也就来了,我们可以在ApplicationattachBaseContext中调用(可以针对问题机型及系统版本去处理,不要矫枉过正):

        try {
            final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
            final Field field = clazz.getDeclaredField("INSTANCE");
            field.setAccessible(true);
            final Object watchdog = field.get(null);
            try {
                final Field thread = clazz.getSuperclass().getDeclaredField("thread");
                thread.setAccessible(true);
                thread.set(watchdog, null);
            } catch (final Throwable t) {
                Log.e(TAG, "stopWatchDog, set null occur error:" + t);

                t.printStackTrace();
                try {
                    // 直接调用stop方法,在Android 6.0之前会有线程安全问题
                    final Method method = clazz.getSuperclass().getDeclaredMethod("stop");
                    method.setAccessible(true);
                    method.invoke(watchdog);
                } catch (final Throwable e) {
                    Log.e(TAG, "stopWatchDog, stop occur error:" + t);
                    t.printStackTrace();
                }
            }
        } catch (final Throwable t) {
            Log.e(TAG, "stopWatchDog, get object occur error:" + t);
            t.printStackTrace();
        }

其实我是用的是stackoverflow这篇帖子中提供的方法:

public static void fix() {
    try {
        Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");

        Method method = clazz.getSuperclass().getDeclaredMethod("stop");
        method.setAccessible(true);

        Field field = clazz.getDeclaredField("INSTANCE");
        field.setAccessible(true);

        method.invoke(field.get(null));

    }
    catch (Throwable e) {
        e.printStackTrace();
    }
}

两种方法都是通过反射最终将FinalizerWatchdogDaemon中的thread置空,这样也就不会执行此线程,所以不会再有超时异常发生。推荐老师的方法,更加全面完善。因为在Android 6.0之前会有线程安全问题,如果直接调用stop方法,还是会有几率触发此异常。5.0源代码如下:

private static abstract class Daemon implements Runnable {

        private Thread thread;// 一种是直接置空thread

        public synchronized void start() {
            if (thread != null) {
                throw new IllegalStateException("already running");
            }
            thread = new Thread(ThreadGroup.systemThreadGroup, this, getClass().getSimpleName());
            thread.setDaemon(true);
            thread.start();
        }

        public abstract void run();

        protected synchronized boolean isRunning() {
            return thread != null;
        }

        public synchronized void interrupt() {
            if (thread == null) {
                throw new IllegalStateException("not running");
            }
            thread.interrupt();
        }

        public void stop() {
            Thread threadToStop;
            synchronized (this) {
                threadToStop = thread;
                thread = null; // 一种是通过调用stop置空thread
            }
            if (threadToStop == null) {
                throw new IllegalStateException("not running");
            }
            threadToStop.interrupt();
            while (true) {
                try {
                    threadToStop.join();
                    return;
                } catch (InterruptedException ignored) {
                }
            }
        }

        public synchronized StackTraceElement[] getStackTrace() {
            return thread != null ? thread.getStackTrace() : EmptyArray.STACK_TRACE_ELEMENT;
        }
    }

这个所谓的线程安全问题就在stop方法中的threadToStop.interrupt()。在6.0开始,这里变为了interrupt(threadToStop),而interrupt方法加了同步锁。

public synchronized void interrupt(Thread thread) {
     if (thread == null) {
         throw new IllegalStateException("not running");
     }
     thread.interrupt();       
}

虽然崩溃不会出现了,但是问题依然存在,可谓治标不治本。通过这个问题也提醒我们,尽量避免重写finalize方法,同时不要在其中有耗时操作。其实我们Android中的View都有实现finalize方法,那么减少View的创建就是一种解决方法。

强烈推荐阅读提升Android下内存的使用意识和排查能力再谈Finalizer对象–大型App中内存与性能的隐性杀手

3.SchedulerPoolFactory

前一阵在用Android Studio的内存分析工具检测App时,发现每隔一秒,都会新分配出20多个实例,跟踪了一下发现是RxJava2中的SchedulerPoolFactory创建的。

一般来说如果一个页面创建加载好后是不会再有新的内存分配,除非页面有动画、轮播图、EditText的光标闪动等页面变化。当然了在应用退到后台时,或者页面不可见时,我们会停止这些任务。保证不做这些无用的操作。然而我在后台时,这个线程池还在不断运行着,也就是说CPU在周期性负载,自然也会耗电。那么就要想办法优化一下了。

SchedulerPoolFactory 的作用是管理 ScheduledExecutorServices的创建并清除。

SchedulerPoolFactory 部分源码如下:

static void tryStart(boolean purgeEnabled) {
        if (purgeEnabled) {
            for (;;) { // 一个死循环
                ScheduledExecutorService curr = PURGE_THREAD.get();
                if (curr != null) {
                    return;
                }
                ScheduledExecutorService next = Executors.newScheduledThreadPool(1, new RxThreadFactory("RxSchedulerPurge"));
                if (PURGE_THREAD.compareAndSet(curr, next)) {

            // RxSchedulerPurge线程池,每隔1s清除一次
                    next.scheduleAtFixedRate(new ScheduledTask(), PURGE_PERIOD_SECONDS, PURGE_PERIOD_SECONDS, TimeUnit.SECONDS);

                    return;
                } else {
                    next.shutdownNow();
                }
            }
        }
    }

   static final class ScheduledTask implements Runnable {
        @Override
        public void run() {
            for (ScheduledThreadPoolExecutor e : new ArrayList<ScheduledThreadPoolExecutor>(POOLS.keySet())) {
                if (e.isShutdown()) {
                    POOLS.remove(e); 
                } else {
                    e.purge();//图中154行,purge方法可用于移除那些已被取消的Future。
                }
            }
        }
    }

我查了相关问题,在stackoverflow找到了此问题,同时也给RxJava提了Issue,得到了回复是可以使用:

 // 修改周期时间为一小时
 System.setProperty("rx2.purge-period-seconds", "3600");

当然你也可以关闭周期清除:

 System.setProperty("rx2.purge-enabled", false);

作用范围如下:

 static final class PurgeProperties {

        boolean purgeEnable;

        int purgePeriod;

        void load(Properties properties) {
            if (properties.containsKey(PURGE_ENABLED_KEY)) {
                purgeEnable = Boolean.parseBoolean(properties.getProperty(PURGE_ENABLED_KEY));
            } else {
                purgeEnable = true; // 默认是true
            }

            if (purgeEnable && properties.containsKey(PURGE_PERIOD_SECONDS_KEY)) {
                try {
                    // 可以修改周期时间
                    purgePeriod = Integer.parseInt(properties.getProperty(PURGE_PERIOD_SECONDS_KEY));
                } catch (NumberFormatException ex) {
                    purgePeriod = 1; // 默认是1s
                }
            } else {
                purgePeriod = 1; // 默认是1s
            }
        }
    }

1s的清除周期我觉得有点太频繁了,最终我决定将周期时长改为60s。最好在首次使用RxJava前修改,放到Application中最好。

4.其他

  • 适配8.0时注意Service的创建。否则会有IllegalStateException异常:
java.lang.IllegalStateException:Not allowed to start service Intent { xxx.MyService }: app is in background uid null

  • 有些手机(已知oppo)在手机储存空间不足时,当你应用退到后台时会自动清除cache下文件,所以如果你有重要数据存储,避免放在cache下,否则当你再次进入应用时,再次获取数据时会有空指针。例如有使用磁盘缓存 DiskLruCache 来存储数据。

现在加Android开发群;701740775,可免费领取一份最新Android高级架构技术体系大纲和视频资料,以及这些年年积累整理的所有面试资源笔记。加群请备注csdn领取xx资料

相关文章
|
27天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
29天前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
25 1
|
1月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
14天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
|
27天前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
14天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14
|
17天前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
15天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
在数字时代,掌握安卓应用开发技能是进入IT行业的关键。本文将引导读者从零基础开始,逐步深入安卓开发的世界,通过实际案例和代码示例,展示如何构建自己的第一个安卓应用。我们将探讨基本概念、开发工具设置、用户界面设计、数据处理以及发布应用的全过程。无论你是编程新手还是有一定基础的开发者,这篇文章都将为你提供宝贵的知识和技能,帮助你在安卓开发的道路上迈出坚实的步伐。
29 5
|
14天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
15天前
|
XML 搜索推荐 前端开发
安卓开发中的自定义视图:打造个性化UI组件
在安卓应用开发中,自定义视图是一种强大的工具,它允许开发者创造独一无二的用户界面元素,从而提升应用的外观和用户体验。本文将通过一个简单的自定义视图示例,引导你了解如何在安卓项目中实现自定义组件,并探讨其背后的技术原理。我们将从基础的View类讲起,逐步深入到绘图、事件处理以及性能优化等方面。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。

相关实验场景

更多