zoukankan      html  css  js  c++  java
  • Android学习之基础知识十三 — 四大组件之服务详解第一讲

    一、服务是什么

      服务(Service)是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要求长期运行的任务。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。

      不过需要注意的是,服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。

      另外,也不要被服务的后台概念所迷惑,实际上服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。所以接下来我们先来学习一下关于Android多线程编程的知识。

    二、Android多线程编程

      如果熟悉Java,那么对多线程编程就一定不会陌生。当我们需要执行一些耗时操作,比如说发起一条网络请求时,考虑到网速等其他原因,服务器未必会立刻响应我们的请求,如果不将这类操作放在子线程里去运行,就会导致主线程被阻塞,从而影响用户对软件的正常使用。那么我们就从线程的基本用法开始学习吧。

    2.1、线程的基本用法

      Android多线程编程其实并不比Java多线程编程特殊,基本都是使用相同的语法。比如说,定义一个线程只需要创建一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时的逻辑即可,如下所示:

      那么该如何启动这个线程呢?其实很简单,只需要new出MyThread的实例,然后调用它的start()方法,这样run()方法中的代码就会在子线程当中运行了,如下所示:

    当然,使用继承的方式耦合性有点高,更多的时候我们都会选择使用实现Runnable接口的方式来定义一个线程,如下所示:

    如果使用了这种写法,启动线程的方法也需要进行相应的改变,如下所示:

      可以看到,Thread的构造函数接收一个Runnable参数,而我们new出的MyThread正是一个实现了Runnable接口的对象,所以可以直接将它传入到Thread的构造函数里,接着调用Thread的start()方法,run()方法中的代码就会在子线程当中运行了。

      当然,如果不想专门再定义一个类去实现Runnable接口,也可以使用匿名类的方式,这种写法更为常见,如下所示:

      以上几种线程的使用方式相信都不陌生,因为在Java中创建和启动线程也是使用同样的方式,了解了线程的基本用法后,我们来看一下Android多线程编程与Java多线程编程不同的地方。

    2.2、在子线程中更新UI

      和许多其他的GUI库一样,Android的UI也是线程不安全的,也就是说,如果想要更新应用程序里的UI元素,则必须在主线程中进行,否则就会出现异常。下面我们通过一个具体的例子来验证一下:

    第一步:新建一个AndroidThreadTest项目,在布局文件中添加一个Button按钮和一个TextView,Button用于改变TextView中显示的内容:

    第二步:修改MainActivity中的代码:

     

      可以看到,我们在Change Text按钮的点击事件里面开启了一个子线程,然后在子线程中调用TextView的setText()方法将显示的字符串改成:"Nice to meet you"。代码的逻辑非常的简单,只不过我们是在子线程中更新UI的。

    第三步:现在运行一下程序,并点击Change Text按钮,你会发现程序果然崩溃了。

       

     第四步:查看logcat中的错误信息,可以看出是由于在子线程中更新UI所导致的。

      由此证实了Android确实是不允许在子线程中进行UI操作的,但是有些时候,我们必须在子线程里去执行一些耗时任务,然后根据任务的执行结果来更新相应的UI控件,这该任何是好呢?

      对于这种情况,Android提供了一套异步消息处理机制,完美的解决了在子线程中进行UI操作的问题。下面我们先来学习一下异步消息处理的使用方法,后面再分析它的原理。

    第一步:修改MainActivity中的代码:

    代码分析:

      这里我们先定义了一个整型常量:UPDATE_TEXT,用于表示更新TextView这个动作,然后新增一个Handler对象,并重写父类的handleMessage()方法,在这里对具体的Message进行处理,如果发现Message的what字段的值等于:UPDATE_TEXT,就将TextView显示的内容改成:Nice to meet you。

      下面来看一下Change Text按钮的点击事件中的代码,可以看到,这次我们并没有在子线程里直接进行UI操作,而是创建了一个Message(android.os.Message)对象,并将它的what字段的值指定为:UPDATE_TEXT,然后调用Handler的:sendMessage()方法将这条Message发送出去,很快,Handler就会收到这条Message,并在handleMessage()方法中对它进行处理,注意此时handleMessage()方法中的代码就是在主线程当中运行的了,所以我们可以放心地在这里进行UI操作,接下来对Message携带的what字段的值进行判断,如果等于:UPDATE_TEXT,就将TextView显示的内容改成:Nice to meet you。

    第二步:运行程序,点击按钮,可以看到TextView的内容被替换了。

      

      到此为止,就已经掌握了Android异步消息处理的基本用法,使用这种机制就可以出色的解决掉在子线程中更新UI的问题,但是它的工作原理是这样的呢?下面就来介绍一下Android异步消息处理机制到底是如何工作的。

    2.3、解析异步消息处理机制

       Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue、Looper。其中Message和Handler在上面我们已经接触过了,而MessageQueue和Looper还是全新的概念,下面就对这4个部分进行一个简单的介绍:

      1、Message

        Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。在上面我们使用到了Message的what字段,除此之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。

      2、Handler

        Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的:handleMessage()方法中。

      3、MessageQueue

        MessageQueue是消息队列的意思,它主要是用于存放所有通过Handler发送的消息。这部分消息会一直存在消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。

      4、Looper

        Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的:handleMessage()方法中。每个线程中也只会有一个Looper对象。

      了解了Message、Handler、MessageQueue以及Looper的基本概念后,我们再来把异步消息处理的整个流程梳理一遍。首先需要在主线程当中创建一个Handler对象,并重写:handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去,之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage()方法中。由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心的进行UI操作了;整个异步消息处理机制的流程示意图如下:

      一条Message经过这样一个流程的辗转调用后,也就从子线程进入到了主线程,从不能更新UI变成了可以更新UI,整个异步消息处理的核心思想也就是如此。

      而我们在前面使用到的:runOnUiThread()方法其实就是一个异步消息处理机制的接口封装,它虽然表面上看起来用法更为简单,但其实背后的实现原理和上面示意图的描述是一模一样的。

    2.4、使用AsyncTask

       不过为了更加方便我们在子线程中对UI进行操作,Android还提供了另外一些好用的工具,比如:AsyncTask。借助AsyncTask,即使你对异步消息处理机制完全不了解,也可以十分简单地从子线程切换到主线程。当然,AsyncTask背后的实现原理也是基于异步消息处理机制的,只是Android帮我们做好了很好的封装而已。

      首先来看一下AsyncTask的基本用法,由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它,在继承时我们可以为AsyncTask类指定3个泛型参数,这3个泛型参数的用途如下:

      1、Params:在执行AsyncTask时需要传入的参数,可用于在后台任务中使用

      2、Progress:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

      3、Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

      因此,一个最简单的自定义AsyncTask就可以写成如下方式:

      这里我们把AsyncTask的第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。

      当然,目前我们自定义的DownloadTask还是一个空任务,并不能进行任何实际的操作,我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下4个:

      1、onPreExecute()

        这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

      2、doInBackground(Params...)

        这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定为Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用:publishProgress(Progress)方法来完成。

      3、onProgressUpdate(Progress...)

        当在后台任务中调用了:publishProgress(Progress...)方法后,onProgressUpdate(Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的,在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。

      4、onPostExecute(Result)

        当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,比如说提醒任务执行的结果,以及关闭掉进度条对话剧等。

      因此,一个比较完整的自定义AsyncTask就可以写成如下方式:

    代码分析:

      在这个DownloadTask中,我们在doInBackground()方法里去执行具体的下载任务,这个方法里的代码都是在子线程中运行的,因而不会影响到主线程的运行。注意,这里虚构了一个doDownload()方法,这个方法用于计算当前的下载进度并返回,我们假设这个方法已经存在了。在得到了当前的下载进度后,下面就该考虑如何把它显示到界面上了,由于doInBackground()方法是在子线程中运行的,在这里肯定不能进行UI操纵,所以我们可以调用publishProgress()方法并将当前的下载进度传进来,这样onProgressUpdate()方法就会很快被调用,在这里就可以进行UI操作了。

      当下载完成后,doInBackground()方法会返回一个布尔型变量,这样onPostExecute()方法就会很快被调用,这个方法也是在主线程中运行的。然后在这里我们会根据下载的结果来弹出相应的Toast提示,从而完成整个DownloadTask任务。

      简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。

      如果想要启动这个任务,只需编写以下代码即可:

      以上就是AsyncTask的基本用法,是不是感觉简单方便了许多?我们并不需要去考虑什么异步消息处理机制,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress()方法,就可以轻松的从子线程切换到UI线程了。

    三、服务的基本用法

       了解了Android多线程编程的技术之后,下面就进入到本章的正题,开始对服务的相关内容进行学习。作为Android四大组件之一,服务也少不了有很多非常重要的知识点。

    3.1、定义一个服务

     首先看一下如何在项目中定义一个服务:

    第一步:新建一个ServiceTest项目,然后New一个Service如下:

      可以看到,在弹出的窗口中,我们将服务命名为:MyService,Exported属性表示是否允许当前程序之外的其他程序访问这个服务,Enable属性表示是否启用这个服务,将这两个属性都勾中,点击Finish完成创建。

    第二步:观察MyService中的代码

      可以看到,MyService是继承自Service类的,说明这是一个服务。目前MyService中只有一个onBind()方法特别醒目,这个方法是Service中唯一的一个抽象方法,所以必须要在子类里实现。我们会在后面的小节中使用到onBind()方法,目前可以暂时将它忽略掉。

    第三步:既然定义了一个服务,自然就应该在服务中去处理一些事情,那处理事情的逻辑应该写在哪里呢?这时就可以重写Service中的另外一些方法了,如下所示:

      可以看到,这里我们又重写了onCreate()、onStartCommand()、onDestroy()这3个方法,它们是每个服务中最常用到的3个方法了,其中onCreate()方法会在服务创建的时候调用,onStartCommand()方法会在每次服务启动的时候调用,onDestroy()方法会在服务销毁的时候调用。

      通常情况下,如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法里面,而当服务销毁时,我们又应该在onDestroy()方法中去回收那些不再使用的资源。

    第四步:需要注意的是,每一个服务都需要在AndroidManifest.xml文件中进行注册才能生效,这是Android四大组件共有的特点,不过智能的Android Studio已经帮我们注册好了,打开AndroidManifest.xml文件查看:

      这样的话,就已经将一个服务完全定义好了。

    3.2、启动和停止服务

      定义好了服务之后,接下来就是如何去启动和停止这个服务了。启动和停止的方法主要是借助Intent来实现的,下面就在ServiceTest项目中尝试去启动和停止MyService这个服务。

    第一步:在布局中设置两个按钮,用于打开和关闭服务:

    第二步:修改MainActivity中的代码:

    代码分析:

      可以看到,这里在onCreate()方法中分别获取到了Start Service按钮和Stop Service按钮的实例,并给它们注册了点击事件,然后在Start Service按钮的点击事件里,我们构建出了一个Intent对象,并调用startService()方法来启动MyService这个服务。在Stop Service按钮的点击事件里,我们同样构建出了一个Intent对象,并调用stopService()方法来停止MyService这个服务。startService()和stopService()方法都是定义在Context类中的,所以我们在活动里可以直接调用这两个方法。

      注意:这里完全是由活动来决定服务何时停止的,如果没有点击Stop Service按钮,服务就会一直处于运行状态。那服务有没有什么办法让自己停止下来呢?当然可以,只需要在MyService的任何一个位置调用stopSelf()方法就能让这个服务停止下来了。

    第三步:证实服务启动和停止:在MyService的几个方法中加入打印日志:

    第四步:运行程序,分别点击启动(上)和停止按钮(下):

      由此证明,MyService启动和停止成功了。

      刚刚在启动服务的时候,onCreate()方法和onStartCommand()方法都执行了,那么这两个方法有什么区别呢?其实onCreate()方法是在服务第一次创建的时候调用的,而onStartCommand()方法则在每次启动服务的时候都会调用,由于刚才我们是第一次点击Start Service按钮,服务此时还未创建过,所以这两个方法都会执行,之后如果再连续多点击几次Start Service按钮,就会发现只有onStartCommand()方法可以得到执行了。

    3.3、活动和服务进行通信

       上面我们学习了启动和停止服务的方法,虽然服务是在活动里启动的,但在启动了服务之后,活动与服务基本就没有什么关系了。我们在活动里调用了startService()方法来启动MyService这个服务,然后MyService的onCreate()和onStartCommand()方法就会得到执行。之后服务会一直处于运行状态,但具体运行的是什么逻辑,活动就控制不了了。这就类似于活动通知了服务一下:“你可以启动了”然后服务就去忙自己的事情了,但活动并不知道服务到底去做了什么事情,以及完成得如何。

      那么有没有什么办法让活动和服务的关系更加紧密一些呢?例如在活动中指挥服务去干了什么,服务就去干什么。这就需要借助我们刚刚忽略的onBind()方法了。

      比如说,目前我们希望在MyService里提供一个下载功能,然后在活动中可以决定何时开始下载,以及随时查看下载进度。实现这个功能的思路是创建一个专门的Binder对象来对下载功能进行管理,修改MyService中的代码:

      可以看到,这里我们新建了一个DownloadBinder类,并让它继承自Binder,然后在它的内部提供了开始下载以及查看下载进度的方法。当然这只是两个模拟方法,并没有实现真正的功能,我们在这两个方法中分别打印了一行日志。

      接着,在MyService中创建了DownloadBinder的实例,然后在onBind()方法里返回了这个实例,这样MyService中的工作就全部完成了。

      下面就要看一看,在活动中如何去调用服务里的这些方法了:

    第一步:首先需要在布局文件里新增两个按钮,用于绑定服务和取消绑定服务的。

    第二步:当一个活动和服务绑定了之后,就可以调用该服务里的Binder提供的方法了。修改MainActivity中的代码:

    代码分析:

      可以看到,这里我们首先创建了一个ServiceConnection的匿名类,在里面重写了onServiceConnected()方法和onServiceDisconnected()方法,这两个方法分别会在活动与服务成功绑定以及解除绑定的时候调用。在onServiceConnected()方法中,我们又通过向下转型得到了DownloadBinder的实例,有了这个实例,活动和服务之间的关系就变得非常紧密了。现在我们可以在活动中根据具体的场景来调用DownloadBinder中的任何public()方法,即实现了指挥服务干什么服务就去干什么的功能。这里仍然只是做了个简单的测试。在onServiceConnected()方法中调用了DownloadBinder的startDownload()和getProgress()方法。

      当然,现在活动和服务其实还没进行绑定,这个功能是在Bind Service按钮的点击事件了完成的。可以看到,这里我们仍然是构建出了一个Intent对象,然后调用bindService()方法将MainActivity和MyService进行绑定。bindService()方法接收3个参数,第一个参数就是刚刚构建出的Intent对象,第二个参数是前面创建出的ServiceConnection的实例,第三个参数则是一个标志位,这里传入:BIND_AUTO_CREATE表示在活动和服务进行绑定后自动创建服务。这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。

      然后如果我们想要解除活动和服务之间的绑定该怎么办呢?调用一下unbindService()方法就可以了,这也是Unbind Service按钮的点击事件里实现的功能。

    第三步:运行程序,分别点击绑定按钮(上)和解除绑定按钮(下),查看打印日志:

      需要注意的是,任何一个服务在这个应用程序范围内都是通用的,即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的活动进行绑定,而且在绑定完成后它们都可以获取到相同的DownloadBinder实例。

    四、服务的生命周期

       之前我们学习过了活动以及碎片的生命周期,类似的,服务也有自己的生命周期,前面我们使用到的onCreate()、onStartCommand()、onBind()、onDestroy()等方法都是在服务的生命周期内可能回调的方法。

      一旦在项目的任何位置调用了Context的startService()方法,相应的服务就会启动起来,并回调onStartCommand()方法。如果这个服务之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。服务启动了之后会一直保持运行状态,直到stopService()或stopSelf()方法被调用。注意,虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个服务都只会存在一个实例,所以不管你调用了多少次startService()方法,只需调用一次stopService()或stopSelf()方法,服务就会停止下来了。

      另外,还可以调用Context的bindService()方法来获取一个服务的持久连接,这时就会回调服务中的onBind()方法,类似的,如果这个服务之前没有创建过,onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到onBind()方法里返回的:IBinder对象的实例,这样就能自由的和服务进行通信了。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。

      当调用了startService()方法后,又去调用stopService()方法,这时服务中的onDestroy()方法就会执行,表示服务已经销毁了。类似的,当调用了bindService()方法后,又去调用unbindService()方法,onDestroy()方法也会执行,这两种情况都很好理解。但是需要注意的是:我们是完全有可能对一个服务既调用了startService()方法,又调用了bindService()方法的,这种情况下该如何才能让服务销毁掉呢?根据Android系统的机制,一个服务只要被启动或被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行。

      以上就是完整的服务的生命周期。

    五、服务的更多技巧

     5.1、使用前台服务

       服务几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是服务的系统优先级还是比较低的,当系统出现内存不足时,就有可能会回收掉正在后台运行的服务,如果你希望服务可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台服务。

      前台服务和普通服务最大的区别在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。当然有时候你也可能不仅仅是为了防止服务被回收掉才使用的前台服务的,有些项目由于特殊的需求会要求必须使用前台服务,比如说彩云天气这款天气预报应用,它的服务在后台更新天气数据的同时,还会在系统状态栏一直显示当前的天气信息。

      接下来我们就来看看如何才能创建一个前台服务吧:

    第一步:修改MyService中的代码

     代码分析:

      这里我们只是修改了onCreate()方法中的代码,相信这部分代码你会非常眼熟,这就是创建通知的方法。只不过这次在构建出Notification对象后并没有使用NotificationManager来将通知显示出来,而是调用了startForeground()方法。这个方法接收两个参数,第一个参数是通知的id,类似于notify()方法的第一个参数,第二个参数则是构建出的Notification对象。调用startForeground()方法后就会让MyService变成一个前台服务,并在系统状态栏显示出来。

    第二步:运行程序,点击Start Service或Bind Service按钮,MyService就会以前台服务的模式启动了,并且在系统状态栏会显示一个通知图标,下拉状态栏后可以看到该通知的详细内容:

      

    5.2、使用IntentService

       在一开始我们就已经知道,服务中的代码都是默认运行在主线程当中的,如果直接在服务里去处理一些耗时的逻辑,就很容易出现ANR(Application Not Responding)的情况。所以这个时候就需要用到Android多线程编程技术了,我们应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑。因此一个标准的服务就可以写成如下形式:

      但是,这种服务一旦启动之后,就会一直处于运行状态,必须调用stopService()或者stopSelf()方法才能让服务停止下来。所以如果想要实现让一个服务在执行完毕后自动停止的功能,就可以这样写:

      虽说这种写法并不复杂,但是总会有一些程序员忘记开启线程,或者忘记调用stopSelf()方法,为了可以简单地创建一个异步的、会自动停止的服务,Android专门提供了一个IntentService类,这个类就很好的解决了前面所提到的两种尴尬,下面我们就来看一下它的用法:

    第一步:新建一个MyIntentService类继承自IntentService

    代码分析:

      这里首先要提供一个无参的构造函数,并且必须在其内部调用父类的有参构造函数,然后要在子类中去实现onHandleIntent()这个抽象方法,在这个方法中可以去处理一些具体的逻辑,而不用担心ANR的问题,因为这个方法已经是在子线程中运行的了。这里为了证实一下,在onHandleIntent()方法中打印了当前线程的id,另外根据IntentService的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了onDestroy()方法,在这里也打印了一行日志,以证实服务是不是停止掉了。

    第二步:修改activity_main.xml中的代码,加入一个用于启动MyIntentService这个服务的按钮,如下所示:

    第三步:修改MainActivity中的代码

      可以看到,在Start IntentService按钮的点击事件里面去启动MyIntentService这个服务,并在这里打印了一下主线程的id,稍后用于和IntentService进行比对。你会发现,其实IntentService的用法和普通的服务没什么两样。

    第四步:服务都需要在AndroidManifest.xml文件中注册:

    第五步:重新运行程序,点击Start IntentService按钮后,查看打印的日志

      从日志中我们看到,不仅MyIntentService和MainActivity所在的线程id不一样,而且onDestroy()方法也得到了执行,说明MyIntentService在运行完毕后确实自动停止了。集开启线程和自动停止于一身,IntentService还是博得了不少程序员的喜爱。

  • 相关阅读:
    poj 1743 Musical Theme 后缀数组
    poj 1743 Musical Theme 后缀数组
    cf 432D Prefixes and Suffixes kmp
    cf 432D Prefixes and Suffixes kmp
    hdu Data Structure? 线段树
    关于position和anchorPoint之间的关系
    ios POST 信息
    CALayers的代码示例
    CALayers详解
    ios中得sqlite使用基础
  • 原文地址:https://www.cnblogs.com/hh8888-log/p/10300488.html
Copyright © 2011-2022 走看看