zoukankan      html  css  js  c++  java
  • Android后台处理最佳实践(Best Practices for Background Jobs)

            本课将告诉你如何通过后台加载来加速应用启动和降低应用耗电。

    后台跑服务

             除非你做了特殊指定,否则在应用中的大部分前台操作都是在一个特殊的UI线程里面进行的。这有可能会导致一些问题,因为长时间运行的操作会影响到你应用的响应速度。为了避免这个问题,android框架提供了一系列帮助你在后台通过线程推迟加载的功能,被使用得最多的非IntentService莫属了。

             本课将向你描述如何实现一个IntentService,发送请求操作并向其它组件报告结果。

    创建一个后台服务

             本课将直观地告诉你如何通过后台线程执行操作,通过它来执行耗时操作从而避免你的界面无法及时响应的问题。IntentService是不会受窗口生命周期回调的影响的,所以在继续运行它之前,你需要关闭AsyncTask。

             每个IntentService都是有限制条件的:

    (1)它不可以直接和应用的界面进行交互,为了将操作结果返回给界面,你需要将他们发送到窗口;

    (2) 工作请求是按顺序进行的,当已经有一个操作在IntentService中运行时,如果这是你发送另外一个请求,需要等到第一个操作执行完毕后才会继续后面的请求;

    (3) 在IntentService中运行的操作是不可以被中断的。

     

            然而,多数情况下一个IntentService是后台操作最简单的处理方式。

            本课将告诉你如何创建你自己的IntentService子类,还会向你展示如何创建一个必要的onHandleIntent()回调。最后,将告诉你如何在清单文件中定义IntentService。

     

    创建一个IntentService

             为了在你的应用中创建一个IntentService组件,需要定义一个继承于IntentService的类并复写其onHandleIntent()方法,比如:

    1. <span style="font-size:18px">public class RSSPullService extends IntentService {  
    2.     @Override  
    3.     protected void onHandleIntent(Intent workIntent) {  
    4.         // Gets data from the incoming Intent  
    5.         String dataString = workIntent.getDataString();  
    6.         ...  
    7.         // Do work here, based on the contents of dataString  
    8.         ...  
    9.     }  
    10. }  
    11. </span>  

             注意,对于任何一个Service都会回调的那些方法,比如onStartCommand(),都会自动地被IntentService引用。在一个IntentService中,你应该避免复写这些回调方法。

     

    在清单文件Manifest中定义IntentService

             IntentService同样需要在你应用的清单文件中有一个入口,通过在<application>标签下声明<service>的方式来为IntentService提供入口:

     
    1. <span style="font-size:18px"><application  
    2.         android:icon="@drawable/icon"  
    3.         android:label="@string/app_name">  
    4.         ...  
    5.         <!--  
    6.             Because android:exported is set to "false",  
    7.             the service is only available to this app.  
    8.         -->  
    9.         <service  
    10.             android:name=".RSSPullService"  
    11.             android:exported="false"/>  
    12.         ...  
    13.     <application/>  
    14. </span>  

             上例中的“android:name”属性指定了IntentService的类名。

             注意,<service>标签没有包含IntentFilter过滤器。该窗口通过一个明确地Intent向服务发送工作请求,所以不需要任何过滤器。也就是说,只有在同一个应用内,或者是有相同ID的其它应用才可以访问这个服务。

             现在你有了基本的IntentService类,你可以通过Intent对象发送工作请求了。

    向后台服务发送工作请求

             之前的课程向我们展示了如何创建一个IntentService类。本课将告诉你如何通过发送Intent来触发IntentService执行一个操作。这个Intent可以包含IntentService需要处理的可选数据。你可以在Activity或者Fragment的任何一个地方向IntentService传递Intent。

     

    创建并发送一个工作请求给IntentService

             为了创建一个工作请求并将其发送到IntentService,需要创建一个明确地Intent来添加工作请求数据,然后通过调用IntentService的StartService()方法来发送它。

             具体请看下面实例:

    1、为IntentService的子类RSSPullService创建一个新的、明确地Intent。

     
    1. <span style="font-size:18px">/* 
    2.  * Creates a new Intent to start the RSSPullService 
    3.  * IntentService. Passes a URI in the 
    4.  * Intent's "data" field. 
    5.  */  
    6. mServiceIntent = new Intent(getActivity(), RSSPullService.class);  
    7. mServiceIntent.setData(Uri.parse(dataUrl));  
    8. </span>  

    2、调用startService()方法

    1. <span style="font-size:18px">// Starts the IntentService  
    2. getActivity().startService(mServiceIntent);  
    3. </span>  

             注意,你可以在Activity或者Fragment的任何地方发送工作请求。比如,如果你需要首先获取用户输入,你可以在按钮点击或者类似于手势操作的回调中来发送请求。

             一旦你调用了startService()方法,IntentService会处理定义在onHandleIntent()方法中的工作,然后自己停止。

             下一步是向原始的Activity或者Fragment报告工作请求的结果,下一个将告诉你如何通过BroadcastReceiver来实现这个功能。

     

    报告工作状态

             本课将告诉你如何将后台服务的请求工作状态报告给发送请求的组件。这将允许你,比如报告一个窗口对象的UI更新请求状态。一般推荐使用LocalBroadcastManager来发送和接收这些状态,但这仅限于在你自己应用的各组件中广播Intent。

     

    从IntentService报告状态

             为了在IntentService中向其他组件发送工作请求状态,首先你需要创建一个包含状态信息数据的Intent,作为了一个选项,你可以在Intent中添加一个操作或者数据URI。

             下一步,通过调用LocalBroadcastManager.sendBroadcast()方法来发送Intent,在你应用中发送到其它组件的Intent是注册过的。通过LocalBroadcastManager的getInstance()方法来实例化LocalBroadcastManager。

    比如:

     
    1. <span style="font-size:18px">public final class Constants {  
    2.     ...  
    3.     // Defines a custom Intent action  
    4.     public static final String BROADCAST_ACTION =  
    5.         "com.example.android.threadsample.BROADCAST";  
    6.     ...  
    7.     // Defines the key for the status "extra" in an Intent  
    8.     public static final String EXTENDED_DATA_STATUS =  
    9.         "com.example.android.threadsample.STATUS";  
    10.     ...  
    11. }  
    12. public class RSSPullService extends IntentService {  
    13. ...  
    14.     /* 
    15.      * Creates a new Intent containing a Uri object 
    16.      * BROADCAST_ACTION is a custom Intent action 
    17.      */  
    18.     Intent localIntent =  
    19.             new Intent(Constants.BROADCAST_ACTION)  
    20.             // Puts the status into the Intent  
    21.             .putExtra(Constants.EXTENDED_DATA_STATUS, status);  
    22.     // Broadcasts the Intent to receivers in this app.  
    23.     LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);  
    24. ...  
    25. }  
    26. </span>  

     

    从IntentService接收广播状态

             为了能够接收Intent对象,需要定义一个BroadcastReciver的子类。在该类中,实现BroadcastReceiver的onReceive()回调方法,在接收到一个Intent时LocalBroadcastManager会引用它。LocalBroadcastManager将接收到的Intent传递到BroadcastReceiver的onRecive()方法中。

    比如:

    1. <span style="font-size:18px">// Broadcast receiver for receiving status updates from the IntentService  
    2. private class ResponseReceiver extends BroadcastReceiver  
    3. {  
    4.     // Prevents instantiation  
    5.     private DownloadStateReceiver() {  
    6.     }  
    7.     // Called when the BroadcastReceiver gets an Intent it's registered to receive  
    8.     @  
    9.     public void onReceive(Context context, Intent intent) {  
    10. ...  
    11.         /* 
    12.          * Handle Intents here. 
    13.          */  
    14. ...  
    15.     }  
    16. }  
    17. </span>  

             一旦你定义了BroadcastReceiver,你就可以通过指定动作、类别和数据等过滤信息来匹配它了。为了达到这种效果,你需要创建一个IntentFilter。下面的代码向你展示了如何定义filter:

    1. <span style="font-size:18px">// Class that displays photos  
    2. public class DisplayActivity extends FragmentActivity {  
    3.     ...  
    4.     public void onCreate(Bundle stateBundle) {  
    5.         ...  
    6.         super.onCreate(stateBundle);  
    7.         ...  
    8.         // The filter's action is BROADCAST_ACTION  
    9.         IntentFilter mStatusIntentFilter = new IntentFilter(  
    10.                 Constants.BROADCAST_ACTION);  
    11.       
    12.         // Adds a data filter for the HTTP scheme  
    13.         mStatusIntentFilter.addDataScheme("http");  
    14.         ...  
    15. </span>  

             为了在系统中注册BroadcastReceiver和IntentFilter,你需要实例化LocalBroadcastManager并调用其registerReceiver()方法。下例展示的是如何注册BroadcastReceiver和其过滤器的过程:

    1. <span style="font-size:18px">// Instantiates a new DownloadStateReceiver  
    2.         DownloadStateReceiver mDownloadStateReceiver =  
    3.                 new DownloadStateReceiver();  
    4.         // Registers the DownloadStateReceiver and its intent filters  
    5.         LocalBroadcastManager.getInstance(this).registerReceiver(  
    6.                 mDownloadStateReceiver,  
    7.                 mStatusIntentFilter);  
    8.         ...  
    9. </span>  

             一个BroadcastReceiver可以操作多于一种类型的广播Intent对象,每个类型都有自己的操作。这种特征允许你在不同的action中运行代码,不需要为每个action都定义一个BroadcastReceiver。为了为同一个BroadcastReceiver定义其它的IntentFilter,创建IntentFilter并重复调用registerReceiver()。比如:

     
    1. <span style="font-size:18px">/* 
    2.          * Instantiates a new action filter. 
    3.          * No data filter is needed. 
    4.          */  
    5.         statusIntentFilter = new IntentFilter(Constants.ACTION_ZOOM_IMAGE);  
    6.         ...  
    7.         // Registers the receiver with the new filter  
    8.         LocalBroadcastManager.getInstance(getActivity()).registerReceiver(  
    9.                 mDownloadStateReceiver,  
    10.                 mIntentFilter);  
    11. </span>  

             发送一个广播Intent不会start或者resume一个窗口。即使你的窗口在后台,窗口中的BroadcastReceiver都是可以接收并处理Intent对象的,但并不会强制让你的应用处于前台。当你的窗口处于后台时如果你想向用户通知这个事件,你可以使用Notification。在接收一个广播Intent时是绝不会启动一个窗口的。

     

    在后台加载数据

             对于你需要显示的数据,但有需要花时间去通过ContentProvider查询时,如果你直接在窗口层面去执行查询操作,可能会严重影响界面的响应速度,比如ANR。就算不会ANR,用户也会明显地感觉到卡顿的现象。为了避免这种问题,你应该在非UI线程里面来初始化查询操作,直到等待它结束后再窗口显示结果。

             你可以通过一个对象在后台执行查询同步,待查询结束后更新UI。这个对象就是CursorLoader。除了初始化后台查询外,当查询有变动时CursorLoader会自动地重新查询数据。

             本课将向你描述如何通过CursorLoader执行后台查询操作。在课程中用到了V1-SupportLibrary版本的类,它支持V1.6及以上的版本执行此操作。

     

    通过CursorLoader执行查询操作

             通过CursorLoader在后台执行同步查询有别于ContentProvider,它会返回结果到调用它的Activity或者FragmentActivity。这样就允许Activity或者FragmentActivity在后台查询数据时可以和用户交互。

     

    定义一个使用CursorLoader的窗口

             为了能够在Activity或者FragmentActivity中使用CursorLoader,需要使用LoaderCallbacks<Cursor>接口,CursorLoader引用接口定义的回调和类进行交互;本课和下一课将详细描述每一个回调。

             比如,下面的实例向你展示如何使用依赖库中的CursorLoader定义FragmentActivity。通过扩展FragmentActivity,可以达到通过Fragment使用CursorLoader一样的效果。

     
    1. <span style="font-size:18px">public class PhotoThumbnailFragment extends FragmentActivity implements  
    2.         LoaderManager.LoaderCallbacks<Cursor> {  
    3. ...  
    4. }  
    5. </span>  

    初始化查询操作

             为了初始化查询操作,你需要调用LoadManager的initLoader()方法,它初始化后台框架,你可以在用户进入查询的数据后执行该操作,或者,如果你不需要任何数据,你可以在onCreate()或者onCreateView()中执行该操作,比如:

     
    1. <span style="font-size:18px">// Identifies a particular Loader being used in this component  
    2.     private static final int URL_LOADER = 0;  
    3.     ...  
    4.     /* When the system is ready for the Fragment to appear, this displays 
    5.      * the Fragment's View 
    6.      */  
    7.     public View onCreateView(  
    8.             LayoutInflater inflater,  
    9.             ViewGroup viewGroup,  
    10.             Bundle bundle) {  
    11.         ...  
    12.         /* 
    13.          * Initializes the CursorLoader. The URL_LOADER value is eventually passed 
    14.          * to onCreateLoader(). 
    15.          */  
    16.         getLoaderManager().initLoader(URL_LOADER, nullthis);  
    17.         ...  
    18.     }</span>  

    注意:getLoaderManager()方法仅仅适用于Fragment类,为了能够在FragmentActivity中获取LoaderManager,需通过调用getSupportLoaderManager()。

     

    开始查询

             为了能够尽快地初始化后台框架,系统会调用你类中的onCreateLoader()方法,为了能够开始查询,需要从该方法中反馈一个CursorLoader对象。你可以初始化一个空的CursorLoader对象然后通过它来定义查询操作,或者你可以在初始化对象的同时定义查询操作。

     
    1. <span style="font-size:18px">/* 
    2. * Callback that's invoked when the system has initialized the Loader and 
    3. * is ready to start the query. This usually happens when initLoader() is 
    4. * called. The loaderID argument contains the ID value passed to the 
    5. * initLoader() call. 
    6. */  
    7. @Override  
    8. public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle)  
    9. {  
    10.     /* 
    11.      * Takes action based on the ID of the Loader that's being created 
    12.      */  
    13.     switch (loaderID) {  
    14.         case URL_LOADER:  
    15.             // Returns a new CursorLoader  
    16.             return new CursorLoader(  
    17.                         getActivity(),   // Parent activity context  
    18.                         mDataUrl,        // Table to query  
    19.                         mProjection,     // Projection to return  
    20.                         null,            // No selection clause  
    21.                         null,            // No selection arguments  
    22.                         null             // Default sort order  
    23.         );  
    24.         default:  
    25.             // An invalid id was passed in  
    26.             return null;  
    27.     }  
    28. }  
    29. </span>  

             一旦生成了后台框架的对象,系统就会开始在后台执行查询操作,当查询操作执行完成后,后台框架会调用onLoadFinished()方法,这将在下一个作详细讨论。

     

    处理查询结果

             正如前面课程所述,你应该在你所实现类的onCreateLoader()方法中通过CursorLoader加载你的数据,加载器会在你Acitivity或者FragmentActivity的LoaderCallbacks.onLoadFinished()方法中返回查询结果。该方法的其中一个入参为包含查询结果的游标。你可以使用这个对象来更新你的数据或者做其它操作。

             除了onCreateLoader()和onLoadFinished()方法,你还需要实现onLoaderReset()方法,这个方法会在数据更新更新时被调用,当数据变化时,框架会重新执行当前的查询操作。

     

    处理查询结果

             为了显示从CursorLoader返回的游标数据,你需要自定义一个继承于AdapterView的视图,并为这个视图定义一个继承于CursorAdapter的适配器。然后系统会自动地将数据从游标移到视图。

             在你显示任何数据之前你可以为视图和适配器建立连接,然后再onLoadFinished()方法中将游标移到适配器。当你将游标移到适配器后,系统会自动地更新视图。在游标的数据有改动时同样会更新视图。

             比如:

     
    1. <span style="font-size:18px">public String[] mFromColumns = {  
    2.     DataProviderContract.IMAGE_PICTURENAME_COLUMN  
    3. };  
    4. public int[] mToFields = {  
    5.     R.id.PictureName  
    6. };  
    7. // Gets a handle to a List View  
    8. ListView mListView = (ListView) findViewById(R.id.dataList);  
    9. /* 
    10.  * Defines a SimpleCursorAdapter for the ListView 
    11.  * 
    12.  */  
    13. SimpleCursorAdapter mAdapter =  
    14.     new SimpleCursorAdapter(  
    15.             this,                // Current context  
    16.             R.layout.list_item,  // Layout for a single row  
    17.             null,                // No Cursor yet  
    18.             mFromColumns,        // Cursor columns to use  
    19.             mToFields,           // Layout fields to use  
    20.             0                    // No flags  
    21.     );  
    22. // Sets the adapter for the view  
    23. mListView.setAdapter(mAdapter);  
    24. ...  
    25. /* 
    26.  * Defines the callback that CursorLoader calls 
    27.  * when it's finished its query 
    28.  */  
    29. @Override  
    30. public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {  
    31.     ...  
    32.     /* 
    33.      * Moves the query results into the adapter, causing the 
    34.      * ListView fronting this adapter to re-display 
    35.      */  
    36.     mAdapter.changeCursor(cursor);  
    37. }</span>  

     

    删除旧的游标信息

             当游标非法时CursorLoader会被重置,这多数情况发生在游标的数据有改动时,在重新执行查询操作前,框架会调用你所实现的onLoaderReset()方法。在这个回调中,你应该删除当前游标的所有信息来避免内存泄露。一旦结束回调onLoaderReset()方法后,CursorLoader会重新执行查询操作。

             比如:

     
    1. <span style="font-size:18px">/* 
    2.  * Invoked when the CursorLoader is being reset. For example, this is 
    3.  * called if the data in the provider changes and the Cursor becomes stale. 
    4.  */  
    5. @Override  
    6. public void onLoaderReset(Loader<Cursor> loader) {  
    7.       
    8.     /* 
    9.      * Clears out the adapter's reference to the Cursor. 
    10.      * This prevents memory leaks. 
    11.      */  
    12.     mAdapter.changeCursor(null);  
    13. }  
    14. </span>  

    管理设备的激活状态

             当一个android设备处于空闲状态时,它首先会变暗,然后会关屏,最终会让CPU停止工作。这样处理是为了避免设备的电池被快速地耗尽,然而有些时候你的应用需要一些不同的表现:

    (1)游戏或者电影应用可能需要保持屏幕常亮;

    (2)有些应用虽然不需要屏幕常亮,但在CPU执行完核心操作之前同样需要保持程序运行。

    本课的目的是告诉你在避免电池被快速耗尽的情况下如何保持设备处于激活状态。

    保持设备处于激活状态

             为了避免电池被耗尽,android设备会在处于空闲状态时立即切换到休眠状态。然而,有些时候一个应用需要保持屏幕常亮或者CPU直到某些事情被处理完成。

             你该采取什么操作取决于你应用的需求。然而,通用的规则是你应该使用最轻量级的操作来处理你的应用程序,使你的应用减少对系统资源的占用。下面将向你描述通过怎样地操作来使得你对应用的处理和系统默认的休眠行为相容。

     

    保持屏幕常亮

             某些应用需要保持屏幕常亮,比如游戏或者电影应用。最好的方式是在你的窗口中使用FLAG_KEEP_SCREEN_ON属性(只在一个窗口,绝不是在一个服务或者其它应用组件中),比如:

     
    1. <span style="font-size:18px">public class MainActivity extends Activity {  
    2.   @Override  
    3.   protected void onCreate(Bundle savedInstanceState) {  
    4.     super.onCreate(savedInstanceState);  
    5.     setContentView(R.layout.activity_main);  
    6.     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
    7.   }  
    8. </span>  

             这种先进的处理方式有别于唤醒锁,它不需要特殊的权限,平台会正确地管理应用之间的切换,你不需要担心自己的应用没有释放没有使用的资源。

             实现该功能的另一种思路是在你应用xml文件中使用android:keepScreenOn属性:

     
    1. <span style="font-size:18px"><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     android:layout_width="match_parent"  
    3.     android:layout_height="match_parent"  
    4.     android:keepScreenOn="true">  
    5.     ...  
    6. </RelativeLayout>  
    7. </span>  

             使用andorid:keepScreenOn=”true”和使用FLAG_KEEP_SCREEN_ON是一样的效果。你可以使用适合于你应用的任何一种方式。通过在程序中设置你窗口常亮状态的优势是:它可以清楚这个标志,从而可以关闭屏幕。

     

    保持CPU持续工作

             如果你希望在设备休眠之前CPU能够完成需要处理的工作,你可以使用一个叫唤醒锁的PowerManager系统服务。唤醒锁允许你的应用可以控制主机设备电源的状态。

             创建和保持唤醒锁会在一定程度上对电池的寿命有所影响,因此你应该只在非常有必要的情况下使用它,并尽量控制使用时间。比如,你绝不应该在一个窗口中使用唤醒锁,正如上面所描述的那样,如果你想保持当前窗口的屏幕常亮,你可以使用FLAG_KEEP_SCREEN_ON。

             应该使用唤醒锁的情况可能就是后台服务在屏幕关闭时需要通过唤醒锁保持CPU持续工作。再次声明,尽量限制它的使用时间,因为它会影响到电池的寿命。

             为了使用唤醒锁,首先需要在清单文件中添加WAKE_LOCK权限:

     
    1. <span style="font-size:18px"><uses-permission android:name="android.permission.WAKE_LOCK" /></span>  

             如果你的应用包含一个使用服务处理某些事情的广播接收器,你可以通过WakefulBroadcastReceiver来管理你的唤醒锁,正如使用WakefulBroadcastReceiver一课中所描述的那样,这是一个比较好的处理方式。如果你的应用没有遵循这种方式,通过下面的代码你可以直接设置唤醒锁:

     
    1. <span style="font-size:18px">PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);  
    2. Wakelock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,  
    3.         "MyWakelockTag");  
    4. wakeLock.acquire();  
    5. </span>  

             为了释放唤醒锁,你需要调用wakelock的release()方法。它将释放你对CPU的声明,在你的应用结束工作后尽快关闭唤醒锁避免电池被耗尽。

     

    使用WakefulBroadcastReceiver

             使用广播接收器和服务可以让你很好地管理后台任务的生命周期。

             一个WakefulBroadcastReceiver是广播接收器的一个特殊类型,它可以创建和管理你应用的PARITAL_WAKE_LOCK。一个WakeBroadcastReceiver接收到广播后将工作传递给Service(一个典型的IntentService),直到确保设备没有休眠。如果你在交接工作给服务的时候没有保持唤醒锁,在工作还没完成之前就允许设备休眠的话,将会出现一些你不愿意看到的情况。

             要使用WakefulBroadcastReceiver的第一步是在清单文件中添加它,和其它广播接收器是一样的:

     
    1. <span style="font-size:18px"><receiver android:name=".MyWakefulReceiver"></receiver></span>  

             接下来是在代码中通过startWakefulService()来启动MyIntentService。和starService()方法相比,除了在服务启动时可以保持唤醒锁外,通过startWakefulService()方法传递的Intent可以保持一个额外的唤醒锁:

     
    1. <span style="font-size:18px">public class MyWakefulReceiver extends WakefulBroadcastReceiver {  
    2.   
    3.     @Override  
    4.     public void onReceive(Context context, Intent intent) {  
    5.   
    6.         // Start the service, keeping the device awake while the service is  
    7.         // launching. This is the Intent to deliver to the service.  
    8.         Intent service = new Intent(context, MyIntentService.class);  
    9.         startWakefulService(context, service);  
    10.     }  
    11. }</span>  

             当服务执行完成后,系统会调用MyWakefulReceiver的completeWakefulIntent()方法来释放唤醒锁,completeWakefulIntent()方法携带的参数是从WakefulBroadcastReceiver传递过来的intent:

    1. <span style="font-size:18px">public class MyIntentService extends IntentService {  
    2.     public static final int NOTIFICATION_ID = 1;  
    3.     private NotificationManager mNotificationManager;  
    4.     NotificationCompat.Builder builder;  
    5.     public MyIntentService() {  
    6.         super("MyIntentService");  
    7.     }  
    8.     @Override  
    9.     protected void onHandleIntent(Intent intent) {  
    10.         Bundle extras = intent.getExtras();  
    11.         // Do the work that requires your app to keep the CPU running.  
    12.         // ...  
    13.         // Release the wake lock provided by the WakefulBroadcastReceiver.  
    14.         MyWakefulReceiver.completeWakefulIntent(intent);  
    15.     }  
    16. }</span>  

    原文:http://developer.android.com/training/best-background.html

  • 相关阅读:
    Jsoup的学习
    String中对字符串进行操作的一些方法
    Httpclient的学习(一)
    初识爬虫见到的两个类 BufferedWriter和 BufferedReader
    ZooKeeper学习笔记(二)——内部原理
    FastDFS安装指南
    基于CentOS6.5的Dubbo及Zookeeper配置
    虚拟机克隆后遇到的网络相关的问题
    ElasticSerach 6.x的安装及配置
    YARN-HA高可用集群搭建
  • 原文地址:https://www.cnblogs.com/android100/p/Android-Best-Practices.html
Copyright © 2011-2022 走看看