Android旁门左道之动态替换系统View类

导语 本文讲述如何通过替换系统View类的方法,定位一个特殊机型问题

作者: yarkeyzhang  2017.6.29

一,ImageView抛来一个异常

应用程序Crash是Android App开发习以为常的问题,大部分Crash我们通过日志找到调用栈可以很快定位到出错的代码。然而有一些Crash却显得没那么直接,比如下面这个由Android系统抛(throw)出来的异常。

17-06-06 11:36:20|1496720179572[20047]1|E|StatisticCollector|  
getCrashExtraMessage...
isNativeCrashed: false 
crashType=java.lang.RuntimeException 
crashAddress=android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1270) 
crashStack=android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1270)
android.graphics.Canvas.drawBitmap(Canvas.java:1404)
android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:544)
android.widget.ImageView.onDraw(ImageView.java:1246)
android.view.View.draw(View.java:16245)
android.view.View.updateDisplayListIfDirty(View.java:15242)
android.view.View.draw(View.java:16015)
android.view.ViewGroup.drawChild(ViewGroup.java:3740)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:3530)
android.view.View.draw(View.java:16248)
android.view.View.updateDisplayListIfDirty(View.java:15242)
android.view.View.draw(View.java:16015)
android.view.ViewGroup.drawChild(ViewGroup.java:3740)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:3530)
android.view.View.updateDisplayListIfDirty(View.java:15237)
android.view.View.draw(View.java:16015)
android.view.ViewGroup.drawChild(ViewGroup.java:3740)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:3530)
android.view.View.updateDisplayListIfDirty(View.java:15237)
android.view.View.draw(View.java:16015)
android.view.ViewGroup.drawChild(ViewGroup.java:3740)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:3530)
android.view.View.draw(View.java:16248)
com.android.internal.policy.PhoneWindow$DecorView.draw(PhoneWindow.java:2822)
android.view.View.updateDisplayListIfDirty(View.java:15242)
android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:282)
android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:288)
android.view.ThreadedRenderer.draw(ThreadedRenderer.java:323)
android.view.ViewRootImpl.draw(ViewRootImpl.java:2649)
android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2468)
android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2072)
android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1108)
android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6146)
android.view.Choreographer$CallbackRecord.run(Choreographer.java:892)
android.view.Choreographer.doCallbacks(Choreographer.java:704)
android.view.Choreographer.doFrame(Choreographer.java:640)
android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:878)
android.os.Handler.handleCallback(Handler.java:739)
android.os.Handler.dispatchMessage(Handler.java:95)
android.os.Looper.loop(Looper.java:148)
android.app.ActivityThread.main(ActivityThread.java:5648)

我们看看日志,ViewRootImpl.doTraversal()是遍历Window所有View刷新界面的过程,这个过程由系统触发,我们在调用栈中找不到任何的App客户代码。过程中,ImageView在执行onDraw()的时候出现了异常。这是某部手机在开启多窗口模式时必现Crash。怎么办?

二、寻求解决思路

这个问题出现在一个编辑图片的页面,页面中含有很多的ImageView(大约20个)实例,单单靠调用栈我们无法定位具体哪个ImageView出现了问题。

不过相信大家已经有很多解决思路:

1. 通过日志文件寻找出错前后是否有更多帮助信息,配合源码定位问题

2. 借到问题手机,连接电脑配合源码打断点(ImageView,BitmapDrawable,Canvas)

思路1无法快速解决问题;思路2恕我实在借不到那个型号的手机,另外我们IDE中的Android源码与手机中行数不一定匹配,给ImageView,BitmapDrawable等等这些系统类打断点,代码行数对不上的话也就很难搞。

这里我想到了一个思路:能不能重写ImageView.onDraw()方法,在出现异常时打印出所有我们需要的日志信息(比如view id)

三、往LayoutInflater下手

重写ImageView.onDraw()方法实际上等于我们需要替换ImageView类,把所有的xml布局文件中的ImageView换成我们新定义的CatchExceptionImageView?这个显然不太好办。最后我在LayoutInflater类中找到了方法。

每个Activity拥有一个LayoutInflater 对象,它负责解析Android xml 布局文件然后实例化View或者View子类对象。核心函数是 LayoutInflater.createViewFromTag(View parent, String name, AttributeSet attrs),它通过xml标签指定的类名字,实例化出View对象。在这里做手脚,我们可以将xml中所有的标签实例化成 CatchExceptionImageView。

查看createViewFromTag()源码我们可以发现, LayoutInflater其实支持外部提供工厂类来自定义View的创建机制,对应的方法是 setFactory() 和 setFactory2()。

如果大家有用过android.support.v7.app.AppCompatActivity,那么你会发现,xml布局中的Button标签实际上创建了android.support.v7.widget.AppCompatButton对象,TextView标签实际上创建了android.support.v7.widget.AppCompatTextView对象,这是通过LayoutInflater.Factory来影响View的创建实现的。

所以,我们调用 setFactory()或者setFactory2()方法有可能会遇到失败:“A factory has already been set on this LayoutInflater”。最后,我通过反射把我定义的Factory对象安全地注入到了LayoutInflater对象中。

四、调试代码协助定位问题

为了捕捉到抛出异常的ImageView,我大概写了下面这样的代码:

    public static class CatchExceptionImageView extends ImageView {
        public CatchExceptionImageView(Context context) {
            super(context);
        }
        public CatchExceptionImageView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        @Override
        protected void onDraw(Canvas canvas) {
            Drawable drawable = getDrawable();
            if (drawable instanceof BitmapDrawable) {
                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
                if (bitmapDrawable.getBitmap().isRecycled()) {
                    Log.e(TAG, "we'll crash !! " + this);
                }
            }
            super.onDraw(canvas);
        }
    }


    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        HashMap hookViewMap = new HashMap();
        hookViewMap.put("ImageView", CatchExceptionImageView.class.getName());
        new LayoutDebugHelper().onActivityCreate(this, hookViewMap);
        
        setContentView(R.layout.activity_main);
    }

我构建了新的包发送给远方的优测测试人员,帮我复现了问题并抓了日志,最后找到了Crash的ImageView信息,通过view id便可以找到了出错的点。

够折腾吧,哈哈,机型问题虐我千百遍!

全文完,感谢阅读!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

gradeview可拖动效果实现

下面先上这次实现功能的效果图:(注:这个效果图没有拖拽的时候移动动画,DEMO里面有,可以下载看看) ? 一、开发心里历程 刚开始接触这个的时候,不知道要如何实...

35580
来自专栏wOw的Android小站

[Android][Framework] Gallery幻灯片流程以及一个Bitmap的bug

用Camera录制任意长度视频。进入Gallery,打开包含刚拍好的视频的相册,然后右上角选择展示“幻灯片”,发现,刚才的视频的Thumbnail出现倾斜,被分...

9210
来自专栏向治洪

SwipeListView实现仿ios的侧滑

github地址:https://github.com/xiangzhihong/SwipeMenuListView 今天介绍一个SwipeMenuListVi...

29580
来自专栏青蛙要fly的专栏

Android技能树 — LayoutInflater Factory小结

前段时间流行起来了突然不愿意写Shape,Selector文件的文章,然后各种方案,编写自定义View等。那时候大家应该都看到了一篇: 无需自定义View,彻底...

20130
来自专栏知识分享

android之WIFI小车编程详述

有了前几篇wifi模块eps8266的使用,单片机设置eps8266程序,android TCP客户端,现在就做一个wifi小车 先上图 ? ? 小车是四个轮子...

43290
来自专栏向治洪

仿今日头条顶部导航效果

 之前发现很多人在群里面、论坛上求网易新闻客户端的源码,之后我就去下了个网易新闻客户端和今日头条新闻客户端,发现他们的大体是一样的,于是在最近的空闲时间,便去琢...

1.1K80
来自专栏用户2442861的专栏

公司(视频 社交)项目分享

http://blog.csdn.net/u011733020/article/details/46786471

16660
来自专栏james大数据架构

列表视图(ListView和ListActivity)

在ListView中显示网络图片  ImageView 类虽然有一个 setImageUri 方法,但不能直接接受一个由网络地址生成的uri作为参数从而显示图片...

34170
来自专栏程序员的诗和远方

看代码学AndroidUI - Tab

最近慢慢学习一点安卓,先看了些基础的,还处于很初级的阶段,平常都是面对弱类型的语言,python,js,现在看java突然有点不适应。 这里推荐郭神的《第一行代...

34290
来自专栏移动开发

图片库的封装

关于图片库的封装相关的文章早已经看到过.图片库的封装可以使得调用者不知道,底层的具体实现,即使我们换了图片加载库,上层处的代码感知不到无需修改.

18220

扫码关注云+社区

领取腾讯云代金券