五年Android开发,让我“刻骨铭心”的那些坑

本人对在开发过程中踩坑经历的一次总结

Posted by SnailStudio on October 19, 2016

“Yeah It’s on. ”

前言

这篇文章是本人对在开发过程中踩坑经历的一次总结;分为系统API的坑、使用不当导致的坑、编译时的坑、开源项目中的坑等几个方面,知识面有限,认知难免会有偏颇,如发现有问题还请指正。


系统API的坑

Android library中的资源ID在R.java中不是final类型

问题现象:在library中使用switch语句区分不同的资源ID时,IDE会报错;

原因分析:这个问题在Android Studio Project Site有提及,在ADT14及以上的版本中,library所对应的R.java中所有ID不再是final类型,所以不能将ID作为switch语句中的case分支属性值。这个问题和IDE无关,在Eclipse和AS中都存在。

解决方案:如果涉及到区分多个ID的情况(比如监听回调事件、初始化通过xml给自定义View设置的属性值等)应该使用if…else if…else代替switch语句;

同一个程序内的多个进程之间使用SharedPreferences不安全

问题现象:在同一个程序内使用多进程时,在不同进程间使用SharedPreferences操作数据会导致SF中的数据随机丢失的情况(获取到的值为空);

原因分析:虽然API中提供了Context.MODEMULTIPROCESS模式打开SF文件,但在官方文档(https://developer.android.com/reference/android/content/SharedPreferences.html)中已有说明:“currently this class does not support use across multiple processes”,因为SF在多进程间操作是不安全的,并行操作时会导致写冲突。

解决方案:Github上有个开源项目Tray(https://github.com/grandcentrix/tray),专门针对在多进程中使用SF不安全的问题提供的解决方案。

Typeface初始化自定义字体慢

问题现象:在使用自定义字体的页面,进入慢;

原因分析:使用Typeface初始化字体很耗时,至少需要100ms(不同文件耗时不一样)以上的时间。

解决方案:如果在Activity的onCreate方法中初始化Typeface,会导致进入Activity慢,出现黑屏/白屏现象,所以应该尽量在非UI线程中做自定义字体的初始化操作。

Activity在没有完全显示/已退出的情况下显示PopupWindow异常

问题现象:进入Activity界面直接报错,log异常显示为:”Unable to add window – token null is not valid”;

原因分析:原因是在Activity的onCreate方法中直接显示了PopupWindow导致,PopupWindow的显示是依附在某一个View上面的(showAtLocation方法第一个参数为需要依附的view),在Activity没有完全显示时,PopupWindow无法依附在该View上,如果在此时显示PopupWindow会导致上面的异常,同样在退出Activity后也不能正常显示PopupWindow。

解决方案:在开发过程中需要考虑通过异步显示PopupWindow,避免PoupWindow显示报异常的问题。

Activity的onDestory方法调用时机不确定

问题现象:连续进入、退出某一个Activity,会出现Activity Crash掉的现象;

原因分析:在Activity的onCreate做的初始化操作(打开文件),在onDestory做的销毁操作(关闭文件);退出Activity后onDestory并没有立即调用,再次快速进入该Activity时,该Activity是另外一个实例,并且首先调用了新Activity的onCreate方法之后再才调用上个Activity实例的onDestory方法,导致文件刚被打开就关闭了,在程序使用数据时Crash掉;

解决方案:准确来讲只要是系统方法,调用时机都不确定。对于这种问题只能尽量不要在Activity的系统回调方法中做资源初始化和释放的操作,比如涉及到IO操作的情况,在使用的时候才打开,使用完后立即关闭;

透明主题导致Activity生命周期回调的变化

问题现象:从当前Activity跳转到其它Activity时,当前Activity的onStop方法并没有调用;

原因分析:给当前Activity设置为透明主题导致,通过添加打印跟踪发现,从该Activity跳转到其它Activity时,该Activity的onStop方法不会执行;

解决方案:谨慎使用透明主题,如果必须要为Activity设置为透明主题,不要在onStop方法中做任何操作,因为该方法并不会被调用。透明主题存在很多问题,比如在设置为透明主题的界面按Home按键时,会存在界面刷不干净的情况。

不要通过Bundle传递很大块的数据

问题现象:从目录界面跳转到内容显示界面,出现随机崩溃的现象,报的异常是:TransactionTooLargeException;

原因分析:跟踪发现如果通过Bundle传递太大块(>1M)的数据会在程序运行时报TransactionTooLargeException异常,具体原因可以上官网查看,大致意思是传递的序列化数据不能超过1M,否则会报TransactionTooLargeException异常;之所以随机是因为每次传递的数据大小不一样。

解决方案:如果你在不同组件之间传递的数据太大,甚至超过了1M,为了提高效率和程序的稳定性,建议通过持久化的方式传递数据,即在传递方写文件,在接收方去读取这个文件;

不要在Application类中缓存数据

问题现象:程序从后台切换到前台,直接崩了;

原因分析:程序在后台时,为了给正在运行的程序提供更多可使用的内存,Application中的数据可能会被清理掉,如果在Application中缓存了数据,并且在程序重新回到前台时没有做好恢复工作,程序会出现不可预见的情况(比如数据错乱、崩溃等),具体可以参照这篇文章Don’t Store Data in the Application Object;

解决方案:不要在Application中缓存数据。

使用AsyncTask无法避开的坑

问题现象:使用AsyncTask异步执行的任务并没有立即执行;

原因分析:AsyncTask这个类的实现可谓一波三折,方案修改了好几个版本,初次引入这个类时,所有的Task是放在一个独立的后台线程中执行的,也就是如果有多个Task同时被调用也是顺序执行的;从1.6开始,改为通过线程池可以支持并行执行多个Task;但从3.0开始,又改回只有一个独立的后台线程执行所有Task,主要是为了避免多个Task并行执行导致的程序错误,但为了让AsyncTask能够支持多个Task并行执行,从3.0起,增加了executeOnExecutor方法,调用者自行实现线程池可以达到并行多个Task的效果。

解决方案:如果在某个地方需要同时执行多个异步任务,强烈建议使用线程池;

数据库升级中的坑

问题现象:在数据库的某个表中增加/修改了某个字段后,程序在运行时崩溃掉了;或者在增加字段时修改了数据库的版本号,但程序升级后,原来的数据丢失了;

原因分析:SQlite数据库升级时需要修改OpenHelper中的版本号,并且数据库升级会删掉原来数据库中的数据,需要手动将原数据库中的数据拷贝到高版本的数据库中;

解决方案:做好数据库升级的恢复工作,避免出现崩溃、数据丢失的情况。

程序在未启动的情况下,静态注册的广播无法收到消息

问题现象:程序添加了对开机广播的监听,但无法接收到;

原因分析:这个问题只有在程序安装但没有启动时才会出现,只要程序启动过一次后就不会有这个问题。并且只有在Android 3.1及以上的版本才会出现,具体原因是:从Android3.1开始,新安装的程序会被置于”stopped”状态,并且只有在至少手动启动这个程序一次后该程序才会改变状态,能够正常接收到指定的广播消息。Android这样做的目的是防止广播无意或者不必要地开启未启动的APP后台服务。也就是说在Android3.1及以上的版本,程序在未启动的情况下通过应用自身完成一些操作是不可能的,但Android提供了一种借助其它应用发送指定Flag广播的方式,达到应用在未启动的情况下仍然能够收到消息的效果。从Android 3.1开始,系统给Intent定义了两个新的Flag,分别为FLAGINCLUDESTOPPEDPACKAGES(表示包含未启动的App)和FLAGEXCLUDESTOPPEDPACKAGES(表示不包含未启动的App),用来控制Intent是否要对处于停止状态的App起作用。

解决方案:只能借助其它应用给自己发送带FLAG_INCLUDESTOPPEDPACKAGES标志的广播才能实现在程序未启动的情况下接收到广播;

android:windowBackground导致的过渡绘制问题

问题现象:界面的布局已无法进一步优化,但仍然存在过渡绘制的问题;

原因分析:window存在默认的背景,会增加过渡绘制的可能。Activity是依附在Window上的,如果给Activity设置了背景,并且没有去掉window的背景,很容易导致过渡绘制;这里还有一个坑,有的应用为了避免程序冷启动时出现黑屏/白屏的问题,在主题中给window设置了背景,并且在Activity的布局中给Activity也设置了背景,这会导致当前界面存在两个背景,占用了双倍的内存,并且还会有过渡绘制的问题。程序启动黑屏应该去优化性能问题,而不是采用给window设置背景的方式;

解决方案:可以通过给Activity自定义主题,在主题中去掉window的默认背景,即:@null;

类的finalize方法调用时机不确定

问题现象:程序随机崩溃;

原因分析:多个地方用到了同一个类,该类用于对数据的IO操作,打开文件后并没有立即关闭,也没有释放资源的public方法,主要通过类的finalize方法关闭文件,释放资源;

解决方案:finalize方法的调用时机是不确定的,不要指望通过该方法释放与类相关的资源,避免出现随机的bug;

Fragment isAdded

问题现象:程序随机崩溃;

原因分析:跟踪异常log发现,是因为Fragment没有完全显示或者已经离开Fragment的情况下,导致的异常,这类异常的主要原因是:使用Fragment时,通过异步操作(比如回调、非UI线程等)更新Fragment的状态,但此时Fragment没有完全显示或者已经离开Fragment;

解决方案:在调用Fragment的方法之前,强烈建议调用isAdded方法判断Fragment是否依附在Activity上,避免出现异常。

在使用Activity + 多个fragment模型时的坑

问题现象:在使用Activity + 多个fragment模型时,若出现异常崩溃后,此时可能会造成第一个fragment重复添加进入FragmentManager的情况(此时会发现第一个界面会层叠在屏幕上)。

原因分析:出现异常崩溃后,Activity重启,会重新执行onCreate方法(此时bundle不为空);

解决方案:可根据bundle做相应的判断。另外,在一些低配手机上,按home建,当程序在后台一段时间后再次唤起,也可能会发生该情况。

Fragment hide、show被调用时,生命周期不会回调

问题现象:同一界面不同Fragment之间切换时,并没有触发一些动态效果,比如播报音频、显示切换动画等;

原因分析:Fragment hide、show被调用时,系统并不会调用Fragment的生命周期回调;

解决方案:不同Fragment之间切换时,主动调用各个Fragment的生命周期回调;

Fragment的onActivityResult无效

问题现象:常见的,我们会在FragmentActivity中嵌套一层Fragment使用,甚至Fragment下层层嵌套使用。这个时候如果使用了activity.startActivityForResult(),在第二级或者更深级别的Fragment将无法收到onActivityResult回调。

原因分析:查看FragementActivity的源码发现:

    public void startActivityFromFragment(Fragment fragment, Intent intent,
           int requestCode) {
        if (requestCode == -1) {
           super.startActivityForResult(intent, -1);
           return;
        }
        if ((requestCode & 0xffff0000) != 0) {
           throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
        }
        super.startActivityForResult(intent, ((fragment.mIndex + 1) << 16) + (requestCode & 0xffff));
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        mFragments.noteStateNotSaved();
        int index = requestCode >> 16;
        if (index != 0) {
           index--;
           if (mFragments.mActive == null || index < 0 || index >= mFragments.mActive.size()) {
              Log.w(TAG, "Activity result fragment index out of range: 0x"
                    + Integer.toHexString(requestCode));
              return;
           }
           Fragment frag = mFragments.mActive.get(index);
           if (frag == null) {
              Log.w(TAG, "Activity result no fragment exists for index: 0x"
                    + Integer.toHexString(requestCode));
           } else {
              frag.onActivityResult(requestCode & 0xffff, resultCode, data);
           }
           return;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

原来,程序猿偷懒,没有处理嵌套Fragment的情况,也就是说回调只到第一级Fragment,就没有继续分发。

解决方案:调用fragment.startActivityForResult()启动可以在fragment中的onActivityResult()接受处理。

如果想根治这个问题,我们可以实现一个自己的FragmentActiviy,来实现继续分发,如下:

public class BaseFragmentActiviy extends FragmentActivity {
    private static final String TAG = "BaseActivity";

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        FragmentManager fm = getSupportFragmentManager();
        int index = requestCode >> 16;
        if (index != 0) {
            index--;
            if (fm.getFragments() == null || index < 0
                    || index >= fm.getFragments().size()) {
                Log.w(TAG, "Activity result fragment index out of range: 0x"
                      + Integer.toHexString(requestCode));
                return;
            }
            Fragment frag = fm.getFragments().get(index);
            if (frag == null) {
                Log.w(TAG, "Activity result no fragment exists for index: 0x"
                      + Integer.toHexString(requestCode));
            } else {
                handleResult(frag, requestCode, resultCode, data);
            }
            return;
        }
    }

    /**
     * 递归调用,对所有子Fragement生效
     *
     * @param frag
     * @param requestCode
     * @param resultCode
     * @param data
     */
    private void handleResult(Fragment frag, int requestCode, int resultCode,
                              Intent data) {
        frag.onActivityResult(requestCode & 0xffff, resultCode, data);
        List<Fragment> frags = frag.getChildFragmentManager().getFragments();
        if (frags != null) {
            for (Fragment f : frags) {
                if (f != null)
                    handleResult(f, requestCode, resultCode, data);
            }
        }
    }
}

然后我们继承这个BaseFragmentActivity即可,但是要注意,在Fragment中启动Activity时,一定要调用根Fragment的启动方法,如下:

    /**
      * 得到根Fragment
      *
      * @return
      */
    private Fragment getRootFragment() {
        Fragment fragment = getParentFragment();
        while (fragment.getParentFragment() != null) {
            fragment = fragment.getParentFragment();
        }
        return fragment;
    }

    /**
     * 启动Activity
     */
    @Override
    public void startActivityForResult(Intent intent, int requestCode) {
        getRootFragment().startActivityForResult(intent, requestCode);
    }
}

RadioButton的坑

问题现象:关于RadioButton。默认的button是在左边。若想将图片放在右边,可以设置android:button=“@null”,设置.setCompoundDrawable(null,null,drawable,null);将图片显示在右边。然而在4.x的手机上。即使设置了android:button=“@null”,radioButton仍然会显示默认的图片。

原因分析:4.x系统对RadioButton设置了默认的图片。

解决方案:若想要默认的不显示,可以给button设置一个空的drawable:

<selector xmlns:android="http://schemas.android.com/apk/res/android">

</selector>

IllegalArgumentException: pointerIndex out of range

问题现象:出现这个Bug的场景还是很无语的。一开始我用ViewPager + PhotoView(一个开源控件)显示图片,在多点触控放大缩小时就出现了这个问题;

原因分析:一开始我怀疑是PhotoView的bug,找了半天无果。要命的是不知如何try,老是crash。后来才知道是android遗留下来的bug,源码里没对pointer index做检查。改源码重新编译不太可能吧。明知有exception,又不能从根本上解决,如果不让它crash,那就只能try-catch了;

解决方案:自定义一个ViewPager并继承ViewPager:

    /*** 自定义封装android.support.v4.view.ViewPager,重写onInterceptTouchEvent事件,捕获系统级别异常*/
    public class CustomViewPager extends ViewPager {

        public CustomViewPager(Context context) {
            this(context, null);
        }

        public CustomViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            try {
                return super.onInterceptTouchEvent(ev);
            } catch (IllegalArgumentException e) {
                LogUtil.e(e);
            } catch (ArrayIndexOutOfBoundsException e) {
                LogUtil.e(e);
            }
            return false;
        }

    }

ListView中item点击事件无响应

问题现象:listView的Item点击事件突然无响应,这个问题一般是在listView中加入了button、checkbox等控件后出现的;

原因分析:这个问题是聚焦冲突造成的。在android里面,点击屏幕之后,点击事件会根据你的布局来进行分配的,当你的listView里面增加了button之后,点击事件第一优先分配给你listView里面的button,所以你的点击Item就失效了;

解决方案:这个时候你就要根据你的需求,是给你的item的最外层layout设置点击事件,还是给你的某个布局元素添加点击事件了。可以自定义一个ViewPager并继承ViewPager:

android:descendantFocusability="blocksDescendants"

官方文档也是这样说明:

官方说明


使用不当造成的坑

9图不要用tinypng压缩

问题现象:使用压缩工具压缩9图后,显示变形;

原因分析:9图除了图片信息外,还存储一些Android在显示9图过程中需要用到的必要信息,通过压缩工具压缩图片会改变文件的信息,9图被压缩后程序能显示,但显示的效果无法达到预期,因为拉伸信息丢失了。

解决方案:9图文件本身就不大,没必要压缩;

同一设备上,相同程序的图片放在不同drawable文件夹下,占用内存不一样

问题现象:程序刚启动就占用了很高的内存;

原因分析:图片放置位置不合理导致的,程序在不同的设备中运行时,会根据设备的分辨率和屏幕密度去从与之分辨率匹配的资源文件夹中取图片,如果没有对应分辨率的文件夹,则从相近分辨率的文件夹中取,但图片会被拉伸到当前设备屏幕的宽高,所以会存在图片被放大或者缩小的问题,导致占用内存会随之变化,具体可以查看这篇博客关于Android中图片大小、内存占用与drawable文件夹关系的研究与分析(http://blog.csdn.net/zhaokaiqiang1992/article/details/49787117);

解决方案:为了减少UI的工作量,并且减少APK的内存占用的方法是让UI出一套高分辨率版本的图片,放在hdpi文件夹下。

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

问题现象:多线程访问数据库时可能出现这个问题;

原因分析:getReadableDatabase()和getWritableDatabase()最后都是调用了以下方法来获取mDatabase对象的:

private SQLiteDatabase getDatabaseLocked(boolean writable)
  • 首次调用的时候,会初始化数据库信息,创建数据库,并打开与数据库的连接……然后返回mDatabase;
  • 如果mDatabase已打开,但只能用于读操作并且参数writable==true,此时返回mDatabase;
  • 如果mDatabase已被用户关闭了,那么将根据参数来设计打开模式,重新打开数据库的连接,并返回mDatabase;

是否需要调用close()方法?什么时候调用?

SQLiteDatabase db = getWritableDatabase();
db.execSQL("DROP TABLE " + TABLE_NAME);
db.close(); //这个操作要不要加?

由于SQLiteOpenHelper内部只缓存一个数据库的连接(即一个SQLiteDatabase 实例mDatabase), 所以,当SQLiteOpenHelper被多个线程调用的时候,就需要注意了。譬如:

  • 线程A通过getWritableDatabase()获取了一个SQLiteDatabase 用来进行删除记录操作,
  • 线程B通过getReadableDatabase()获取了一个SQLiteDatabase 用来进行其它操作,操作已完成正在close()。

当线程A还在进行删除记录操作的时候,线程B调用了SQLiteDatabase.close()方法断开了数据库连接,那么将会导致线程A的删除操作出现异常。

解决方案:一般来说不要随便close(),我认为在在Activity执行onDestory的时候调用close()比较合理,或者整个App退出的时候再close()。

Adapter ViewHolder缓存导致显示错乱的坑

问题现象:ListView每一项在滑动的过程中内容显示错乱;

原因分析:在Adapter的getView方法中通过position更新每一项的内容时,对于根据判断条件给每一项设置属性的情况,每个判断条件下都需要给每一项的每个属性赋值,否则在滑动ListView或GridView时会导致内容错乱;

解决方案:在getView方法里面,给每一项都要设置对应的属性,比如给每一项的头像设置图片,如果某一项没有头像,不能不设置,应该设置为透明,否则会错乱。

Toast连续显示时长时间不消失

问题现象:多个Toast同时显示时,Toast一直显示不消失,退出程序了仍然显示;

原因分析:看Toast的源码可以发现,同时显示多个toast时是排队显示的,所以才会出现同时显示多个Toast时很久都不消失的情况;

解决方案:这属于体验问题,很多应用都存在。建议定义一个全局的Toast对象,这样可以避免连续显示Toast时不能取消上一次Toast消息的情况(如果你有连续弹出Toast的情况,避免使用Toast.makeText);

android.view.WindowManager$BadTokenException: Unable to add window – token null is not for an application

问题现象:使用dagger2注入依赖AlertDialog时出现这个问题;

原因分析:导致报这个错是在于new AlertDialog.Builder(mcontext),虽然这里的参数是AlertDialog.Builder(Context context),但我们不能使用getApplicationContext()获得的Context,而必须使用Activity,因为只有一个Activity才能添加一个窗体;

解决方案:将new AlertDialog.Builder(Context context)中的参数用Activity的实例来填充;

build.gradle中的versionName和versionCode

问题现象:从Eclipse转到AS的项目,在机器上运行时报版本比之前APK版本低的错误;

原因分析:从Eclipse转到AS的过程中,如果你是通过AS直接新创建的一个工程,注意模板会在build.gradle中给程序设置默认versionName和versionCode为1,如果AndroidManifest.xml中的versionCode、versionName比build.gradle中的更高,会导致因为版本问题安装不上的情况(报INSTALL_FAILEDVERSIONDOWNGRADE错误);

解决方案:只在build.gradle中设置版本名和版本号;

AS中依赖包的动态更新

问题现象:依赖包频繁更新,因为AS编译有缓存,每次更新都需要修改依赖包的版本号,特别麻烦,特别是依赖关系比较复杂的情况下;

解决方案:在AS中,如果你想动态同步一个依赖包的更新,可以在依赖包的最后面写上“+”,比如:compile ‘com.android.support:appcompat-v7:23.0.+’ ,但这种方法需要谨慎使用,否则会因为依赖包的变动导致你的项目不稳定:Don’t use dynamic versions for your dependencies(https://link.zhihu.com/?target=http%3A//blog.danlew.net/2015/09/09/dont-use-dynamic-versions-for-your-dependencies/);

AS中同一个工程module太多导致编译慢

问题现象:编译一个工程要好几分钟,特别是clean的时候,经常10分钟以上;

原因分析:其实这个很好理解,每个module中都有一个build.gradle,编译的时候,每个module的build.gradle中的task都需要执行,所以编译时间会很长。

解决方案:要解决这个问题很简单,将不经常变动的module打包成aar,主工程依赖aar而不是module,这样避免了每次都需要重新编译module的情况。

频繁的GC操作导致程序卡顿

问题现象:通过AS Monitor观察应用运行过程中的内存抖动厉害,通过GPU呈现模式观察每一帧的曲线差别很大,整体感受程序运行时不流畅;

原因分析:在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC程序就只能阻塞住等待GC结束,在2.3之后GC操作改成了并发的方式进行,GC过程中不会影响程序的正常运行,但在GC操作的开始和结束还是会短暂阻塞一段时间,所以频繁的GC会导致使用应用的过程中卡顿。

解决方案:为了应用在使用过程中更流畅,需要尽量减少触发GC操作,这涉及到性能优化,对于静态代码的分析,AS已经很强大了,可以使用Android Studio的Analyze→Inspect Code…进行分析;

TextView 的setText方法,如果传入一个数字会直接当作字符串资源ID处理

问题现象:程序运行时报“NotFoundException”异常;

原因分析:TextView.setText(int value)的传值有问题,在xml文件中没有找到id对应的字符串;

解决方案:给TextView设置文本的时候一定要转成String或者Charsequence类型,避免TextView将setText中的参数当做字符串资源ID处理,去加载字符串资源,因为字符串在xml文件中不存在导致程序运行时崩溃。

通过反射访问方法和字段的效率大不一样

问题现象:程序运行卡、慢;

原因分析:在一个循环中使用到了反射,并且是调用的反射方法,改成反射字段后,卡、慢的现象得到明显的改善;

解决方案:通过反射修改或者获取类中的某个属性时,强烈建议使用访问字段的方式,不要使用访问方法的方式,这两者之间的效率相差很大,亲测访问方法是访问字段耗时的1.5倍,具体情况和类的复杂度有关。

.nomedia文件的使用

问题现象:程序中的缓存文件在相册、音乐播放器中显示;

原因分析:相册、音乐播放器等多媒体应用是读取媒体库中的数据,而程序的缓存文件被缓存到了媒体数据库中;

解决方案:如果你希望自己应用生成的数据不被媒体库扫描到,应该在生成数据的文件夹下创建一个名为”.nomedia”的隐藏文件,避免出现一些无意义的文件也被媒体库扫描到的情况,比如APP的缓存图片在相册中显示、宣传视频在视频播放器中显示、音效在音乐播放器中显示等。

循环动画

问题现象:在不待机的情况下,长时间处于一个界面时,手机发烫;

原因分析:界面中存在循环动画,CPU、GPU一直在工作;

解决方案:循环动画会导致界面一直在刷新,CPU、GPU持续工作,会有功耗问题,建议拒掉这种视觉呈现效果。

谨慎使用aaptOptions.cruncherEnabled = false;aaptOptions.useNewCruncher = false;

问题现象:编译生成的APK文件特别大,超过了正常的大小;

原因分析:解压APK发现,主要是图片资源导致,将APK中的res文件夹和源码下的res文件夹对比,发现多了很多图片文件;跟踪原因发现最新的buildtools对资源文件的检测很严格,对于Eclipse转AS的项目,很多时候都是因为图片问题导致在AS上编译不过,比如将jpg强转为png在AS上就编译不过,在项目中可以在build.gradle中加上这两句:aaptOptions.cruncherEnabled = false;aaptOptions.useNewCruncher = false,屏蔽掉aapt对图片的严格检测。但需要谨慎使用这两个属性,否则可能会导致编译生成的APK特别大(解压生成后的APK发现,对于有问题的图片,每个drawable文件夹下都会拷贝一份);

解决方案:去掉属性设置,解决编译问题。


编译时的坑

编译环境的坑

问题现象:如果在使用Eclipse开发Java项目时,在使用 @Override 出现以下错误:

The method *** of type *** must override a superclass method

原因分析:主要是因为你的Compiler是jdk5,(5不支持@Override等形式的批注)只要把它改为6就可以了;

解决方案:将window -> preferences -> java-compiler中的Compiler compliance level修改为6.0。

Android Studio Caused by java.lang.ClassNotFoundException

问题现象:项目一直运行的很正常,最近在做友盟社会化分享的时候需要导入jar包,导入相关jar编译没问题一运行APP就报ClassNotFoundException,删除了jar包就一切正常了,当时一直以为是jar引用方法不对,但是后来发现多写几个函数后同样会报错;

原因分析:Android系统中,一个App的所有代码都在一个Dex文件里面。Dex是一个类似Jar的存储了多有Java编译字节码的归档文件。因为Android系统使用Dalvik虚拟机,所以需要把使用Java Compiler编译之后的class文件转换成Dalvik能够执行的class文件。这里需要强调的是,Dex和Jar一样是一个归档文件,里面仍然是Java代码对应的字节码文件。当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。但是在早期的Android系统中,DexOpt的LinearAlloc存在着限制: Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小65536时,会造成dexopt崩溃,导致无法安装;

解决方案:

1) 在项目目录下build.gradle中android中添加multiDexEnabled true

multiDexEnabled true

2) 集成类Application的类中添加代码MultiDex.install(this);

MultiDex.install(this)

The connection to adb is down

解决方案:

在任务管理器中强制结束adb.exe进程以及第三方的相关进程,如kadb.exe,重新连接便可:

adb kill-server
adb start-server

adb无法识别

解决方案:

终端下输入:

lsusb

结果:

Bus 001 Device 002: ID 8087:8008 Intel Corp. 
Bus 002 Device 002: ID 8087:8000 Intel Corp. 
Bus 003 Device 015: ID 04ca:0061 Lite-On Technology Corp. 
Bus 003 Device 003: ID 413c:2107 Dell Computer Corp. 
Bus 003 Device 016: ID 04e8:6860 Samsung Electronics Co., Ltd GT-I9100 Phone [Galaxy S II]
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub

其中我的手机是Ltd GT-I9100 Phone [Galaxy S II],记下这一行的04e8:6860

然后输入以下下指令(将其中的ATTR{idVendor}改为04e8,MODE改为6860):

echo '0x1782' | tee -a ~/.android/adb_usb.ini

echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1782", MODE="0666", GROUP="plugdev"' | tee -a/etc/udev/rules.d/51-android.rules

chmod a+r /etc/udev/rules.d/51-android.rules

service udev restart

adb kill-server

adb start-server

Kotlin-android: unresolved reference databinding

解决方案:

Try use this configuration:

In main build.gradle:

buildscript {
    ext.kotlin_version = '<kotlin-version>'
    ext.android_plugin_version = '2.2.0-alpha4'
    dependencies {
        classpath "com.android.tools.build:gradle:$android_plugin_version"
        //... rest of the content
    }
}

App build.gradle:

android {
    dataBinding {
        enabled = true
    }
}

dependencies {
    kapt "com.android.databinding:compiler:$android_plugin_version"
}

kapt {
    generateStubs = true
}

See https://stackoverflow.com/questions/33165324/kotlin-android-unresolved-reference-databinding

Kotlin Dagger2 配置使用

根目录下的build.gradle:

buildscript {
    ext.kotlinVersion = '1.1.2-2'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
    }
}

module下的build.gradle:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

...

dependencies {
...
    //dagger2
    ext.daggerLibVersion = "2.10"
    compile "com.google.dagger:dagger:$daggerLibVersion"
    kapt "com.google.dagger:dagger-compiler:$daggerLibVersion"

    //dagger2 android 一个dagger2 关于Android的增强库 可选项
    kapt "com.google.dagger:dagger-android:$daggerLibVersion"
    //可选项
    kapt "com.google.dagger:dagger-android-support:$daggerLibVersion"
    //可选项
    kapt "com.google.dagger:dagger-android-processor:$daggerLibVersion"
}

Circular dependency between the following tasks

问题现象:在使用Kotlin 1.1.2-4 plugin时,使用kapt,会出现循环依赖的错误;

解决方案:仅需要将Kotlin 1.1.2-4降级到Kotlin 1.1.2-2版本即可。

根目录下的build.gradle:

buildscript {
    //此处降级
    ext.kotlinVersion = '1.1.2-2'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
    }
}

java.lang.NoClassDefFoundError: com.android.tools.fd.runtime.AppInfo

问题现象:Android Studio直接从build文件目录下拿到apk装到目标机器上,出现了异常报错的情况;

原因分析:Android Studio在调试的时候Instance Run给程序添加了一些自己的代码,但脱离Android Studio去安装到目标机器时,它就肯定找不到这个类;

解决方案:

  • 在Android Studio中关闭Instance Run功能,并且清除build目录。然后再进行Run的安装,此时可以直接从新生成的build目录提取apk,安装到目标机器上;
  • clean整个工程,然后再Build APK,此时生成新的APK在build目录下,提取APK安装到目录机也不会报错了;


开源项目中的坑

FancyCoverFlow

这个控件在API高于16的设备中,滑动的过程中会强制刷新一遍,导致切换和初始化的时候都很卡,当时觉得这个效果挺好,后来用上之后这个控件成了性能瓶颈;

Fresco

这个控件用起来特别爽,唯一的缺陷的相比于相同功能的其它开源项目(Glide、Picasso),体积过大;

ActiveAndroid

这个轻量级的数据库框架也挺好用,但缺陷是初始化耗时,可以看一下这篇文章:在Android中使用反射到底有多慢?

JXL

一个读写Excel文件的开源库,用起来很方便,但有个问题:文件大小超过5M直接挂掉;

JPinyin

汉字转拼音的一个工具库,APK加密后这个库不能正常使用,后来查出是因为项目中数据的问题,加密后数据的内容变化了,最后只能自己改造,将数据按照我们自己的方式处理。

Glide

Google推荐的图片加载库,专注于流畅的滚动。Glide在Listview或GridView等列表中使用时,需设置.dontAnimate().into(ImageView)。清除默认动画,否则图片显示会有异常。

Crosswalk

Crosswalk是一款开源的Web引擎,其基于 Chromium/Blink 的应用运行环境,对于混合开发的轻量级应用尤为受欢迎。缺点是需要承受APK激增 20M~ OR 40M~ 体积;另外我在使用XWalkView播放html5的video时,在Tablet系统上运行,会出现视频被一块黑色区域挡住的问题,Phone或者TV都没有此问题,目前尚未找到解决方案。

End

在工作过程中肯定会遇到很多问题,虽然网络发达,但亲力亲为去解决问题会让自己对各个知识点的理解更深刻,工作经验就是一个一个坑填过来的,上面的总结只是冰山一角,强烈推荐看一看StackOverflow和Android Issue Tracker上关于android标签的热点问题,里面都是开发过程中可能会遇到的问题,非常值得一读。另外,如果能翻墙,可以参看这个视频《Developing for Android: the Naughty Bits》,讲述了为什么开发Android这么难。

—— SnailStudio 后记于 2017.5