| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
bmri1550
8年前发布

浅谈 Android Service

   <h3>Serviec(服务)简述</h3>    <ol>     <li> <p>什么是Service</p> <p>Service 是一个可以在后台执行长时间运行操作而不提供用户界面的应用组件。Service 可以由其他应用组件启动,即便用户切换到其他应用,Service 仍将在后台继续运行。此外,组件可以绑定到 Service ,进行交互,甚至执行进程间通信(IPC)。例如,Service 可以处理网络事务,播放音乐,执行文件读写或与内容提供程序交互,这一切都可以在后台进行。</p> </li>     <li> <p>服务的两种基本形式</p> <p>启动</p> <pre>  <code class="language-java">当应用组件(如 Activity )通过调用 startService() 启动服务时,服务处于 “启动” 状态,一旦启动,服务可以在后台无限期运行,即使启动服务的组件被销毁了也不受影响。已经启动的服务通常执行单一操作,而且不会讲结果返回给调用方。例如,它可能通过网络下载或者上传文件。操作完成后,服务会自动停止运行。</code></pre> <p>绑定</p> <pre>  <code class="language-java">当应用组件通过调用 bindService() 绑定到服务时,服务处于绑定状态。绑定服务提供了一个客户端-服务器( client-serve )接口,允许组件与服务进行交互,发送请求,获取结果,甚至是利用进程间通信( IPC )跨进程执行这些操作。只有与另一个组件绑定时,绑定服务才会运行。多个组件可以绑定同个服务,但全部取消绑定后,该服务将会被销毁。</code></pre> <p>虽然服务的形式有两种,但服务可以同时以两种方式运行,也就是说,它既可以是启动服务(以无限期运行),也允许绑定。问题在于你是否实现一组回调方法: onStartCommand() (允许组件启动服务) 和 onBind() (允许绑定服务)。</p> <p>无论应用是否处于启动状态,绑定状态,或是处于启动并且绑定状态,任何应用组件均可以像使用 Activity 那样通过调用 Intent 来使用服务(即使服务来自另一个应用)。不过,你可以通过清单文件声明服务为私有服务,阻止其他应用访问。</p> <p>注意:服务在其托管进程的主线程中运行,它不创建自己的线程,也不在单独的进程中运行(除非另行指定)。这意味着,如果服务将执行任何CPU密集型工作或者阻止性操作(例如 MP3 播放或联网),则应在服务内创建新的线程来完成这项工作,通过使用单独的线程,可以降低发生ANR错误的风险,而应用的主线程仍可以继续专注于运行用户与 Activity 之间的交互。</p> </li>    </ol>    <h3>认识 Service</h3>    <p>要创建服务,必须创建 Service 的子类(或者使用它的一个现有子类)。需要重写一些回调方法,以处理服务生命周期的有些关键方面,并提供一种机制将组件绑定到服务应重写的最重要的回调方法包括:</p>    <ul>     <li> <p>onStartCommand()</p> <pre>  <code class="language-java">当另一个组件(如 Activity )通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务会启动并可在后台无限期运行。如果你实现了此方法,在服务工作完成后,需要调用 stopSelf() 或 stopService() 来停止服务(如果只是提供绑定则无需实现此方法)</code></pre> </li>     <li> <p>onBind()</p> <pre>  <code class="language-java">当另一个组件调用 bindService() 与服务绑定时,系统将调用此方法。在此方法中必须返回 IBinder 提供一个接口,供客户端与服务器进行通信。如果不希望允许绑定,则可以返回 null 。</code></pre> </li>     <li> <p>onCreate()</p> <pre>  <code class="language-java">首次创建服务时,系统调用次方法来执行一次性程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已经运行则不会调用此方法。</code></pre> </li>     <li> <p>onDestory()</p> <pre>  <code class="language-java">当服务不再使用且将被销毁是,系统调用此方法。服务应该实现方法来清理所有资源,如线程、注册的监听器,接收器等。</code></pre> </li>    </ul>    <p>如果组件调用 startService()启动服务(会导致对 onStartCommand() 的调用),则服务将一直运行,知道服务使用 stopSelf() 自行停止运行或者其他组件调用 stopService() 停止它为止。</p>    <p>如果组件调用 bindService() 来创建服务(且未调用 onStartCommand() ),则服务只会在该组件与其绑定时运行,一旦服务与所有客户端全部取消绑定时,系统会销毁它。</p>    <p>仅当内存过低且系统必须回收资源供具有用户焦点的 Activity 使用时,Android 系统才会强制停止服务。如果将服务绑定到具有用户焦点的 Activity ,它被系统终止的可能性不大;如果将服务声明为在前台运行,则它几乎永远不会终止。如果服务已经启动且要长时间运行,则系统会随着时间推移降低服务在后台列表中的位置,服务也将变得容易被终止;如果服务是启动服务,则必须将其设计为能够妥善处理系统对它的重启。如果系统终止服务,那么一旦资源变得再次可用,系统便会重启服务(取决于从 onStartCommand() )返回的值。</p>    <h3>使用清单声明服务</h3>    <p>如同 Activity (或是其他组件)一样,必须在应用的清单文件中声明所有服务。</p>    <p>要声明服务,清添加 元素作为 元素的子元素。例如</p>    <pre>  <code class="language-java"><manifest ... >    ...    <application ... >        <service android:name=".DemoService" />        ...    </application>  </manifest></code></pre>    <p>元素还可以包含其他属性,定义一些特性,如启动服务及运行所需要的权限。其中 android:name 属性是唯一必须的属性,用于指定服务的类名。应用一经发布,不可更改类名,不然会因依赖显示 Intent 启动或绑定服务而导致破坏代码的风险。</p>    <p>为了确保应用的安全性,请使用显示 Intent 或 绑定 Service , 而且不要为服务声明 Intent 过滤器。启动哪个服务存在不确定性,对这种不确定性的考量非常有必要,可以为服务提供 Intent 过滤器并从 Intent 中排除想应的组件名称,但必须使用 setPackage() 方法设置 Intent 的软件包, 这样做可以消除目标服务的不确定性。</p>    <p>此外还可以通过添加 android:exporeted = "false" ,确保服务为应用私有,可以阻止其他应用启动你的服务,同理,使用显示 Intent 时也是如此。</p>    <h3>启动服务的创建</h3>    <p>启动服务由另一个组件通过调用 startService() 启动,服务的 onStartCommand() 方法也将被调用。</p>    <p>服务启动之后,其生命周期完全独立,且可以在后台无限期地运行,即使启动服务的组件被销毁了,该服务也不受影响。如果要结束该服务,可以调用 stopSelf() 自行停止运行,或者由另一个组件调用 stopService() 来停止。</p>    <p>应用组件(如 Activity )可以通过调用 startService() 方法且传递一个 Intent 对象(指定服务和其所有数据)来启动服务。服务通过 onStartCommand() 方法来接收 Intent 对象。</p>    <p>例如,某个 Activity 需要保存一些数据到线上的数据库中,这时它可以启用一个协同服务,调用 startService() 并传递一个 Intent ,提供需要保存的数据。服务通过 onStartCommand() 接收 Intent ,连接到互联网并执行数据库事务,事务完成后,服务将自行停止运行且随即被销毁。</p>    <p>注意: 默认情况下,服务与服务声明所在的应用处于同一进程,而且运行在主线程中。因此,如果是执行一些耗时操作,需要在服务内启动新的线程,避免影响应用的性能。</p>    <p>你可以通过扩展两个类来创建启动服务:</p>    <p>Service</p>    <pre>  <code class="language-java">这是适用于所有服务的基类。默认情况下该服务将在应用的主线程中运行,你需要创建一个新的线程供服务工作,避免影响正在运行的所有 Activity 的性能。</code></pre>    <p>IntentService</p>    <p>这个是 Service 的子类,它适用工作线程逐一处理所有启动请求。如果不要求服务同时处理 请求,这毫无疑问是最好的选择。只需要实现 onHandleIntent() 方法即可。该方法会接收每个启动请求的 Intent ,使你能够执行后台工作。</p>    <p>下面演示如何使用其中任意一个类来实现服务。</p>    <ol>     <li> <p>扩展 IntentService 类</p> <p>由于大多数启动服务不用同时处理多个请求(这种多线程情况可能很危险),因此选择 IntentService 类实现服务无疑是最好的。</p> <p>IntentServic 执行以下的操作:</p>      <ul>       <li>创建默认的工作线程,在主线程外执行传递给 onStartConmmand() 的所有 Intent。</li>       <li>创建工作队列,将 Intent 逐一传递给 onHandleIntent() 实现,不用担心多线程问题。</li>       <li>处理所有启动请求后停止服务(不用手动调用 stopSelf() 来结束服务)</li>       <li>提供 onBind() 的默认实现(返回 null )。</li>       <li>提供 onStartCommand() 的默认实现,可以将 Intent 依次发送到工作队列 和 onHandleIntent() 实现。</li>      </ul> <p>综上所述,只需要实现 onHandleIntent() 来完成客户端提供的工作即可。(需要为服务提供构造函数)</p> <p>下面是 IntentService 的实现示例:</p> <pre>  <code class="language-java">public class DemoIntentService extends IntentService {       private static final String TAG = "DemoIntentService";       /**        * Creates an IntentService.  Invoked by your subclass's constructor.        *        * @param name Used to name the worker thread, important only for debugging.        */       public DemoIntentService(String name) {           super(name);       }         @Override       protected void onHandleIntent(@Nullable Intent intent) {           //模拟耗时操作,线程沉睡3秒           try {               Thread.sleep(3000);           } catch (InterruptedException e) {               Thread.currentThread().interrupt();             }       }         @Override       public int onStartCommand(@Nullable Intent intent, int flags, int startId) {           Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();           Log.i(TAG, "service starting");           return super.onStartCommand(intent, flags, startId);       }   }</code></pre> <p>只需要一个构造函数和一个 onHandleIntent() 实现即可。</p> <p>如果重写其他回调方法(如 onCreate() 、onStartCommand() 或 onDestroy),要确保调用超类实现,让 IntentService 能够妥善处理工作线程的生命周期。</p> <p>例如, onStartCommand() 必须返回默认实现(将 Intent 传递给 onHandleIntent() ):</p> <pre>  <code class="language-java">@Override   public int onStartCommand(@Nullable Intent intent, int flags, int startId) {       Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();       Log.i(TAG, "service starting");       return super.onStartCommand(intent, flags, startId);   }</code></pre> <p>除了 onHandleIntent() 之外 ,无需调用的方法就是 onBind() (仅当服务允许绑定时,才需要实现该方法)</p> </li>     <li> <p>扩展服务类</p> <p>如上部分所述,使用 IntentService 简化了启动服务的实现,如果要服务执行多线程(不是通过工作队列处理启动请求),则可以扩展 Service 类来处理每个 Intent 。</p> <p>以下是 Service 类实现代码示例,该类执行的工作与上面的 IntentService 示例相同。对每个启动请求,它都使用工作线程执行作业,且每次仅处理一个请求。</p> <pre>  <code class="language-java">public class DemoService extends Service {   private Looper mServiceLooper;   private ServiceHandle mServiceHandle;     @Override   public void onCreate() {       //启动运行该服务的线程       HandlerThread thread = new HandlerThread("ServiceStartArguments", Process               .THREAD_PRIORITY_BACKGROUND);       thread.start();         //获取HandlerThread的Looper并将其用于自己的Handler       mServiceLooper = thread.getLooper();       mServiceHandle = new ServiceHandle(mServiceLooper);     }     @Override   public int onStartCommand(Intent intent, int flags, int startId) {       Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();         //每一个启动请求,发送一个消息来启动一个工作并提交开始Id       Message msg = mServiceHandle.obtainMessage();       msg.arg1 = startId;       mServiceHandle.sendMessage(msg);         return START_STICKY;   }     @Nullable   @Override   public IBinder onBind(Intent intent) {       return null;   }     @Override   public void onDestroy() {       Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();   }     //从线程接收消息的处理程序   private final class ServiceHandle extends Handler {       public ServiceHandle(Looper looper) {           super(looper);       }         @Override       public void handleMessage(Message msg) {           //模拟耗时操作,线程沉睡3秒           try {               Thread.sleep(3000);           } catch (InterruptedException e) {               Thread.currentThread().interrupt();           }             stopSelf(msg.arg1);       }   }  }</code></pre> <p>如上面的示例,与使用 IntentService 相比,这需显得复杂一些。</p> <p>但是,自己处理 onStartCommand() 的每个调用,因此可以同时执行多个请求。上面的示例没有实现。如果你有需要,可以为每个请求创建一个新线程,然后立即运行这些线程(不是等待上一个请求完成)。</p> <p>注意: onStartCommand() 方法必须返回整型数。 该返回值用于描述系统改如何在服务终止的情况下继续运行服务,从 onStartCommand() 返回的值必须是以下常量注意之一:</p>      <ul>       <li> <p>START_NOT_STICKY</p> <p>如果系统在 onStartCommand() 返回后终止服务,除非有挂起的 Intent 要传递,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时一级应用能够轻松启动所有未完成的作业时运行服务。</p> </li>       <li> <p>START_STICKY</p> <p>如果系统在 onStartCommand() 返回后终止服务,则会重建服务并调用 onStartCommand(),但不会重新传递最后一个 Intent 。相反,除非有挂起 Intent 要启动服务(在这种情况下,传递这些 Intent ),否则系统会通过空 Intent 调用 onStartCommand() ,这个返回值在适用于不执行命令,但无限期运行并等待作业的媒体播放器(或类似服务)。</p> </li>       <li> <p>START_REDELIVER_INTENT</p> <p>如果系统在 onStartCommand() 返回后终止服务,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand() 。任何挂起 Intent 均依次传递。适用于主动执行应该立即恢复的作业(如下载文件)的服务。</p> </li>      </ul> </li>     <li> <p>启动服务</p> <p>可以通过将 Intent (指定要启动的服务)传递给 startService,从 Activity 或其他应用组件启动服务。 Android 系统调用服务的 onStartCommand() 方法,并向其传递 Intent 。(请勿直接调用 onStartCommand() )</p> <p>例如,Activity 可以结合使用显式 Intent 与 startService(),启动上文中的示例服务( DemoService ):</p> <pre>  <code class="language-java">Intent intent = new Intent(this,DemoService.class);  startService(intent);</code></pre> <p>startService() 方法将立即返回,且 Android 系统调用服务的 onStartCommand() 方法。如果服务尚未运行,则系统会先调用 onCreate() ,然后调用 onStartCommand() 。</p> <p>如果服务未提供绑定,则使用 startService() 传递的 Intent 是应用组件与服务之间唯一的通信模式。但是,如果你希望服务返回结果,则启动服务的客户端可以为广播创建一个 PendingIntent(使用 getBroadcast()),并通过启动服务的 Intent 传递给服务,然后服务可以通过广播来传递结果。</p> <p>多个服务启动请求会导致多次对服务的 onStartCommand() 进行相应的调用。但是要停止服务,只需要一个服务停止请求(使用 stopSelf() 或 stopService() )即可。</p> </li>     <li> <p>停止服务</p> <p>启动服务必须管理自己的生命周期。也就是说,除非系统必须回收内存资源,否则系统不会停止或销毁服务,而且服务会在 onStartCommand() 返回后继续运行。因此,服务必须通过调用 stopSelf() 自行停止运行,或者其他组件调用 stopService() 来停止。</p> <p>一旦请求使用 stopSelf() 或者 stopService() 停止服务,系统就会尽快销毁服务。</p> <p>但是,如果服务同时处理多个 onStartCommand() 请求,则不应该在处理第一个启动请求后停止服务,有可能你已经接收新的启动请求(第一个请求结束时停止服务会终止第二个请求)。为了避免这个问题,可以使用 stopSelf( int ) 确保服务停止于最近的启动请求。也就是,在调用 stopSelf( int ) 时,传递与停止请求的 ID 对应的启动请求 ID (传递给 onStartCommand() 的 startId )。然后,在调用 stopSelf( int ) 之前服务收到了新的请求,ID 不匹配,服务也就不会停止。</p> <p>注意: 为了避免浪费系统资源和小号电池电量,应用必须在工作完成后停止其服务。如有必要,其他组件可以通过调用 stopService 来停止服务,即使为服务启用了绑定,一旦服务受到对 onStartCommand 的调用, 始终需要亲自停止服务。</p> </li>    </ol>    <h3>创建绑定服务</h3>    <p>绑定服务允许应用通过调用 bindService() 与其绑定,以便创建长期连接(通常不允许组件通过调用 startService() 来启动它)。</p>    <p>如需与 Activity 和其他应用组件中的服务进行交互,或者需要跨进程通信,则应创建绑定服务。</p>    <p>创建绑定服务,必须实现 onBind() 回调方法以返回 IBinder ,用于定义与服务通信的接口。然后其他应用组件可以调用 bindService() 来检索该接口,并开始对服务调用方法。服务只用于与其绑定的应用组件,因此如果没有组件绑定到服务,则系统会销毁服务(不必通过 onStartCommand() 启动的服务来停止绑定服务)。</p>    <p>要创建绑定服务,必须定义与指定客户端与服务通信的接口。服务与客户端之间的这个接口必须是 IBinder 的实现,且服务必须从 onBind() 回调方法返回它。一旦客户端收到 IBinder ,即可开始通过该接口与服务器进行交互。</p>    <p>多个客户端可以同时绑定到服务,客户端完成与服务的交互后,会调用 unbindService() 取消绑定。一旦没有客户端绑定到该服务,系统就会销毁它。</p>    <p>有多种方法实现绑定服务,实现方式比启动服务更复杂,这里就不详说了,后续会单独的说明。</p>    <h3>向用户发送通知</h3>    <p>服务一旦运行起来,即可使用 Toast 通知或状态栏通知来通知用户所发生的事情。</p>    <p>Toast 通知是指出现在当前窗口的表面、片刻随即消失不见的消息,而状态通知栏则在状态栏中随消息一起提供图标,用户可以选择图标来采取操作(例如启动 Activity )。</p>    <p>通常,当某些后台工作完成(录入文件下载完成)且用户现在可以对其进行操作时,状态栏通知是最佳方法。当用户从展开视图中选定通知时,通知即可启动 Activity (例如查看下载的文件)。</p>    <h3>在前台运行服务</h3>    <p>前台服务被认为是用户主动意识到的一种服务,在内存不足时,系统也不会考虑将其终止。前台服务必须为状态栏提供通知,放在 “正在进行” 标题下方,除非服务停止或者从前台溢出,否则不会清除通知。</p>    <p>例如,通过服务播放音乐的音乐播放器设置为在前台运行,用户能明确意识到其操作。状态栏中的通知可能表示正在播放的歌曲,并允许用户启动 Activity 来与音乐播放器进行交互。</p>    <p>要请求让服务运行于前台,可以调用 startForeground() 。此方法采用两个参数:唯一标识通知的整型数和通知栏的 Notification 。例如:</p>    <pre>  <code class="language-java">Notification.Builder builder = new Notification.Builder(this);  builder.setContentTitle("Notification title")          .setContentText("Notification describe")          .setSmallIcon(R.mipmap.ic_launcher);    Intent notificationIntent = new Intent(this,DemoActivity.class);  PendingIntent pendingIntent = PendingIntent.getActivity(this,0,notificationIntent,0);    builder.setContentIntent(pendingIntent);  Notification notification = builder.build();  startForeground(NOTIFICATION_ID,notification);</code></pre>    <p>注意:提供给 startForegrond() 的整型 ID 不可以为0。</p>    <p>要从前台移除服务,需要调用 stopForeground() 。次方法采用一个布尔值,指示是否移除状态栏通知,此方法不会停止服务。但是,如果你的服务正在前台运行时将其停止,则通知也会被移除。</p>    <h3>管理服务生命周期</h3>    <p>服务的生命周期比 Activity 的生周期要简单多。但是密切关注如何创建和销毁服务反而更重要,因为服务可以在用户没有意识到的情况下运行于后台。</p>    <p>服务生命周期可以(从创建到销毁)可以遵循两条不同的路径:</p>    <ul>     <li> <p>启动服务</p> <p>该服务在其他组件调用 startService() 时创建,然后无限期运行,且必须通过调用 stopSelf() 来自行停止运行。此外,其他组件也可以通过调用 stopService 来停止服务。服务停止后,系统将其销毁。</p> </li>     <li> <p>绑定服务</p> <p>该服务在另一个组件(客户端)调用 bindService() 时创建,然后客户端通过 IBinder 接口与服务进行通信。客户端可以调用 unbindService() 关闭连接。多个客户端可以绑定到相同的服务,而且当所有绑定取消后,系统会销毁该服务(服务不必自行停止)。</p> </li>    </ul>    <p>这两条路径并非完全独立,也就是说,你可以绑定到已经使用 startService() 启动的服务。例如,通过使用 Intent (标识要播放的音乐)调用 startService() 来启动后台音乐服务。随后可能在用户需要稍加控制播放器或获取有关当前播放歌曲的信息时, Activity 可以通过调用 bindService() 绑定到服务,在这种情况下,除非所有客户端取消绑定,否则 stopService() 或 stopSelf() 不会实际停止服务。</p>    <p>实现生命周期回调</p>    <p>与 Activity 类似,服务也拥有生命周期回调方法,你可以实现这些方法来监控服务状态的变化,并适时执行工作。下面的例子展示了每种生命周期方法:</p>    <pre>  <code class="language-java">public class TestService extends Service {        int mStartMode;       // 指示如果服务被杀死,该如何操作      IBinder mBinder;      // 客户端绑定接口      boolean mAllowRebind; // 指示是否应使用onRebind        @Override      public void onCreate() {          //服务正在创建中      }        @Override      public int onStartCommand(Intent intent, int flags, int startId) {          //调用了startService(),服务正在启动          return mStartMode;      }        @Nullable      @Override      public IBinder onBind(Intent intent) {          //客户端绑定到具有bindService()的服务          return mBinder;      }        @Override      public boolean onUnbind(Intent intent) {          //所有客户端都使用unbindService()取消绑定          return mAllowRebind;      }        @Override      public void onRebind(Intent intent) {          //客户端绑定到具有bindService()的服务,          //onUnbind()已经被调用      }        @Override      public void onDestroy() {          //该服务已不再使用并被销毁      }  }</code></pre>    <p>注:与 Activity 生命周期回调方法不同,你不需要调用这些方法的超类实现。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a9b4015e178a07bc5ef1334f89bc3e67.png"></p>    <p>Service生命周期图</p>    <p>左图显示了使用 startService() 所创建的服务的生命周期,右图显示了使用 bindService() 所创建的服务的生命周期。</p>    <p>通过实现这些方法,你可以监控这两种服务生命周期:</p>    <ul>     <li> <p>服务的整个生命周期从调用 onCreate() 开始,到 onDestroy() 返回时结束。与 Activity 类似,服务也在 onCreate() 中完成初始设置,在 onDestroy() 中释放所有剩余资源。例如音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后可以在 onDestroy() 中停止该线程。无论服务是通过 startService() 还是 bindService() 创建,都会为所有服务调用 onCreate() 和 onDestroy() 方法。</p> </li>     <li> <p>服务的有效生命周期从调用 onStartCommand() 或 onBind() 方法开始。每种方法均有 Intent 对象,该对象分别传递到 startService() 或 bindService() 。</p> <p>对于启动服务,有效生命周期与整个生命周期同时结束(即便是在 onStartCommand() 返回之后,服务仍然处于活动状态)。对于绑定服务,有效生命周期在 onUnbind() 返回时结束。</p> </li>    </ul>    <p>注:尽管启动服务是通过 stopSelf() 或 stopService() 来停止,但是该服务并无相应的回调(没有 onStop 回调)。因此,除非服务绑定到客户端,否则在服务停止时,系统会将其销毁而 onDestroy() 是接收到的唯一回调。</p>    <p>尽管该图分开介绍通过 startService() 创建的服务和通过 bindService() 创建的服务,但是记住一点,不管启动方式如何,任何服务均有可能允许客户端与其绑定。因此,最初使用 onStartCommand()(通过客户端调用 startService())启动的服务仍可接收对 onBind() 的调用(当客户端调用 bindService() 时)。</p>    <p> </p>    <p>来自:https://juejin.im/post/58efa9da8d6d8100646bdbce</p>    <p> </p>    
 本文由用户 bmri1550 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1492136571654.html
安卓开发 Android开发 移动开发