Android-Tips(实用Android开发技巧)

整理本人实际开发中遇到的一些问题以及解决办法和一些开发技巧,以后会不定时更新。

tip:利用“目录”可快速导航

1.追溯sdk中某一个类随sdk版本升高导致的历史变迁。(find API changes)

问题来源:SwipeRefreshLayout源码:判断子View是否能向上滚动(或者是否滚动到顶部):

    /** * @return Whether it is possible for the child view of this layout to * scroll up. Override this if the child view is a custom view. */
    public boolean canChildScrollUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (mTarget instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) mTarget;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                                .getTop() < absListView.getPaddingTop());
            } else {
                return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(mTarget, -1);
        }
    }
  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

进入Android开发官网,假如要查看View API的变化,输入View,选择android.view.View,如图:

Android-Tips(实用Android开发技巧)_第1张图片

进入View的API参考页面(文档页),如图:

Android-Tips(实用Android开发技巧)_第2张图片

从图中可以看到三个主要信息:

  • View是在API level 1添加的
  • View的类层次
  • 通过左侧的API level choose button 可以查看不同API level下的View API,进行纵向的查看,同时可以将不同API level下的View API进行横向对比。

追溯API 变化就是通过上述第三条实现的。比如我们想看看API level 13和 API level 14之间有什么变化,将左侧API level设置为13,查看方法列表:

Android-Tips(实用Android开发技巧)_第3张图片

我们会发现一些API 是灰色的,当鼠标hover过方法名时,会显示出一个提示,如图: 
Android-Tips(实用Android开发技巧)_第4张图片

这个提示告诉我们:View中的canScrollVertically(int direction) 方法是在API level 14以后才添加的,另外canScrollHorizontally(int direction) 也是API level 14以后才添加的方法。当我把API level 切换到14时,发现上述两个方法的颜色变为蓝色了,说明他们的确是在API level 14添加的:

Android-Tips(实用Android开发技巧)_第5张图片

总结:

使用这种方法的好处是不用下载每一个api 版本的源代码,也可以很方便的对比他们之间的变化。开发参考除了可以对比方法的变化以外,还可以对比内部类,接口等变化,当前选中的API level 为9,结果如图: 
Android-Tips(实用Android开发技巧)_第6张图片

2.使用device monitor 中的method profiling 工具寻找app卡顿的元凶

问题来源:使用RxJava时,出现莫名的卡顿,方法嵌套过深,或类关系过于复杂,难以定位问题。

进入sdk->tools文件夹。双击运行monitor.bat 打开device monitor:

Android-Tips(实用Android开发技巧)_第7张图片
左侧Device tab下是当前的设备名以及待调试应用的包名。在要测试某个操作(方法调用)之前,点击method profiling 按钮,弹出对话框: 
Android-Tips(实用Android开发技巧)_第8张图片

输入采样间隔:

Android-Tips(实用Android开发技巧)_第9张图片

输入采样间隔,间隔越大,采集到某个方法调用栈的可能性就越小,可能漏掉某个调用栈,越小,采样精度越高(或者说覆盖率越高)但是采样间隔太小,会导致卡顿。所以需要输入一个合适的采样间隔。输入后点确认,然后在app上执行你的操作,执行完后点stop method profiling,会生成一个名为“ddms+时间戳+.trace”的文件,这个文件记录了方法的调用栈信息,这个文件是我们分析的重点: 
Android-Tips(实用Android开发技巧)_第10张图片

视图中上一栏,展示了在method profile过程中并行执行的所有线程或进程。下一栏的表格展示了方法的调用信息,如方法名,所耗时间,cpu占用等。可参考:http://blog.csdn.net/androiddevelop/article/details/8223805 
Android-Tips(实用Android开发技巧)_第11张图片
表格中相关列名的说明:

Android-Tips(实用Android开发技巧)_第12张图片
点击表格每一栏的名字可以进行排序,根据Name找到上述操作调用的方法,如fetchData:

Android-Tips(实用Android开发技巧)_第13张图片

Parents值得是fetchData的调用入口,而Children指的是fetchData方法中的子调用。 
可以看出每个子调用或父调用的耗时情况,fetchData中的子调用fetchDataImpl耗时22.777ms,继续点击fetchDataImpl可以进入fetchDataImpl的调用栈,分析方式就同fetchData了,通过这种方式可以定位耗时操作的源头: 
Android-Tips(实用Android开发技巧)_第14张图片

总结:

使用method profiling可以分析方法的调用栈,找出app的性能瓶颈。android device monitor的功能很强大,是一个工具的合集,还可以分析Heap Dump,Allocation,Network,查看Device的文件等。好的工具可以提升开发效率,科学使用工具可以事半功倍。另外推荐一款在线app 冷启动性能分析工具:Nimbledroid 可以分析app 冷启动的调用栈,通过分析app冷启动调用栈,可以找到影响app的启动速度的因素。另外,Nimbledroid上可以查找到许多app不同版本的冷启动调用栈信息,我们可以从中借鉴加速app启动的方式。

3.使用显式Intent启动Service

问题来源:Android Decelopers 官网API guide.

Service是无界面的,处于安全性等方面的考虑,应避免隐式启动:

  • 为了避免无意中运行不同应用的 Service,请始终使用显式 Intent 启动您自己的Service,且不必为该服务声明 Intent 过滤器。
  • 为了确保应用的安全性,启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务将响应 Intent,且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会抛出异常。
  • 如果启动哪个服务存在一定的不确定性,而且对这种不确定性的考量非常有必要,则可为服务提供 Intent 过滤器并从 Intent 中排除相应的组件名称,但随后必须使用 setPackage() 方法设置 Intent 的软件包,这样可以充分消除目标服务的不确定性。(注意上一条的限制)
  • 此外,还可以通过添加 android:exported 属性并将其设置为 “false”,确保服务仅适用于您的应用。这可以有效阻止其他应用启动您的服务,即便在使用显式 Intent 时也如此。 
    参考:http://developer.android.com/intl/zh-cn/guide/components/intents-filters.html

总结

启动Service时如没有特殊需求,采用显式启动,且不必为该服务声明 Intent 过滤器。当API>=21时bindService强制接受显式Intent,如果传入隐式Intent,会抛出异常。

4.查找是否有可以启动的Activity

问题来源:使用隐式Intent启动Activity时,如果没有匹配的Activity就会抛出异常。

警告:用户可能没有任何应用处理您发送到 startActivity() 的隐式 Intent。如果出现这种情况,则调用将会失败,且应用会崩溃。要验证 Activity 是否会接收 Intent,请对 Intent 对象调用 resolveActivity()。如果结果为非空,则至少有一个应用能够处理该 Intent,且可以安全调用 startActivity()。如果结果为空,则不应使用该 Intent。如有可能,您应禁用发出该 Intent 的功能。

代码片:

// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}
  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

相关方法:

PackageManager 提供了一整套 query…() 方法来返回所有能够接受特定 Intent 的组件。此外,它还提供了一系列类似的 resolve…() 方法来确定响应 Intent 的最佳组件。例如,queryIntentActivities() 将返回能够执行那些作为参数传递的 Intent 的所有 Activity 列表,而 queryIntentServices() 则可返回类似的服务列表。这两种方法均不会激活组件,而只是列出能够响应的组件。对于广播接收器,有一种类似的方法: queryBroadcastReceivers()

总结

使用隐式Intent启动Activity/Service/BroadcastReceiver时,必须要先判断是否有可处理该Intent的应用,否则会导致应用奔溃。

5.处理最大应用内存限制

问题来源:加载大图出现OOM 
Android-Tips(实用Android开发技巧)_第15张图片

翻译过来就是移动设备使用的系统资源是有约束的,安卓设备的每个应用仅能拥有16MB的运行时内存(这个值应该是2.x的设备的值,已经很老了,许多设备可配置高于这个值),CDD根据不同设备的屏幕大小和密度,定了最小应用内存(设备实现时可以高于这个值)

Note that memory values specified below are considered minimum values and device 
implementations MAY allocate more memory per application.

Android N 对应的内存标准: 
Android-Tips(实用Android开发技巧)_第16张图片

应用应该在这个最小内存值(上限)下进行优化,不要超过这个上限,当然,许多设备配置的上限要大些。比如CDD规定 Android 6.0下,屏幕大小为small/normal,像素密度为320dpi(xxhdpi)的设备最小应用内存为80MB,小米4可以实现为90MB(上限),那么应用在内存优化时,不应该超过90MB这个上限。

注:as little as 翻译为“仅仅”,一些地方翻译成了“最小”是错误的。

参考: 
http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/index.html

CDD Android N: 
http://static.googleusercontent.com/media/source.android.com/zh-CN//compatibility/6.0/android-6.0-cdd.pdf

问题来了,如果你的应用需要120MB的内存,而设备的上限为100MB,该如何解决呢?

  • 使用多个进程,比如两个进程,每个进程可分担60MB,且每个进程的上限都是120MB;
  • 使用JNI,在c code的层面使用内存分配函数,分配的内存,不计在最大内存限制下。
  • 使用OpenGL textures,textures具有独立的内存空间,所耗内存不计在上限中。

参考: 
http://blog.javia.org/how-to-work-around-androids-24-mb-memory-limit/

如何获得设备最大内存上限: 
内存探测:

Runtime rt = Runtime.getRuntime();
long maxMemory = rt.maxMemory();
  
  
  
  
  • 1
  • 2

参考: 
http://stackoverflow.com/questions/2630158/detect-application-heap-size-in-android

总结

在加载大图时,往往会消耗大量的内存。

For example, the camera on the Galaxy Nexus takes photos up to 2592x1936 pixels (5 megapixels). If the bitmap configuration used is ARGB_8888 (the default from the Android 2.3 onward) then loading this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the per-app limit on some devices.

我们必须要知道和理解上述的内存限制,然后使用内存探测判断设备支持的最大应用内存,然后根据业务需求采取不同的方式,应对这一限制。其实微博,微信等,都是通过进程拆分解决这一问题的。微博有四个进程:主进程、推送进程、图片浏览进程以及插件进程,通过拆分,单个进程只占用少量的内存,当系统内存吃紧时,会首先杀死占用内存较大的进程,通过进程的分离,可以让解决内存上限问题,同时起到进程保活的作用。

6.Activity销毁时,其attach的Fragment一定会销毁吗

问题来源:一次面试,被问到Activity有被销毁的风险,如配置改变,系统内存不足等,这种情况下,该如何及时地保存Activity中的数据,除了在onPause和onSaveInstanceState中保存,还有别的方式吗?

使用Fragment保存。

Code:

//activity
private Object mCachedData;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mCachedData = retainFragment.mCachedData;
    if (mCachedData == null) {
        mCachedData = new Object();
        retainFragment.mCachedData = mCachedData;
    }
    ...
}
//fragment
class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public Object mCachedData;
    public RetainFragment() {}
    public static RetainFragment  findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}
  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

我们通常会认为,当Activity销毁时,其attach的Fragment也会销毁,然而当Fragment调用了setRetainInstance(true)后,其实例不会销毁。以下是setRetainInstance()方法的官方说明:

public void setRetainInstance (boolean retain)

Added in API level 11

Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:

1.onDestroy() will not be called (but onDetach() still will be, because the fragment is being detached from its current activity). 
2.onCreate(Bundle) will not be called since the fragment is not being re-created. 
3.onAttach(Activity) and onActivityCreated(Bundle) will still be called.

使用条件:该fragment不加入返回栈,即使用FragmentManager通过事务添加该fragment时,不调用addToBackStack()。

调用了setRetainInstance(true)后,Activity被销毁以及重新创建的过程中,fragment的实例将会被保存。fragment的onDestory()方法将不会被调用,fragment的onCreate(Bundle)方法也不会被调用。

如此,我们就可以用fragment来保存数据了。

总结

除了在onPause和onSaveInstanceState中可以保存数据外,还可以使用不含任何UI元素的Fragment来保存数据。

Fragment与其关联的Activity的生命周期息息相关,具体可见FragmentActivity的实现,在FragmentActivity的每一个生命周期方法中都间接地调用了fragment的相应生命周期方法,所以一般情况下,Activity的某个生命周期方法被调用时,fragment的一个或多个生命周期方法也会被调用,但是当setRetainInstance(true)后,fragment的生命周期方法的调用会略有不同。

你可能感兴趣的:(Android-Tips(实用Android开发技巧))