让多个Fragment 切换时不重新实例化

在项目中需要进行Fragment的切换,一直都是用replace()方法来替换Fragment:

1
2
3
4
5
6
7
8
9
public void switchContent(Fragment fragment) {
if(mContent != fragment) {
mContent = fragment;
mFragmentMan.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, R.anim.slide_out)
.replace(R.id.content_frame, fragment) // 替换Fragment,实现切换
.commit();
}
}

但是,这样会有一个问题:
每次切换的时候,Fragment都会重新实例化,重新加载一边数据,这样非常消耗性能和用户的数据流量。

就想,如何让多个Fragment彼此切换时不重新实例化?

翻看了Android官方Doc,和一些组件的源代码,发现,replace()这个方法只是在上一个Fragment不再需要时采用的简便方法。

正确的切换方式是add(),切换时hide()add()另一个Fragment;再次切换时,只需hide()当前,show()另一个。
这样就能做到多个Fragment切换不重新实例化:

1
2
3
4
5
6
7
8
9
10
11
12
public void switchContent(Fragment from, Fragment to) {
if (mContent != to) {
mContent = to;
FragmentTransaction transaction = mFragmentMan.beginTransaction().setCustomAnimations(
android.R.anim.fade_in, R.anim.slide_out);
if (!to.isAdded()) { // 先判断是否被add过
transaction.hide(from).add(R.id.content_frame, to).commit(); // 隐藏当前的fragment,add下一个到Activity中
} else {
transaction.hide(from).show(to).commit(); // 隐藏当前的fragment,显示下一个
}
}
}

————Edited 2015.2.7————-

问题一:保存UI与数据的内存消耗

上面所述为避免重新实例化而带来的“重新加载一边数据”、“消耗数据流量”,其实是这个Fragment不够“纯粹”。

Fragment应该分为UI FragmentHeadless Fragment

前者是指一般的定义了UI的Fragment,后者则是无UI的Fragment,即在onCreateView()中返回的是null。将与UI处理无关的异步任务都可以放到后者中,而且一般地都会在onCreate()中加上setRetainInstance(true),故而可以在横竖屏切换时不被重新创建和重复执行异步任务。

这样做了之后,便可以不用管UI Fragment的重新创建与否了,因为数据和异步任务都在无UI的Fragment中,再通过Activity 的 FragmentManager 交互即可。

只需记得在Headless Fragment销毁时将持有的数据清空、停止异步任务。

根据Android系统版本禁用/启用某些Activity

在开发中,需要用到Android新版本的特性,而又要适配旧版本。如何让Activity低版本中不显示,而高版本无影响呢?

在查看APIDemos源码时,看到AndroidManifest.xml中有这样一段

AndroidManifest.xml
1
2
3
4
5
6
<activity android:name=".animation.AnimationLoading"
android:enabled="@bool/atLeastHoneycomb">
<intent-filter>
...
</intent-filter>
</activity>

android:enabled这个属性是控制Activity是否能被实例化的。
@bool/atLeastHoneycomb是指values/bools.xml中的atLeastHoneycomb属性的值。
该文件定义了许多与平台相关的atLeastXxx属性,默认值都是false

在APIDemos/src/res/ 目录下有许多的values-vXX目录,各自下面都有bools.xml
values-v11/bools.xml中可以看到

bools.xml
1
2
3
4
<resources>
<!-- True if running under Honeycomb or later. -->
<bool name="atLeastHoneycomb">true</bool>
</resources>

即,.animation.AnimationLoading这个Activity在v11版本以上系统可以被实例化,以下则不能。
如此便做到了让应用更好的适配各个版本。

延伸:

activity-alias
为某个Activity起别名,重复使用同一个Activity,同样可以做到平台相关的一些处理:
比如有Activity需要在Launcher上创建入口,但又不想在低版本上这样做。

BuildConfig.DEBUG 的妙用

ADT(R17)会自动生成一个名称为BuildConfig的类,该类包含一个DEBUG 常量,该常量会根据当前项目的Build类型自动设置值。
可以通过(BuildConfig.DEBUG) 常量来编写只在Debug模式下运行的代码。
如果有些代码不想在发布后执行,就可以使用该功能。

Added a feature that allows you to run some code only in debug mode.
Builds now generate a class called BuildConfig containing a DEBUGconstant that is automatically set according to your build type. You can check the (BuildConfig.DEBUG) constant in your code to run debug-only functions.

比如调试日志,不想在软件发布后被其他开发者看到,过去的方式是设置一个全局变量,标记软件为DEBUG模式还是其他模式。

1
2
3
4
5
public static boolean DEBUG = true;  
//...
if(DEBUG==true){
Log.d(TAG,"output something");
}

这样打包发布之前还要修改DEBUG变量的值,有时候不记得或者变量到处都有,重新修改、编译、发布,费时费力。

有了BuildConfig.DEBUG之后,代码变成了

1
2
3
if (BuildConfig.DEBUG) {  
Log.d(TAG, "output something");
}

在编码期,编译发布前,BuildConfig.DEBUG的值自动为true
需要打包时,先禁用 “Build Automatically”, “Clean”工程目录,再通过 “Android Tools -> Export Signed Application Package“ 编译打包,BuildConfig.DEBUG的值会被改为false。
开发者自己不用修改其他东西了。

流程如下[1]

  1. Project -> Build Automatically
  2. Project -> Clean
  3. Project -> Build
  4. Android Tools -> Export Android application

从此,Logcat清爽了许多。

[1]-When does ADT set BuildConfig.DEBUG to false?

Android - Service

Android中的服务与windows中的服务非常类似——没有用户操作界面、长时间在后台运行而不易被用户所知。

应用程序可以开启某些服务在后台运行,即使用户切换到其他应用程序也不会被停止。同时,应用程序可以bind一个服务与之交互,甚至可以执行IPC,例如:网络处理、音乐播放、文件输入输出或者是与内容提供者交互,都可以放在后台执行。

服务的两个运行方式

应用的组件(如Activity)可以通过下面两个方式运行服务:

  1. startService():开启服务(started)。
    即使应用被关闭,服务也不会被停止。这种开启方式,没有返回值,不能和应用交互,如下载和上传文件到网络,注意的是:服务应该在业务执行完毕后自我停止。
  2. bindService():绑定服务(bound)。
    服务被绑定后可以与应用交互数据。当应用被关闭后,服务应立即被解绑(unbind)。一个服务可以被多个应用绑定,当这些应用都解绑了服务,服务将被销毁(destoryed)。

Tips:服务可以同时被这两个方式开启。但要注意的是:如果被绑定,则服务只能在解绑后才能被停止。
一般两者混用的调用顺序为:startService()->bindService()->unbindService()->stopService()。

Service的生命周期

  1. onStartCommand():当其他组件startService()来启动服务时,系统调用这个方法。一经执行,服务开始运行在后台。当服务业务执行完毕后应调用stopSelf()自我停止或者其他组件来调用stopService()停止服务。如果这个服务只是提供内容绑定,则不应该去实现这个方法。
  2. onBind():服务通过bindService()方式启动,系统调用此方法。应该提供一个接口给应用,使之能回调服务中的方法(接口其实就服务的代理),方法应返回一个实现了该接口的IBinder对象。如果不想让应用绑定则应该返回null。
  3. onCreate():系统仅在服务第一次被创建时调用。之前会执行上面两个方法
  4. onUnbind():在被解绑时调用。如果服务可以被重新绑定(onRebind()),应返回true。
  5. onRebind():在被解绑(onUnbind()返回true)后又被重新bindService()启动,则会调用此方法。
  6. onDestory():系统会在服务许久不使用或在销毁时调用。可以实现此方法,在服务销毁前清除使用过的资源。

Tips:

  1. 服务的生命周期方法不需要像Activity的生命周期方法那样,在实现方法体中需要显式调用其父类的方法来维护生命周期。
  2. 多次startService()开启服务不会多次调用onCreate()但会多次调用onStart()
  3. 服务在所有绑定组件unbindService()解绑时会被系统销毁,需要自我停止。

Android - Content Provider

ContentProvider在android中的作用是对外共享应用的私有数据,也就是说可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对应用中的数据进行添删改查。

关于数据共享,前面的文件操作模式中知道通过指定文件的操作模式为Context.MODE_WORLD_READABLE或Context.MODE_WORLD_WRITEABLE同样也可以对外共享数据。

那么,为何还要使用ContentProvider 对外共享数据呢?
如果采用文件操作模式对外共享数据,数据的访问方式会因数据存储的方式不同而不同,导致数据的访问方式无法统一,
如:采用xml文件对外共享数据,需要进行xml解析才能读取数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读取数据等等。

使用ContentProvider对外共享数据的好处是统一了数据的访问方式

成为ContentProvider

当应用需要通过ContentProvider对外共享数据时,第一步需要继承ContentProvider并重写下面方法:

ContentProvider - PersonContentProvider.java
1
2
3
4
5
6
7
8
public class PersonContentProvider extends ContentProvider{
public boolean onCreate(){}
public Uri insert(Uri uri, ContentValues values){}
public int delete(Uri uri, String selection, String[] selectionArgs){}
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs){}
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder){}
public String getType(Uri uri){}
}

第二步需要在AndroidManifest.xml 的<provider>节点对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider,ContentProvider 采用了authority(主机名/域名)对它进行唯一标识(详见Content URIs)

Manifest - AndroidManifest.xml
1
2
3
4
5
<manifest .... >
<application android:icon="@drawable/icon" android:label="@string/app_name">
<provider android:name=".PersonContentProvider" android:authorities="net.yrom.providers.personprovider"/>
</application>
</manifest>

Tips:Content Provider在<provider>节点中也可以声明一些访问的权限,当访问者没有申请到权限是不能访问provider的。

ContentProvider类主要方法

  • boolean onCreate()
    该方法在ContentProvider创建后就会被调用, Android开机后, ContentProvider在其它应用第一次访问它时才会被创建。
  • Uri insert(Uri uri, ContentValues values)
    该方法用于供外部应用往ContentProvider添加数据。
  • int delete(Uri uri, String selection, String[] selectionArgs)
    该方法用于供外部应用从ContentProvider删除数据。
  • int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    该方法用于供外部应用更新ContentProvider中的数据。
  • Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
    该方法用于供外部应用从ContentProvider中获取数据。
  • String getType(Uri uri)
    该方法用于返回当前Url所代表数据的MIME类型。

如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头。
例如:要得到所有person记录的Uri为content://net.yrom.provider.personprovider/person,那么返回的MIME类型字符串应该为:“vnd.android.cursor.dir/person”。

如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头。
例如:得到id为10的person记录,Uri为content://net.yrom.provider.personprovider/person/10,那么返回的MIME类型字符串应该为:“vnd.android.cursor.item/person”。

Android - 数据的存储

Android中的数据存储有以下几种方式:

  1. 文件存储
  2. SharedPreferences
  3. SQLite数据库
  4. Content Provider

文件存储

Activity中的openFileOutput()方法可以用于把数据输出到文件中,具体的实现过程与在JavaSE 环境中保存数据到文件中是一样的。
openFileInput()方法则用于读取当前应用的保存的数据

openFileOutpupt详解

openFileOutput(String name, int mode)
name - 指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data//files目录,如: /data/data/net.yrom.xxx/files/xxx.txt
mode - 文件操作模式,即访问权限

0 或者MODE_PRIVATE 默认的模式,文件为私有的,只能本应用程序才能访问;
MODE_APPEND 添加默认,数据将追加到文件末尾;
MODE_WORLD_READABLE 全局可读;!危险
MODE_WORLD_WRITEABLE 全局可写。!危险

eg.

1
2
3
4
5
6
7
8
9
public class FileActivity extends Activity {
//...
public void save2File(byte[] data, String filename) {
//...
FileOutputStream out = this.openFileOutput(filename, Context.MODE_PRIVATE);
out.write( data);
out.close();
}
}

获得应用的文件存储路径

getFileDir() - /data/data/<当前应用包名>/files/
getCacheDir() - /data/data/<当前应用包名>/cache/
Environment.getExternalStorageDirectory() - 用于获取SDCard的目录,
注意:

  1. 写数据应在程序清单文件中加入sdcard的访问权限:
    android.permission.WRITE_EXTERNAL_STORAGE
  2. 先判断sdcard是否挂载:
    Environment.getExternalStorageState() 应返回 Environment.MEDIA_MOUNTED
    eg.获取sdcard的可用大小:
    1
    2
    3
    4
    5
    6
    7
    8
    if(Environment.getExternalStorageState().equals(Environment. MEDIA_MOUNTED)){
    File sd = Environment. getExternalStorageDirectory();
    StatFs stat = new StatFs(sd.getPath());
    long availableBlocks = stat.getAvailableBlocks();
    long blockSize = stat.getBlockSize();
    long availableSize = availableBlocks * blockSize;
    String totalAvailableSize = Formatter.formatFileSize(getApplicationContext(), availableSize);
    }

    SharedPreferences

    Android应用一般采用SharedPreferences来存储于应用相关的配置参数
    其实就是 /data/data/<package name>/shared_prefs/ 目录下的xml文件

Android - Activity

Android组件:Activity、Service、Broadcast Receiver、Content Provider

Activity

Android中的Activity有四个基本状态:

  1. Actived/Runing 一个新的Activity被启动,处于Activity任务栈栈顶,显示在屏幕最前端,此时它处于可见并可和用户交互的激活状态。
  2. Paused 被另一个透明或者 Dialog 样式的 Activity 覆盖时的状态。仍然可见,但已失去焦点,不能与用户交互
  3. Stoped 被另一个Activity覆盖、不可见、失去焦点的状态。
  4. Killed/Destoryed 被系统回收,Activity实例被销毁

这些状态之间转换都依赖于用户的操作。程序员可以决定一个Activity何时启动,但不能决定它何时被销毁。

Activity生命周期方法

在android.app.Activity类中定义了一系列生命周期相关的方法,在应用自定义的Activity中只要复写了这些方法中所需的,就可以确保被android系统调用到。
Activity生命周期