后臺任務隊列管理神器 Android-Priority-Job-Queue

有人說“Android的開發,玩的就是多線程”。從某個角度來說的確如此,現在的App被設計的越來越復雜,相信很多開發人員都因大量而又復雜的后臺任務(background work)而焦頭爛額:Async-Task和Activity的生命周期太過于耦合,雖然實現簡單但是對于重要的后臺任務還是不靠譜;Loaders雖然可以用于異步從磁盤列讀取數據,但是對于異步的網絡請求就無能為力了;相對給力點的方案是后臺服務中開辟進程池(Thread Pool),使用ThreadPoolExecutor來幫助管理線程,但是app越復雜后臺操作越多,需要處理的多線程的問題越多,想一想就頭大.....
但是各位讀者不要沮喪,今天就是向大家介紹一個后臺任務隊列管理庫Android-Priority-Job-Queue,它將提供一個優雅的架構來解決以上所有的問題!

1. 簡介

用官方的話來說,Android-Priority-Job-Queue是一款專門為Android平臺編寫的,實現了Job Queue的后臺任務隊列類庫,能夠輕松的在后臺執行定時任務,并且提高了用戶體驗和應用的穩定性。其設計理念以靈活性和功能性為主,并且一直在更新。

“Priority Job Queue is an implementation of a Job Queue specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability.”
It is written primarily with flexibility & functionality in mind. This is an ongoing project, which we will continue to add stability and performance improvements.

github : https://github.com/yigit/android-priority-jobqueue
在這里可以了解到更多更全面的介紹。

其使用框架也很簡便直接:

  1. 構造一個任務管理器JobManager,為我們管理任務;
  2. 自定義Job類,來作為任務的載體;
  3. 在需要時,將自定義的Job類實例加入到JobManager中;

這樣就OK了,JobManager會根據優先級、持久性、負載平衡、延遲,網絡控制、分組等因素來管理任務的執行。由于是獨立于各個Activity,JobManager為Job的執行提供了一個很好的生命周期,用戶體驗更為棒。是不是很驚喜!閑話少敘,我們來看使用范例吧!

2. 使用實例

2.1 添加方法

在Android Studio添加,如下引用:

dependencies {
    compile 'com.birbit:android-priority-jobqueue:2.0.1'
}

如果你很懷舊,還在堅持使用Eclipse,那就在maven這樣配置:

<dependency>
    <groupId>com.birbit</groupId>
    <artifactId>android-priority-jobqueue</artifactId>
    <version>2.0.1</version>
</dependency>

2.2 配置JobManager

JobManager是整個框架的核心。作為一個重型的對象,建議Application只構建一個JobManager實例供全局使用。另一方面,為了讓任務的執行有一個更好的生命周期,建議將JobManager放在Application類,而不是一個具體的Activity。以下是示例代碼:

import android.app.Application;
import android.util.Log;
import com.path.android.jobqueue.JobManager;
import com.path.android.jobqueue.config.Configuration;
import com.path.android.jobqueue.log.CustomLogger;

public class JobQueueApplication extends Application {
    private JobManager jobManager;
    private static JobQueueApplication instance;
    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;//1. Application的實例
        configureJobManager();//2. 配置JobMananger
    }

   //私有構造器
   private JobQueueApplication(){
        instance=this;
    }
    public JobManager getJobManager() {
        return jobManager;
    }

    public static JobQueueApplication getInstance() {
        return instance;
    }

    private void configureJobManager() {
        //3. JobManager的配置器,利用Builder模式
        Configuration configuration = new Configuration.Builder(this)
                .customLogger(new CustomLogger() {
                    private static final String TAG = "JOBS";
                    @Override
                    public boolean isDebugEnabled() {
                        return true;
                    }

                    @Override
                    public void d(String text, Object... args) {
                        Log.d(TAG, String.format(text, args));
                    }

                    @Override
                    public void e(Throwable t, String text, Object... args) {
                        Log.e(TAG, String.format(text, args), t);
                    }

                    @Override
                    public void e(String text, Object... args) {
                        Log.e(TAG, String.format(text, args));
                    }
                })
                .minConsumerCount(1)//always keep at least one consumer alive
                .maxConsumerCount(3)//up to 3 consumers at a time
                .loadFactor(3)//3 jobs per consumer
                .consumerKeepAlive(120)//wait 2 minute
                .build();
        jobManager = new JobManager(this, configuration);
    }
}

以上是整個自定義Application類的代碼,其邏輯很清晰。首先為Application類設置單例模式,并保存私有變量jobManager;然后在onCreate()中調用configureJobManager方法來完成jobManager的初始化。我們來看下在其初始化參數Configuration實例中都配置了哪些內容:

  • CustomLogger:日志設置,便于用戶查看任務隊列的工作信息,在調試的過程中很有用,后面分析JobManager的任務調度時就會用到;
  • minConsumerCount&maxConsumerCount: 最少消費者和最多消費者數量,所謂的消費者就是開啟的線程,用來執行任務。任務隊列實際上就是一個生產者和消費者問題,用戶是生產者,提交任務(Job),開啟的線程就是消費者來執行任務,任務被執行就是“消費”。這里所謂的最少和最大將會下面具體解釋;
  • loadFactor(int): 其意義是設置多少個任務為一組被分配個一個消費者(Thread),也就是一個Thread最多要“承包”幾個任務來執行;
  • consumerKeepAlive :設置消費者在沒有任務的情況下保持存活的時長,以秒為單位,如果過了這個時長還沒有任務,消費者線程就會被回收;

Configuration以建筑者模式來鏈式配置,對此不是很熟悉的讀者可以參考如何構建含有大量參數的構造器:淺談Builder Pattern的使用和鏈式配置.
有了JobManger自然還需要Job,下面就來看看如何設置Job.

2.3 Job

自定義的Job類需要繼承Android-Priority-Job-Queue提供的Job類,下面就是是一個簡單的范例,這個任務的內容就是睡眠5秒。

import android.util.Log;
import com.path.android.jobqueue.Job;
import com.path.android.jobqueue.Params;
import com.path.android.jobqueue.RetryConstraint;

public class MyJob extends Job{
    public static final int PRIORITY = 1;
    private String text;
    String TAG = "Myjob";
    int sleepTime;
    public MyJob(String text) {
        // A job should be persisted in case the application exits 
        // before job is completed.
        super(new Params(PRIORITY).persist());
        this.text = text;
        sleepTime = 5;
        Log.i(TAG, text+"  goin");
    }
    @Override
    public void onAdded() {
        // Job has been saved to disk.
        // This is a good place to dispatch a UI event 
        // to indicate the job will eventually run.
        Log.i(TAG, text+"  Onadded");
    }
    @Override
    public void onRun() throws Throwable {
        // Job logic goes here. 
        // All work done here should be synchronous, 
        // a job is removed from the queue once onRun() finishes.
        Thread.sleep(sleepTime*1000);
        Log.i(TAG, text+"  onRun");
    }
    @Override
    protected RetryConstraint shouldReRunOnThrowable(Throwable throwable, int runCount,
                                                     int maxRunCount) {
        // An error occurred in onRun.
        // Return value determines whether this job should retry or cancel. You can further
        // specify a backoff strategy or change the job's priority. You can also apply the
        // delay to the whole group to preserve jobs' running order.
        return RetryConstraint.createExponentialBackoff(runCount, 10);
    }
    @Override
    protected void onCancel() {

    }
}

Job類的模塊很清晰,我們只需要按照要求覆蓋以下方法即可:

  1. 在構造器,利用Params類中配置參數;
  2. onAdded(): 任務加入隊列并被保存在硬盤上,定義此時要處理的邏輯;
  3. onRun(): 任務開始執執行,在此定義任務的主題邏輯,當執行完畢后,任務將被從任務隊列中刪除;
  4. onCancel():任務取消的時候要執行的邏輯;
  5. shouldReRunOnThrowable():當onRun()方法中拋出異常時,就會調用該函數,該函數返回Job類在執行發生異常時的應對策略,是重新執行還是取消,或者是一定時間之后再嘗試。

在這里特別說明下Params類,通過該類可以配置Job類的各種信息,同樣也采用類Builder Pattern的鏈式配置:

  1. 默認構造器傳入的是int參數是該任務的優先級,優先級越高,越優先執行。
public Params(int priority) {
        this.priority = priority;
    }
  1. requireNetwork(): 設置該任務要求訪問網絡;
  2. groupBy(String groupId):設置組ID,被設置相同組ID的任務,將會按照順序執行;
  3. persist():設置任務為可持久化的,持久化要求Job類為序列化的,這一點并不意外,因為一個類的內容只有序列化之后才能變成字節模式保存在硬盤上;
  4. delayInMs(long delayMs):設置延遲時間,ms為單位,在該時間之后再放入任務隊列中。

2.4 執行任務

Job的執行很簡單,就把任務類加入到任務隊列中即可以。


public class MainActivity extends AppCompatActivity {
    private JobManager jobManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_start = (Button)findViewById(R.id.start_job_button);
        //JobManager對象
        jobManager = JobQueueApplication.getInstance().getJobManager();       
        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for(int i=0; i<9 ;i++) {
                    //將任務加入后臺隊列中
                    jobManager.addJobInBackground(new MyJob(“"+i));
                } 
            }
        });
    }
}

在一個Activity中點擊Button,就會把9個MyJob實例加入到后臺隊列中。其運行效果如下:

03-25 21:44:47.207 I/Myjob: 0 goin
03-25 21:44:47.208 I/Myjob: 1 goin
03-25 21:44:47.208 I/Myjob: 2 goin
03-25 21:44:47.208 I/Myjob: 3 goin
03-25 21:44:47.208 I/Myjob: 4 goin
03-25 21:44:47.208 I/Myjob: 5 goin
03-25 21:44:47.208 I/Myjob: 6 goin
03-25 21:44:47.208 I/Myjob: 7 goin
03-25 21:44:47.208 I/Myjob: 8 goin
03-25 21:44:47.218 I/Myjob: 0 Onadded
03-25 21:44:47.228 I/Myjob: 1 Onadded
03-25 21:44:47.241 I/Myjob: 2 Onadded
03-25 21:44:47.251 I/Myjob: 3 Onadded
03-25 21:44:47.260 I/Myjob: 4 Onadded
03-25 21:44:47.274 I/Myjob: 5 Onadded
03-25 21:44:47.280 I/Myjob: 6 Onadded
03-25 21:44:47.291 I/Myjob: 7 Onadded
03-25 21:44:47.307 I/Myjob: 8 Onadded
03-25 21:44:52.235 I/Myjob: 1 onRun
03-25 21:44:52.267 I/Myjob: 4 onRun
03-25 21:44:52.297 I/Myjob: 7 onRun
03-25 21:44:57.250 I/Myjob: 8 onRun
03-25 21:44:57.282 I/Myjob: 6 onRun
03-25 21:44:57.310 I/Myjob: 5 onRun
03-25 21:45:02.264 I/Myjob: 3 onRun
03-25 21:45:02.299 I/Myjob: 2 onRun
03-25 21:45:02.324 I/Myjob: 0 onRun

為了便于查看,這里輸出的日志信息只保留最主要的內容,我們可以看到任務0-8依次啟動并加入到任務隊列中,然后再被執行,JobManager幫你管理了一切,完美!
有的讀者可能已經發行了,任務0-8的執行順序是混亂的,這就涉及到Android-Priority-JobQueue任務的調度問題,也是本人最好興趣的內容。

3. 任務調度分析

回顧一下,我們對JobManager的設置:至少一個消費者線程,至多三個;每個消費者線程最多處理三個任務。這些設置就是在影響任務調度。
當一個任務要加入任務隊列時,會做出如下的判斷:

  1. 首先計算當前能處理的任務數的最大值(CurrentTaskCapacity)= 已啟動的消費線程數(CurrentThreadNum) * 每個線程處理任務的容量(PerThreadTaskCapacity);
CurrentTaskCapacity = CurrentThreadNum * PerThreadTaskCapacity
  1. 如果當前任務數加上現在要加入任務,小于或等于CurrentTaskCapacity,則將該任務加入到任務隊列中;
  2. 如果當前任務數加上現在要加入任務,大于CurrentTaskCapacity,則判斷CurrentThreadNum是不是已經達到設置的最大值,如果沒有就開辟新的消費者線程來承擔任務;
  3. 如果CurrentTaskCapacityCurrentThreadNum都達到上限,那就對不起了,該任務就不能加入隊列,需要等到有任務執行完畢,任務容量又有了空余時才能進入隊列等待執行。

如何驗證上面結論呢?還記得我們在Application類中配置JobManager時設置的日志信息嗎? 這個時候它就發揮作用了,下面是任務0-8加入到任務隊列中時輸出的日志,內容很多,請注意的表黑的部分:

03-25 21:44:47.218 D/JOBS: added job id: 84 class: MyJob priority: 0 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.218 I/Myjob: 0 Onadded
03-25 21:44:47.222 D/JOBS: pool-1-thread-1: load factor check. true = (0 < 1)|| (0 * 3 < 1 + 0). consumer thread: false
03-25 21:44:47.222 D/JOBS: adding another consumer
03-25 21:44:47.223 D/JOBS:** starting consumer Thread-177**
03-25 21:44:47.224 D/JOBS: looking for next job
03-25 21:44:47.224 D/JOBS: running groups
03-25 21:44:47.224 D/JOBS: non persistent result null
03-25 21:44:47.228 D/JOBS: added job id: 85 class: MyJob priority: 1 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.228 I/Myjob: 1 Onadded
03-25 21:44:47.234 D/JOBS: persistent result com.path.android.jobqueue.JobHolder@55
03-25 21:44:47.234 D/JOBS: running job MyJob
03-25 21:44:47.237 D/JOBS: pool-1-thread-1: load factor check. false = (1 < 1)|| (1 * 3 < 1 + 1). consumer thread: false
03-25 21:44:47.241 D/JOBS: added job id: 86 class: MyJob priority: 2 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.241 I/Myjob: 2 Onadded
03-25 21:44:47.245 D/JOBS: pool-1-thread-1: load factor check. false = (1 < 1)|| (1 * 3 < 2 + 1). consumer thread: false
03-25 21:44:47.251 D/JOBS: added job id: 87 class: MyJob priority: 3 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.251 I/Myjob: 3 Onadded
03-25 21:44:47.255 D/JOBS: pool-1-thread-1: load factor check. true=(1 < 1) || (1 * 3 < 3 + 1). consumer thread: false
03-25 21:44:47.255 D/JOBS: adding another consumer
03-25 21:44:47.256 D/JOBS: starting consumer Thread-178
03-25 21:44:47.257 D/JOBS: looking for next job
03-25 21:44:47.257 D/JOBS: running groups
03-25 21:44:47.257 D/JOBS: non persistent result null
03-25 21:44:47.260 D/JOBS: added job id: 88 class: MyJob priority: 4 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.260 I/Myjob: 4 Onadded
03-25 21:44:47.264 D/JOBS: persistent result com.path.android.jobqueue.JobHolder@58
03-25 21:44:47.264 D/JOBS: running job MyJob
03-25 21:44:47.270 D/JOBS: pool-1-thread-1: load factor check. false = (2 < 1)|| (2 * 3 < 3 + 2). consumer thread: false
03-25 21:44:47.274 D/JOBS: added job id: 89 class: MyJob priority: 5 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.274 I/Myjob: 5 Onadded
03-25 21:44:47.277 D/JOBS: pool-1-thread-1: load factor check. false = (2 < 1)|| (2 * 3 < 4 + 2). consumer thread: false
03-25 21:44:47.280 D/JOBS: added job id: 90 class: MyJob priority: 6 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.280 I/Myjob: 6 Onadded
03-25 21:44:47.283 D/JOBS: pool-1-thread-1: load factor check. true = (2 < 1)|| (2 * 3 < 5 + 2). consumer thread: false
03-25 21:44:47.283 D/JOBS: adding another consumer
03-25 21:44:47.287 D/JOBS: starting consumer Thread-179
03-25 21:44:47.288 D/JOBS: looking for next job
03-25 21:44:47.288 D/JOBS: running groups
03-25 21:44:47.288 D/JOBS: non persistent result null
03-25 21:44:47.291 D/JOBS: added job id: 91 class: MyJob priority: 7 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.291 I/Myjob: 7 Onadded
03-25 21:44:47.295 D/JOBS: persistent result com.path.android.jobqueue.JobHolder@5b
03-25 21:44:47.295 D/JOBS: running job MyJob
03-25 21:44:47.302 D/JOBS: pool-1-thread-1: load factor check. false = (3 < 1)|| (3 * 3 < 5 + 3). consumer thread: false
03-25 21:44:47.306 D/JOBS: added job id: 92 class: MyJob priority: 8 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.307 I/Myjob: 8 Onadded
03-25 21:44:47.310 D/JOBS: pool-1-thread-1: load factor check. false = (3 < 1)|| (3 * 3 < 6 + 3). consumer thread: false

日志中這樣的load factor check. true = (0 < 1)|| (0 * 3 < 1 + 0)計算表達式,就是在進行線程調度的判定,當計算表達式為true時,就意味著要啟動新的消費者進程。 ||右邊括號內的表達式就是在比較當前任務數和當前任務容量。

通過日志我們可以看到,面對任務0-8,JobManager依次啟動了三個消費者進程,并將這9個任務分配給他們:

  • Thread-177:任務0、1、2;
  • Thread-178:任務3、4、5;
  • Thread-179:任務6、7、8;

三個消費者線程并發執行,由于所有任務的優先級都是一樣的,消費者線程們就會隨機執行任務。

消費者線程被創建,自然也會被回收,這才是完整的生命周期。以下是當任務執行完成時日志的輸出:

03-25 21:47:02.280 D/JOBS: Thread-177: load factor check. false = (2 < 1)|| (2 * 3 < 0 + 0). consumer thread: true
03-25 21:47:02.280 D/JOBS: finishing consumer Thread-177
03-25 21:47:02.310 D/JOBS: Thread-178: load factor check. false = (1 < 1)|| (1 * 3 < 0 + 0). consumer thread: true
03-25 21:47:02.310 D/JOBS: finishing consumer Thread-178
03-25 21:47:02.337 D/JOBS: Thread-179: load factor check. true = (0 < 1)|| (0 * 3 < 0 + 0). consumer thread: true
03-25 21:47:02.337 D/JOBS: didn't allow me to die, re-running Thread-179
03-25 21:47:02.337 D/JOBS: re-running consumer Thread-179

我們可以看到,當任務執行完畢之后Thread-177和Thread-178都被回收了,但是由于我們設置了最小消費者線程數為1,所以Thread-179被留下“堅守崗位”,等待下一個任務的到來,直到超時。

由此可知,JobMananger的任務調度機制還是十分復雜和完備的,真慶幸已經有人幫我們實現了。

4. RetryConstraint

最后要說的就是RetryConstraint,即任務在執行中發生異常之后要執行的策略。用戶可以根據自己的使用情況來設置:

  1. RETRY: RetryConstraint的自帶策略,立刻重新嘗試執行策略,直到執行成功或者嘗試次數達到最大(18次);
  2. CANCEL:RetryConstraint的自帶策略,取消當前任務的執行;
  3. *createExponentialBackoff(int runCount, long initialBackOffInMs) *:定期延遲嘗試執行任務,如果任務執行失敗,下次執行的延遲時間會以指數形式增長,最大嘗試次數為20次;
public static RetryConstraint createExponentialBackoff(int runCount, long initialBackOffInMs) {
        RetryConstraint constraint = new RetryConstraint(true);
        constraint.setNewDelayInMs(initialBackOffInMs *
                (long) Math.pow(2, Math.max(0, runCount - 1)));
        return constraint;
    }

在官方給出的示例PostTweet中,是這樣定義* RetryConstraint*的:

    @Override
    protected RetryConstraint shouldReRunOnThrowable(Throwable throwable, int runCount,
            int maxRunCount) {
        if(throwable instanceof TwitterException) {
            //if it is a 4xx error, stop
            TwitterException twitterException = (TwitterException) throwable;
            int errorCode = twitterException.getErrorCode();
            return errorCode < 400 || errorCode > 499 ? RetryConstraint.RETRY : RetryConstraint.CANCEL;
        }
        return RetryConstraint.RETRY;
    }

如果是4XX的錯誤(服務器錯誤)就取消訪問,其他異常就離開重新嘗試請求。

關于Android-Priority-JobQueue,今天先介紹到這里,內容已經夠多了。總之,這是一個很優秀的任務隊列管理庫,很值得使用和研究。歡迎大家嘗試,以及給我留言指教。

閱讀參考:

  1. Android Priority Job Queue 入門
  2. 如何構建含有大量參數的構造器:淺談Builder Pattern的使用和鏈式配置
  3. Java序列化心得(一):序列化設計和默認序列化格式的問題
  4. Java序列化心得(二):自定義序列化
  5. Android Priority Job Queue (Job Manager):線程任務的容錯重啟機制(二)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • **2014真題Directions:Read the following text. Choose the be...
    又是夜半驚坐起閱讀 9,692評論 0 23
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 導語: 在情竇初開的年少時代, 我們每人都有一個關于情感的幸福夢想。 或許是一個眼神, 或許是一個轉身, 或許是一...
    摯若初見閱讀 365評論 0 0
  • 每天給自己定的計劃總是感覺沒有認真的對待,感覺都是在敷衍了事。以后每天早上給自己做計劃的時候一定要切合實際,必須要...
    平凡的進步閱讀 143評論 0 0
  • 細節固然重要,但不是所有的細節都重要。要先把握主題再談細節。基本功能沒有實現,去折磨細節就是扯淡。
    suxiliu閱讀 224評論 0 0