Givemepass's Android 惡補筆記

如何自訂Dialog之二

| Comments

如果想要做出這樣的畫面

一般會使用什麼元件?
我試過popupwindow, spinner都沒有達到我要的效果,
因此我用dialog刻出了一個view。

這個view位置可以自訂

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/more_outside_layout"
                android:layout_width="match_parent"
                android:layout_height="@dimen/PXP600_PX"
                android:background="#00000000" >
    <LinearLayout
        android:id="@+id/more_inside_layout"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_marginRight="@dimen/PXP8_PX"
        android:layout_marginTop="@dimen/PXP10_PX"
        android:layout_width="@dimen/PXP330_PX"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:background="@drawable/abcd_menu_dropdown_panel_holo_light"
        >
        <TextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/PXP90_PX"
            android:gravity="center_vertical"
            android:layout_marginLeft="@dimen/PXP32_PX"
            android:id="@+id/file_sharing_add_new_folder"
            android:textColor="#707070"
            android:textSize="@dimen/PXP32_TS"
            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/PXP90_PX"
            android:gravity="center_vertical"
            android:layout_marginLeft="@dimen/PXP32_PX"
            android:id="@+id/file_sharing_upload_file"
            android:textColor="#707070"
            android:textSize="@dimen/PXP32_TS"
            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/PXP90_PX"
            android:gravity="center_vertical"
            android:layout_marginLeft="@dimen/PXP32_PX"
            android:id="@+id/file_sharing_upload_photo"
            android:textColor="#707070"
            android:textSize="@dimen/PXP32_TS"
            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/PXP90_PX"
            android:gravity="center_vertical"
            android:layout_marginLeft="@dimen/PXP32_PX"
            android:id="@+id/file_sharing_upload_video"
            android:textColor="#707070"
            android:textSize="@dimen/PXP32_TS"
            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/PXP90_PX"
            android:gravity="center_vertical"
            android:layout_marginLeft="@dimen/PXP32_PX"
            android:id="@+id/file_sharing_take_picture_or_record"
            android:textColor="#707070"
            android:textSize="@dimen/PXP32_TS"
            />
    </LinearLayout>
</RelativeLayout>


因為dialog是全螢幕的, 而內部的view可以根據我們的需求進行調整位置跟大小,
如上面程式碼是設定成330*600的狀態。

public class MoreDialog extends Dialog {

    private Context mContext;

    private View outSideLayout;

    private OnItemSelectedListener mOnItemSelectedListener;

    public interface OnItemSelectedListener{
        public void OnItemSelected(int pos);
    }

    public void setOnItemSelectedListener(OnItemSelectedListener listener){
        mOnItemSelectedListener = listener;
    }

    private TextView item1, item2, item3, item4, item5;

    public MoreDialog(Context context) {
        super(context, android.R.style.Theme);
        mContext = context;

        getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        View mainView = LayoutInflater.from(mContext).inflate(R.layout.file_sharing_more, null, false);
        setContentView(mainView);

        outSideLayout = mainView.findViewById(R.id.more_outside_layout);
        outSideLayout.getLayoutParams().height = 2000;
        outSideLayout.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                dismiss();

            }
        });
        item1 = (TextView) findViewById(R.id.file_sharing_add_new_folder);
        item2 = (TextView) findViewById(R.id.file_sharing_upload_file);
        item3 = (TextView) findViewById(R.id.file_sharing_upload_photo);
        item4 = (TextView) findViewById(R.id.file_sharing_upload_video);
        item5 = (TextView) findViewById(R.id.file_sharing_take_picture_or_record);

        item1.setText("Item 1");
        item2.setText("Item 2");
        item3.setText("Item 3");
        item4.setText("Item 4");
        item5.setText("Item 5");

        item1.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if(mOnItemSelectedListener != null){
                    mOnItemSelectedListener.OnItemSelected(0);
                }
                dismiss();
            }
        });
        item2.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if(mOnItemSelectedListener != null){
                    mOnItemSelectedListener.OnItemSelected(1);
                }
                dismiss();
            }
        });
        item3.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if(mOnItemSelectedListener != null){
                    mOnItemSelectedListener.OnItemSelected(2);
                }
                dismiss();
            }
        });
        item4.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if(mOnItemSelectedListener != null){
                    mOnItemSelectedListener.OnItemSelected(3);
                }
                dismiss();
            }
        });
        item5.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if(mOnItemSelectedListener != null){
                    mOnItemSelectedListener.OnItemSelected(4);
                }
                dismiss();
            }
        });
    }


}

再來就是new出dialog,
我就可以在右上角產生出一個小list。

private Button showDialog;

    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = getApplicationContext();
        setContentView(R.layout.activity_main);
        showDialog = (Button) findViewById(R.id.show_dialog);

        showDialog.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                MoreDialog mMoreDialog = new MoreDialog(MainActivity.this);
                mMoreDialog.setOnItemSelectedListener(new MoreDialog.OnItemSelectedListener() {
                    @Override
                    public void OnItemSelected(int pos) {
                        Toast.makeText(MainActivity.this, "Item " + (pos + 1) + " is selected.", Toast.LENGTH_SHORT).show();
                    }
                });
                mMoreDialog.show();
            }
        });

    }

註冊dialog的listener,
而當點下item的時候, 顯是對應的文字,
大功告成。
程式碼

如何自訂Dialog之一

| Comments

如果想要想要跳出一個畫面, 又不想處理複雜的生命周期,
其實Dialog是你的好朋友。

我也蠻建議最好一隻程式, 不要開太多Activity,
之前在寫專案的時候, 就是因為開太多Activity, 所以效能大爆炸。

而最單純的頁面就是Dialog, 只要呼叫它, 就不用太多複雜的事情要處理。
如果要做管理, 其實他就是疊加在上一層畫面, 可以在建立的時候,
就決定該層畫面是否要關閉, 非常方便。

一開始建立一個很簡單的畫面, 只要有一個Button在上面,
按下去以後, 就會跳出我們自訂的Dialog。

private Button showDialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    showDialog = (Button) findViewById(R.id.show_dialog);
    
    showDialog.setOnClickListener(new OnClickListener() {
        
        @Override
        public void onClick(View v) {
            new MyDialog(MainActivity.this).show();
        }
    });
    
} 

那接著就實做我們的Dialog

public class MyDialog extends Dialog{

    public MyDialog(Context context) {
        super(context, android.R.style.Theme_Light);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.dialog_layout);
    }

}

就這麼簡單? 沒錯!
而且只需要呼叫show(); 就可以把畫面叫出來了。

白茫茫的一片, 好空虛。
加個背景顏色跟文字好了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff000" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="hi"
        />

</RelativeLayout>

這樣就有背景顏色跟文字了, 內容想放什麼都可以,
之後在寫一篇如何傳遞資訊跟即時關閉。

程式碼

如何使用IntentService

| Comments

IntentService提供了Thread跟Handler、Looper機制, 讓你在操作Service更加方便,
而且確保了執行緒的安全。

跟操作Service相同, 必須在AndroidManifest.xml開啟權限。

<service android:name=".IntentServiceDemo"/>

接著宣告一個IntentService的類別。

public class IntentServiceDemo extends IntentService{
    public IntentServiceDemo(String name){
        super(name);
    }
    
    @Override
    protected void onHandleIntent(Intent intent){
        //這邊是背景執行緒

    }
}

這邊用法跟HandlerThread相同, 必須傳入名稱到父類別,
而且只要覆寫onHandleIntent方法, 就可以將長任務寫進這個方法。
如果你想跟Service一樣使用重啟的選項, IntentService提供了兩種方法,

  • START_NOT_STICKY
  • START_STICKY 預設是後者, 如果你想要調整為前者, 則可以使用以下方法。
setIntentRedelivery(true);

如果在Activity想要使用它, 跟啟動的Service相同。

public class MainActivity extends Activity{
    public void onCreate(Bundle bundle){
        Intent intent = new Intent(this, IntentServiceDemo.class);
        startService(intent);
    }
}

你不需要自行停止service, 因為它會自動判斷是否有在使用, 而自我停止。

如何使用Service

| Comments

  • 為什麼要使用Service?
    • 將元件的生命週期跟Thread的生命週期分開(避免前述Thread參考到元件, 在Thread結束前無法釋放物件導致Memory leak)
    • 當一個Process內只剩下Thread在執行, 避免Process被系統意外回收, 導致Thread被提前結束
    • 跨行程演出

Service分成兩種: startService和bindService。

  • startService
    • 由第一個元件啟動, 由第一個元件結束, 這個是最常見的, 也是最簡單的啟動方式。

一開始先宣告一個Service的類別

 public class ServiceDemo extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

由於是啟動Service, 因此onBind會是return null, 由於Service是運作在UI Thread上面,
因此你必須將長任務開個Thread並且執行在onStartCommand方法內。

接著Actvitiy對此類別進行啟動。

     Intent intent = new Intent(this, ServiceDemo.class);
     startService(intent);

這樣就可以啟動一個Service了。

  • 有時候在系統記憶體吃緊的時候, 會將系統某些Service收起來, 這時候有幾個參數可以讓系統替你重新啟動Service。

    • START_STICKY : Service被殺掉, 系統會重啟, 但是Intent會是null。
    • START_NOT_STICKY : Service被系統殺掉, 不會重啟。
    • START_REDELIVER_INTENT : Service被系統殺掉, 重啟且Intent會重傳。 透過以上的參數, 放在onStartCommand的return參數就可以使用重啟的功能了。
  • bindService

    • 由第一個元件進行連繫(bind), 此時Service就會啟動, 接著後面可以多個元件進行bind, 而當所有的元件都結束連繫(unbind), 則Service會進行銷毀。
public class ServiceDemo extends Service {
    private MyBinder mBinder = new MyBinder();
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }
    public String getServiceName(){
        return ServiceDemo.class.getName();
    }
    private class MyBinder extends Binder{
        public ServiceDemo getService(){
            return ServiceDemo.this;
        }
    }
}

以上程式碼可以看到Service的宣告, 此時你可以宣告一個Binder, 透過Binder來取得整個Service的實體, 因此你可以透過這個實體來引用Service上任何一個公開的方法。

在Activtiy部分

private LoaclServiceConnection mLoaclServiceConnection = new LoaclServiceConnection();
public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        bindService(new Intent(this, ServiceDemo.class), mLoaclServiceConnection, Context.BIND_AUTO_CREATE);
    }
}
private class LoaclServiceConnection implements ServiceConnection{
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        //透過Binder調用Service內的方法

        String name = ((ServiceDemo)service.getService()).getServiceName();
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
        //service 物件設為null

    }
}

在這邊連繫的Service必須傳入一個ServiceConnection物件, 而當連繫成功以後, 就會呼叫ServiceConnection的onStartConnection方法, 將Service的Binder回傳回來, 透過轉型為Service本身的實體, 就可以操作Service上的任意方法。
這樣就是一個最簡單的Service雙向溝通。

如何避免新建Project出現value-v23,xml紅字

| Comments

今天踩到一個雷, 新建一個專案最小sdk設定為15, target為22,
結果居然跳出value-v23.xml有紅字,
我就納悶了, google一下, 原來v23跟v22會有衝突,

解法一, 下載sdk v23, target選定23。
解法二, 在gradle那邊將v7那包lib改成22版本。

如何使用Future Pattern

| Comments

Future Pattern中,
我們實作了Future Pattern, 但是其實Java本身就實作了這個Pattern。

Java提供Callable以及FutureTask這兩個類別讓我們能夠輕易完成Future Patern,
一開始先建立一個Data類別。

public class Data{
        private String data;

        public String getData() {
            return data;
        }

        public void setData(String data) {
            this.data = data;
        }
}

這個類別很單純的只是把字串設定跟取得。

接著就是我們的主要類別

 private FutureTask<Data> setup(){
    FutureTask<Data> future = new FutureTask<Data>(new Callable<Data>(){

        @Override
        public Data call() throws Exception {
            Data data = new Data();
            for(int i = 1; i <= 10; i++){
                System.out.println(i * 10 + "%");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            data.setData("completed");
            return data;
        }
    });
    Thread thread = new Thread(future);
    thread.start();
    return future;
}

Callable這個類別基本上跟Runnable很像, 都是可以丟進Thread內執行的,
唯一的差別是他會回傳出資料。
FutureTask就不陌生了, Future Pattern就說明了他的原理。

將主要任務放在call裡面, 接著將FutureTask物件直接丟給Thred執行即可,
當執行完畢就把FutureTask物件回傳出去。

    public static void main(String[] args) {
            Main m = new Main();
            FutureTask<Data> futureTask = m.setup();
            try {
                    while(!futureTask.isDone());
                    System.out.println(futureTask.get().getData());
            } catch (InterruptedException e) {
                    e.printStackTrace();
            } catch (ExecutionException e) {
                    e.printStackTrace();
            }
    }

在主程式內不斷的去確認FutureTask是否完成, 如果完成就可以印出completed字串。

結果

10%
20%
30%
40%
50%
60%
70%
80%
90%
100%
completed

github

Simple Factory Pattern

| Comments

簡單工廠模式(Simple Factory Pattern)是一種把演算法包起來的模式,

public class AFactory {
    public A creator(int type){
            A a = null;
            switch(type){
            case 1:
                a = new A1();
              break;
          case 2:
                a = new A2();
              break;
        }
        return a;
    }
}

就像這樣把creator封裝起來,
好處是只要傳入對應的參數, 就可以得到對應的物件,
壞處就是這樣未來要新增刪除修改類型, 並不好維護。

如何使用Glide-2

| Comments

如何使用LRUCache中, 土法煉鋼自己控制下載網路圖片(當然還有很多方法可以實現),
不過如果不是喜歡控制一些複雜的程序,
其實可以直接使用第三方做掉,
Glide就是一個很好的工具。

下載一模一樣的圖

private String[] mImgsPath = {
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/01.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/02.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/03.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/04.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/05.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/06.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/07.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/08.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/09.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/10.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/11.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/12.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/13.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/14.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/15.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/16.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/17.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/18.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/19.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/20.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/21.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/22.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/23.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/24.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/25.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/26.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/27.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/28.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/29.jpg"
    };

我們只需要改變讀圖方式, 在getView加入

@Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            View v = convertView;
            final Holder holder;
            if(null == v){
                v = LayoutInflater.from(MainActivity.this).inflate(R.layout.list_item, null);
                holder = new Holder();
                holder.img = (ImageView) v.findViewById(R.id.img);
                v.setTag(holder);
            } else{
                holder = (Holder) v.getTag();
            }
            holder.img.setImageResource(R.drawable.default_img);
            Glide.with(MainActivity.this)
                    .load(mImgsPath[position])
                    .error(R.drawable.default_img)
                    .centerCrop()
                    .fitCenter()
                    .into(holder.img);
            return v;
        }

就可以完成讀圖了, 就是如此簡單。

github

如何使用LRUCache

| Comments

如果要使用圖片Cache,
Android官網LRUCacheCache Image都建議使用LRU Cache,
他可以限定cache的大小並且把最少使用的cache清除掉。

所以我們用ListView呈現從網路上抓下來的圖片

private String[] mImgsPath = {
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/01.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/02.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/03.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/04.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/05.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/06.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/07.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/08.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/09.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/10.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/11.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/12.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/13.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/14.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/15.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/16.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/17.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/18.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/19.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/20.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/21.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/22.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/23.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/24.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/25.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/26.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/27.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/28.jpg",
            "https://dl.dropboxusercontent.com/u/24682760/Android_AS/LRUCacheDemo/29.jpg"
    };

圖片來源

LRUCache的使用方法很簡單

    private LruCache<String, Bitmap> mLruCache;
    //宣告的時候, 可以設定chache多大

mLruCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount() / 1024;
            }
    };
//如果要使用的時候

mLruCache.put(key, bmp);
//然後取出

Bitmap b = mLruCache.get(key);

類似Map的存取方式, 比較不同的是他會自動清除內部的資料。

接著利用如何使用HandlerThread來宣告一個Thread, 用來處理從網路上下載的圖片。

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandlerThread = new HandlerThread("LRU Cache Handler");
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 2;

        mLruCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount() / 1024;
            }
        };
        mListView = (ListView) findViewById(R.id.list);
        mAdapter = new MyAdapter();
        mListView.setAdapter(mAdapter);
}

前面先把HandlerThread宣告出來, 接著在宣告出LRUCache, 用來存放我們的Map,
利用Runtime.getRuntime().maxMemory()方法可以得知硬體最大cache放到多少,
這樣就不會發生OOM。

從官網這篇Loading Large Bitmaps Efficiently, 建議利用縮圖的方式來進行讀圖。

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image

        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both

            // height and width larger than the requested height and width.

            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

    public static Bitmap decodeBitmap(String url, int maxWidth){

        Bitmap bitmap = null;
        try{
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            options.inSampleSize = calculateInSampleSize(options, maxWidth, maxWidth);

            InputStream is = (InputStream) new URL(url).getContent();
            bitmap = BitmapFactory.decodeStream(is, null, options);
        } catch (MalformedInputException e){
            e.printStackTrace();
        } catch(Exception e){
            e.printStackTrace();
        }
        return bitmap;
    }

因此改寫了從網路上抓取以及縮圖的兩個方法。

接下來是Adapter重頭戲部分。

private class MyAdapter extends BaseAdapter {
        private Map<String, String> mLoadingMap;

        public MyAdapter() {
            mLoadingMap = new HashMap<String, String>();
        }

        @Override
        public int getCount() {
            return mImgsPath.length;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
                View v = convertView;
                final Holder holder;
                if(null == v){
                        v = LayoutInflater.from(MainActivity.this).inflate(R.layout.list_item, null);
                        holder = new Holder();
                        holder.img = (ImageView) v.findViewById(R.id.img);
                        v.setTag(holder);
                } else{
                        holder = (Holder) v.getTag();
                }
                holder.img.setImageResource(R.drawable.default_img);
                final String key = position + "_cache";
                Bitmap b = mLruCache.get(key);
                if(b == null && !mLoadingMap.containsKey(key)) {
                        mLoadingMap.put(key, mImgsPath[position]);
                        Log.e("lru", "load pic" + position);
                        mHandler.post(new Runnable() {
                                Bitmap bmp;
                                @Override
                                public void run() {
                                        bmp = decodeBitmap(mImgsPath[position], 200);
                                        mLruCache.put(key, bmp);
                                        runOnUiThread(new Runnable() {
                                                @Override
                                                public void run() {
                                                        notifyDataSetChanged();
                                                mLoadingMap.remove(key);
                                        }
                                });
                        }
                });

            } else{
                Log.e("lru", "cache");
                holder.img.setImageBitmap(b);
            }
            return v;
        }
        class Holder{
            ImageView img;
        }
    }

這邊利用網路讀圖, 然後呈現出來,
當中利用了三個小技巧,
一個是當讀完圖的時候, 不是直接設定到ImageView而是去刷新整個頁面,
這樣一來就會統一讀取cache, 如果直接設定ImageView,
當上下滑動快速的時候, 就會不斷的更新之前滑過的部分, 直到最新的image。

第二個技巧就是利用HandlerThread去進行抓圖任務, HandlerThread的好處就是執行緒安全,
他會一個接一個任務去抓, 因此不會出現同時兩個任務共同存取cache,
但是壞處是滑過的地方會循序, 這邊還可以用其他方法改進。

第三個技巧就是利用Map讓正在Queue等待的Task, 不會重覆,
如果少了這個Queue, 我們印出的訊息就會長這樣。

load pic6
load pic7
load pic8
load pic9
load pic10
load pic7
load pic6
load pic5
load pic4
load pic3
load pic2
load pic1
load pic0
load pic0
load pic1
load pic2

同時出現0、1、2已經在Queue內了還會重覆出現。



github

reference

Future Pattern

| Comments

如果有一個任務需要花一下時間來執行, 可是使用者卻不可以那邊等待任務完成,
這時候就可以使用Future Pattern,
有點像預購iPhone的概念, 我先拿到一個預購單, 等到iPhone來了再拿預購單換實機。


如上圖, 我先給你一個Future Data物件,
但是我會去執行Real Data物件, 等我執行完畢,
則會將結果回傳給你的Future Data物件。

題目: 模擬跟server要一個物件
解法: 使用Future Pattern, server先給你一個Future Data, 
      等待Real Data好了, 再將結果回傳給Future Data。

先宣告一個Data物件。

public interface Data {
    public String getResult();
}

由於只是例子, 只宣告一個回傳結果的方法。

public class FutureData implements Data{

    protected RealData mRealData;

    public synchronized void setRealData(RealData realData){
        mRealData = realData;
        notifyAll();
    }

    public synchronized String getResult() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return mRealData.result;
    }
}

接著讓我們的Future Data去實作Data, 宣告兩個方法,
第一個方法是將ReadData物件指派給它, 代表我們的Real Data好了,
因此就可以去把Future Data等待的部分叫醒, 跟它說可以回傳結果了。

第二個方法就是在這邊等待, 讓Real Data來叫醒我, 就可以回傳Real Data的結果。

注意這兩個方法都要使用synchronized關鍵字, 避免上鎖有人開門進來。

那來看Real Data部分

public class RealData implements Data{

    protected String result;

    public RealData() {}

    public void doTask(){
        for(int i = 1; i <= 10; i++){
            System.out.println(i * 10 + "%");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        result = "completed";
    }
    @Override
    public String getResult() {
        return result;
    }
}

當Real Data被建立出來的時候, doTask每次迴圈睡1秒用來模擬server執行的進度,
並且映出百分比, 當整段程式碼完成以後, 則顯示完成。

接下來看我們Client物件怎麼呼叫。

public class Client {
    private FutureData mFutureData;

    public Client(){
        mFutureData = new FutureData();
    }

    public Data request(){
        new Thread(){
            @Override
            public void run() {
                System.out.println("start!");
                RealData realData = new RealData();
                realData.doTask();
                mFutureData.setRealData(realData);
            }
        }.start();
        return mFutureData;
    }
}

一開始會去建立假物件, 拿到假物件以後就可以使用request,
中間的Thread在request來的時候, 就去建立Real Data,
並且回傳Future Data

剩下主程式

public class Main {
    public static void main(String[] args){
        Client client = new Client();
        Data data = client.request();
        System.out.println("result = " + data.getResult());
    }
}

動作非常簡單, 只要呼叫Client物件的request, 就可以看到百分比進度開始跑直到結束,
回傳結束的字串。

start!
10%
20%
30%
40%
50%
60%
70%
80%
90%
100%
result = completed

github

Master-Worker Pattern

| Comments

Master-Worker是一個很好用的設計模式,
它可以把一個大工作分成很多小工作去執行, 等所有小工作都回來以後,
將所有的小工作結果做一個統整, 得到最終結果,
就很像專題小組有五個人, 把系統分成四等分, 每個人負責一份,
最後那個人負責統整, 會比只有一個人處理整個系統還要有效率。

從上圖可以得知, master是負責協調任務跟整合結果的中繼者,
所以我們試著寫出該設計模式。

題目: 在一個很大的字串陣列內, 找出那些字串含有"1"的元素。

解法: 先將每個陣列元素視為一個Worker處理的task, 再透過每個Worker分工找出字串, 
      結果回傳至結果區, 繼續找下一個task, 直到所有task都結束。

master程式碼

public class Master {
    protected Queue<Object> mWorkQueue;
    protected List<Thread> mThreadList;
    protected Map<String, Object> mResultMap;

    public boolean isComplete() {
        for (Thread t : mThreadList) {
            if (t.getState() != Thread.State.TERMINATED) {
                return false;
            }
        }
        return true;
    }

    public Master(Worker worker, int countWorker) {
        mWorkQueue = new ConcurrentLinkedQueue<Object>();
        mResultMap = new ConcurrentHashMap<String, Object>();
        mThreadList = new ArrayList<Thread>();

        worker.setWorkQueue(mWorkQueue);
        worker.setResultMap(mResultMap);
        for (int i = 0; i < countWorker; i++) {
            mThreadList.add(new Thread(worker, Integer.toString(i)));
        }
    }

    public void addTask(Object job) {
        mWorkQueue.add(job);
    }

    public Map<String, Object> getResult() {
        return mResultMap;
    }

    public void execute() {
        for (Thread t : mThreadList) {
            t.start();
        }
    }
}

建構子把一個Worker丟進來, 包成一個Thread放進Thread Array。

宣告一個Queue, 所有底下的工作者, 都會存取這個Queue, 由於一次只能有一個worker取出, 避免race conditiion,
因此宣告成Concurrent, 又由於先進先出的特性, 因此會宣告成Queue。

再來宣告一個一個Map, 理由同上, 由於所有工作者都會把計算好的結果丟進Map, 因此只允許一次一個Worker進行存取,
所以宣告成Concurrent, Map是用來存放所有結果的區域。

那我們會將每個要找的字串(或物件)透過addTask存放到Queue內, 等到所有工作完成, 則可以回傳Map,
因此會多宣告一個方法來進行判斷是否全部task都完成了。

Worker程式碼

public abstract class Worker implements Runnable{
    protected Queue<Object> mWorkQueue;
    protected Map<String, Object> mResultMap;

    public void setWorkQueue(Queue<Object> workQueue) {
        this.mWorkQueue = workQueue;
    }

    public void setResultMap(Map<String, Object> resultMap) {
        this.mResultMap = resultMap;
    }

    //subclass handle task

    public abstract Object handle(Object task);

    @Override
    public void run() {
        while (true) {
            Object input = mWorkQueue.poll();
            if (input == null) {
                break;
            }
            Object re = handle(input);
            mResultMap.put(Integer.toString(input.hashCode()), re);
        }
    }
}

Worker就是用來處理每個小任務的人, 一開始會有兩個方法, 一個是取得Queue, 這樣才能拿到每一個task,
另外一個方法就是取得Map, 這樣才可以把算完的結果丟回去,
那至於run就是我們要從Queue取出任務, 執行完再塞回Map的部分,
至於handle方法不知道, 因為這是一個虛擬類別, 所以真正的worker才會知道要處理怎樣的任務,
因此我們必須繼承這個類別, 並且覆寫handle這個方法。

Task的程式碼

public class MyString{
    private String str;

    private boolean isFind;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    public boolean isFind() {
        return isFind;
    }

    public void setIsFind(boolean isFind) {
        this.isFind = isFind;
    }
}

我們任務裝的物件很簡單, 就是一個字串, 以及這個結果是否有被找到。

再來就是我們實際Worker的程式碼了。

public class StringWorker extends Worker {

    private String mSearchText;

    public StringWorker(String searchText){
        mSearchText = searchText;
    }

    @Override
    public Object handle(Object task) {
        MyString strObj = new MyString();
        if(task == null){
            return strObj;
        }
        if(task instanceof MyString){
            strObj = (MyString) task;
            String s = strObj.getStr();
            if(s.indexOf(mSearchText) != -1){
                strObj.setIsFind(true);
            } else{
                strObj.setIsFind(false);
            }
        }
        return strObj;
    }
}

他覆寫了Worker類別, 在handle方法內, 將task拆解開來, 從建構子傳入要尋找的字串進行比對,
將結果塞入task的物件並且回傳。

主程式程式碼

public class Main {
    public static void main(String[] args){
        Master master = new Master(new StringWorker("1"), 5);
        for (int i = 0; i < 20; i++) {
            MyString ms = new MyString();
            ms.setStr(String.valueOf(i));
            ms.setIsFind(false);
            master.addTask(ms);
        }
        master.execute();

        while (!master.isComplete()) ;
        Map<String, Object> resultMap = master.getResult();
        for(String key : resultMap.keySet()){
            MyString ms = ((MyString) resultMap.get(key));
            System.out.println("name:" + ms.getStr());
            System.out.println("is find:" + ms.isFind());
        }
    }
}

在此先做一個小範圍的實驗, 即20筆資料即可, 利用Master建構子宣告出5個Worker,
並且希望工作者找出task內是否有"1"字串存在,
在for迴圈內, 是用來初始化所有的工作, 以index做為資料的內容,
最後將每個task透過master加入到Queue內部, 最後執行master內所有的Worker。

在我們所有任務沒有完成之前, 會讓master一直空轉在while迴圈上面,
如果是在Android建議將此部分放入到Thread, 避免user無法進行操作。

等到所有任務完成, 則把每個Task的name跟isFind都印出。

name:8
is find:false
name:13
is find:true
name:2
is find:false
name:15
is find:true
name:12
is find:true
name:1
is find:true
name:18
is find:true
name:14
is find:true
name:10
is find:true
name:5
is find:false
name:9
is find:false
name:0
is find:false
name:11
is find:true
name:3
is find:false
name:17
is find:true
name:4
is find:false
name:7
is find:false
name:19
is find:true

你會看到每個任務完成的時間不一定, 所以每次執行的結果順序也都不一致。
以上就是有效率的Master-Worker Pattern。
程式碼
github

如何使用正規表示式找出大小寫同時存在的字串

| Comments

讀書會的朋友問了一個問題

private static boolean findStr(String text, String reg){
    Pattern pattern = Pattern.compile(reg);
    Matcher matcher = pattern.matcher(text);
    while(matcher.find()) {
        String s = matcher.group();
        if(s != null){
            return true;
        }
    }
    return false;
}

上面為土法煉鋼的方法
主程式

public static void main(String[] args){
    String str = "1111";
    if(findStr(str, "[a-z]") && findStr(str, "[A-Z]") && findStr(str, "[0-9]")){
        System.out.println("pass!");
    } else{
        System.out.println("U can not pass!");
    }
}

如何使用Padding和Margin

| Comments

讀書會的朋友教我一個觀念

  • Padding
    就是裡面的框往後面退一點 不要靠近我

  • Margin
    就是外面的框 我不想靠近你 所以我往內縮一點
    而Margin的距離就離開view的控制範圍, 因此margin的touch事件會變成parent的。

如何了解Task、Thread、ThreadPool、Handler、Looper、Message、MessageQueue、AsyncTask

| Comments

Task、Thread、ThreadPool、Handler、Looper、Message、MessageQueue、AsyncTask
這邊一開始會很混亂, 試圖用一個例子來解說每個所代表的腳色,
想來想去覺得電影院蠻適合的
Task就是你要看的電影
Thread就是人
ThreadPool就是電影廳
Looper就是售票員
Message就是門票
MessageQueue就是出票機
Handler就是剪票員
AsyncTask是電影院

你想要去看電影(執行task)的話,
就要先去電影院找售票員(Looper), 售票員從出票機(MessageQueue, 一次一張循序)就會給你一張電影票(得到Message),
剪票員(Handler)檢查過你的電影票(Message)確定是這個電影廳就讓你去看電影(執行Task)。

電影院(AsyncTask)有很多電影廳(ThreadPool), 每一個電影廳可以裝很多人(Threads)看電影(執行Task),
也可以一個人(single thread)看一場電影(執行Task)。

如何使用AsyncTask

| Comments

Asynctask是一種在背景執行緒運作的非同步任務,
如果你使用它, 則必須覆寫它唯一的方法doInBackground。

class MyTask extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        return null;
    }
}

當AsyncTask執行完以後, 就不能再被執行了, 跟Thread行為相同,
如果多次執行將會丟出illegalStateException,
AsyncTask除了提供背景執行功能外, 還可以把物件傳到背景Thread,
透過AsyncTask你不必費心Hanlder傳送跟Message處理。

class MyTask extends AsyncTask<Params, Progress, Result>{
    protected void onPreExecute(){
       // in main thread

    }

    protected Result doInBackground(Params... params){
       // in background thread

    }

    protected void onProgressUpdate(Progress... progress){
       // in main thread

    }

    protected void onPostExecute(Result result){
       // in main thread

    }
 
    protected void onCancelled(Result result){
       // in main thread

    }

}
  • Params 輸入背景任務的資料
  • Progress 由Background thread向UI Thread報告進度
  • Result
    Background thread向UI Thread報告結果

  • 如果你要執行一個AsyncTask必須在Main Thread上面呼叫execute, 否則callback method將會傳不到。

如果你要取消一個AsyncTask可以這樣

AsyncTask task = new MyTask();
task.exectue(/*參數*/);
task.cancel(true);

當你呼叫execute()方法時, 則會使用預設的ThreadPool, 而預設的ThreadPool只會有5個core Thread,
因此如果超過5個task將會被放進Queue等待,
使用沒有參數的execute方法, 在3.0之後將採取循序完成,
如果想要調整, 可以選擇是否要採取循序。

//循序

executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, Object... objs);
//並行

executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, Object... objs);

另外由於預設的ThreadPool是static,
當app內其他功能使用asynctask, 則將會讓任務更晚完成。
如果使用cancel(true)方法則會發出InterruptException,
當收到取消請求時, 則不會呼叫onPostExecute方法, 改回呼onCancelled。

AsyncTask存在幾種狀態

  • PENDING 當AsyncTask建立但是卻尚未執行。
  • RUNNING 執行execute
  • FINISHED onPostExecute或onCancelled被呼叫
AsyncTask.getStatus();

可以透過上面的方法來觀察到哪個狀態。

以下兩種狀況不適合使用AsyncTask 不使用任何參數執行任務

AsyncTask<Void, Void, Void>

沒有使用參數, 背景執行緒無法跟UI執行緒溝通, 沒有任何回傳結果。

只實做doInBackground

沒有提供報告進度或結果, 就只是個背景任務。

如果有以上的情況, 請洽Thread或HandlerThread, 謝謝。

AsyncTask內塞入有Looper的Thread, 也無法傳遞訊息給該執行緒。
如有上述情況, 要注意不能任意更改該Thread的Looper, 否則會很不方便阻塞其他Thread,
如果更換Looper也會跳出RuntimeException, 真的要用Looper還是乖乖使用HandlerThread吧!
AsyncTask塞入Runnable, 這樣只是把AsyncTask當普通的Thread在使用,
優點: 如果AsyncTask內部已經存在Thread, 則會讓資源有效利用。
缺點: AsyncTask是全域環境則會干擾其他Thread運作。

如何使用ThreadPool

| Comments

HandlerHanlderThread介紹單一Thread如何運作,
如果想要讓多個Thread並行, 可以使用ThreadPool。

Java的Executor框架為Thread以及使用的資源有更進一步的控制,
Executor提供一個簡單的interface, 目標是將任務的建立跟執行分開。

public interface Executor{
    void execute(Runnable runnable);
}

public class SimpleExecutor extends Executor{
    public void execute(Runnable runnable){
        new Thread(runnable).start();
    }
}

不過一般我們不會自己設計Executor,Java提供了ThreadPoolExecutor讓我們使用, 有幾個優點:

  • Thread能保持存活, 等待新任務, 不會隨著任務建立再銷毀。
  • Thread Pool限制最大Thread數量, 避免系統浪費。
  • Thread的生命週期被Thread Pool控制。

ThreadPoolExecutor可以自行定義一些設定。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    int corePoolSize,
    int maxPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue
);
  • core pool size(核心緩衝池數量):
    Thread pool的Thread下限, Thread數量不會低於這個數字,

  • maxumum pool size(最大緩衝池數量):
    Thread pool的Thread最大數量, 如果Thread都被執行, 則Task會被塞進Queue直到有空閒的Thread出現為止。
    比較好的做法是根據底層硬體來決定數量。

int N = Runtime.getRuntime().availableProcessors();
  • keep-alive time(最大閒置時間):
    如果超過閒置時間, 則系統會回收core Thread數量以上的Thread。

  • task queue type(任務佇列類型):
    被加進來的Task, 根據策略不同, 所用的演算法也會不同。

利用allowCoreThreadTimeOut(true)這個方法, 可以在Core Thread閒置的時候, 讓系統回收。

int N = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    N,
    N * 2,
    60L,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

這邊有一個重點可能需要注意一下。
如果一開始

BlockingQueue<Runnable> rList = new LinkedBlockingQueue<Runnable>();
rList.add(new Runnable(){
    @Override
    public void run() {
        //run 1

    }    
});
rList.add(new Runnable(){
    @Override
    public void run() {
        //run 2

    }    
});
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1,
    2,
    1,
    TimeUnit.SECONDS,
    rList);

一開始Thread Pool剛建立Thread, 此時並沒有任何Thread可以執行Task,
因此所有的Task將會被丟進Queue內等待, 直到有新的Task後來又被加入,
才會連同之前等待的Task一起執行。

如果要解決這個問題可以預先建立core thread。

executor.prestartCoreThread();
//or

executor.prestartAllCoreThreads();

呼叫其中一個方法可以先建立core thread。

另外還有一個陷阱就是如果core thread設定為0,
那麼講無論如何就必須等有其他的task再被加到Thread Pool內才會一起把Queue內的task執行。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    0,
    2,
    1,
    TimeUnit.SECONDS,
    rList);

如果你不想要自建Excutor, Java有內建一些Executor。

ExecutorService fixExecutor = Executors.newFixedThreadPool(2);
ExecutorService cacheExecutor = Executors.newCachedThreadPool();
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();

分別是

  • 固定尺寸執行器(newFixedThreadPool)
    只要固定好數量, 假設是2, 那就代表Thread永遠固定是2個, 特性是用完就丟, 執行新任務會再開新的Thread。

  • 動態尺寸執行器(newCachedThreadPool)
    Thread會隨著任務多寡, 新增或刪除, 假設一個Thread被閒置60秒, 系統則會進行移除。

  • 單一執行緒執行器(newSingleThreadExecutor)
    這個執行器最多只會有一個Thread, 因此是執行緒安全的, 但是相對效率會下降, 因為Task會被阻塞。

透過Callable搭配Future可以讓我們更方便管理執行緒。

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Object> future = executor.submit(new Callable<Object>(){
        public Object call() throws Exception{
            Object obj = doLongTask();
        return obj;
    }
});

//wait future

while(future.isDone());
Object result = future.get();

與Runnable不同的是Callable可以回傳結果,
透過blocking直到long task完成, 回傳物件。

Executor提供同時多個Thread並行的操作。
InvokeAll : 同時並行多個Thread, 並且透過blocking來取回每一個Task的結果。
InvokeAny: 同時並行多個Thread, 只要有一個回傳成功, 則終止剩下的Task。

也可以利用ExecutorCompletionService來查詢所完成的任務,
他會將完成任務的結果放置BlockingQueue內, 透過polling的方式, 來查詢任務是否完成。

如何使用Thread-2

| Comments

執行緒有自己的生命周期。

  • New(新建)
new Thread(myRunnable);

尚未啟動的狀態。

  • Runnable(可執行的)
new Thread(myRunnable).start();

進入可排程系統, 只要排程器(Scheduler)指派它給CPU執行, 則會開始執行run方法。

  • Blocked/Waiting(阻塞/等待)
Thread.sleep();
Thread.yield();

當執行緒遇到別的執行緒Lock住資源, 則Thread會進入waitng或blocked的狀態。

  • Terminated(終止) 當執行緒被中斷, 或者跑完run方法內的內容後, 則會進入結束狀態, 此時的設定跟燒毀是一項繁重的工作, 如果不斷的重複這件事情, 則會浪費資源, 應該考慮以Thread Pool來實作。

一般最常遇到的是如果執行緒跑到一半想要中斷它, 應該怎麼做?
當你查看API時,你會發現Thread的stop()方法已經被標示為deprecated,使用這個方法來停止一個執行緒是不被建議的。
如果你想要停止一個執行緒,你最好自行實作。

例如給他一個旗標(flag)。

new Thread(new Runnable(){
    public void run(){
        while(flag){
            //task

        }
    }
}).start();

由上面的例子可以知道, 加入一個flag當想要中止該thread, 可以讓flag不成立, 則立即結束Thread。
又或者可以這樣做, 發出一個Exception。

Thread thread = new Thread(new Runnable(){
    public void run(){
       try{
           //task

       } catch(InterruptExcetption e){
           //handle exception

       }
    }
});
thread.start();
thread.interrupt();

如上面的例子, 當啟動Thread以後, 想要停止就呼叫Interrupt則會發出Exception來停止Thread。

除了stop()之外,suspend()、resume()方法也被標示為"deprecated",這些方法如果你要達成相同的功能,你都必須自行實作。

如果想知道Android Thread的傳遞機制可以參考如何使用Handler

如何使用Thread-1

| Comments

在Android要使用執行緒必須實作Runnable,

public class MyTask implements Runnable{
    public void run(){
        //execute your task

    }
}

然後塞到Thread呼叫start方法就可以開始執行了。

new Thread(new MyTask()).start();

Android是一個多工環境, 利用MultiThread可以讓系統效能更有效的利用,
CPU會根據排程器(Scheduler)來決定要執行哪一個Thread,
如果想要讓CPU優先執行你的任務, 則可以設定priority。

myThread.setPriority(10);

順序是1(最低)到10(最高), 一般預設值是5。
如果一個Thread優先權太低, 那麼有可能發生沒有足夠時間執行完任務, 稱之為執行緒飢餓(starvation)。
聽過一個故事, 曾經有一個系統執行了10年, 後來發現有一個Thread在系統內飢餓了10年。

CPU如果要從ThreadA切換到ThreadB執行, 稱之為本文切換(Context Switch),
Context swtich對於CPU來說是一種負擔, 因此如果開太多Thread則會造成系統上很大的負擔,
效能上也就越來越差。

太多Thread也會造成複雜度增加, 因為你會無法得知執行順序。

for(int i = 0; i < 10; i++){
    new Thread(new Runnable(){
        public void run(){
            //long time task

        }
    }).start();
}

以上的例子將會開啟10個Threas執行任務, 但是完成的時間卻是不固定的。

如果2個或以上的執行緒一起共同存取一塊資料又稱為臨界區段(Critical section),
則會造成資料上的不一致,稱之為競態條件(Race Condition)。

解決這個問題, 一般會在進入Critical section加入一把鎖(Lock),
舉個例子:
當你在使用廁所, 所以你把門鎖起來, 等你使用完畢以後, 就把門打開, 讓其他人可以使用廁所,
但是加入鎖卻會讓CPU效能降低, 因為其他的Thread必須等待你使用完才可以使用,
因此鎖就出現了許多演算法(後面再講)。

那一般JAVA要使用鎖通常都會使用synchronized這個關鍵字。

synchronized(this){
    mVar++;
}

而synchronized可以鎖物件、鎖方法、鎖變數...等。
請參考官網

在Thread上執行不會發生Race Condition稱之為執行緒安全(Thread safety)。
在某些資料結構上面, 會考慮到執行緒安全,
例如HashMap就是一個執行緒不安全的資料結構, 可以改用ConcurrentHashMap。

Thread的生命週期可以參考如何使用Thread-2

如何使用HandlerThread

| Comments

如何使用Handler中解說了Handler的運作原理, 但是他是預設在Main Thread, 如果要多開Thread來使用Hanlder, 則必須自行控制Looper,
相對上比較麻煩, 因此Android開發了HandlerThread讓你能夠方便操作。

HandlerThread整合了Thread、Looper和MessageQueue。
啟動方式跟Thread相同。

HandlerThread handlerThread = new HandlerThread("HandlerThread");
handlerThread.start();

Handler mHandler = new Hanlder(handlerThread.getLooper()){
    public void handleMessage(Message msg){
        super.handleMessage(msg);
        switch(msg.what){
        case 1:
            Logger.e("message receive");
        break;
                }
    }
}
handler.sendEmptyMessage(1);
//或者

new Hanlder(handlerThread.getLooper()).post(new Runnable(){
    public void run(){
       //長時間任務1

       //長時間任務2

    }
});

上面的例子一旦執行, 除非你手動停止, 否則該thread會一直等待執行下去。
利用Handler丟Message,
透過Looper循序message發送, 因此確保是執行緒安全的,
但是相對缺點就是效率會下降。

HandlerThread也有生命周期

  • Creation(建立): HandlerThread建構子有名稱, 或者名稱跟優先權
HandlerThread(String name)
HandlerThread(String name, int priority)
  • Execution(執行): 代表已經被start了, 隨時可以接受訊息到來。
  • Reset(重設): 你可以將Queue內的訊息清空, 除了已經送出去的Message或者正在執行的Message
mHandlerThread.removeCallbackAndMessages(null);
  • Termination(終止):
   public void stopHandlerThread(HandlerThread handlerThread){
       handlerThread.quit();
       handlerThread.interrupt();
   }
   //或者直接在handler上面

   handler.post(new Runnable(){
       public void run(){
           Looper.myLooper.quit();
       }
   });   

HandlerThread確保是循序且執行緒安全的, 有人會覺得既然如此, Thread也可以達成這樣的需求,
是沒錯, 不過這樣變成你必須在程式碼內同時寫在同一個Thread內。

   new Thread(new Runnable(){
       public void run(){
           //task 1

           //task 2

           //task 3

       }
   });

也許有人會反駁, 那麼我可以寫在另外一個Thread內啊!
也沒錯, 但是相對的你就必須開多個Thread, 這樣一來就沒有確保執行緒安全了!

  new Thread(new Runnable(){
       public void run(){
           //task 1

       }
  });
  new Thread(new Runnable(){
       public void run(){
           //task 2

       }
  });
  new Thread(new Runnable(){
       public void run(){
           //task 3

       }
  });

如上述例子 task1,task2,task3有存取到共同的資料結構, 則可能會產生concurrent的問題。
那也許會有人說(還真多人XD), 只要確保資料結構是同步的即可。
還是對的! 但是那個資料結構就必須使用Concurrent系列的, 或者自行實作synchronized,
效能相對會下降, 程式碼也會變得比較複雜。
不過使用方法必須對應到使用情境, 也不一定就是HandlerThread是萬用解藥,
畢竟它只有一個Thread, 要達到多核心必須使用多執行緒才能效能最大化,
之後可以參考如何使用ThreadPool如何使用AsyncTask

如何使用Handler

| Comments

在Android使用執行緒要非常的小心,
使用者在進行操作時, 執行緒也在進行大量運算,
會造成使用者畫面卡死不動, 這樣的使用者體驗是不好的。

Android將Main Thread用來處理UI,
因此需要使用Thread讓大量運算在背景跑, 卻不影響使用者操作的畫面,
而如果需要畫面更新, 則會透過Handler機制去更新。

執行緒處理訊息機制(Handler、Looper、Message and MessageQueue)

/*
* 一個Thread只能有一個Looper。
* 當Message處理完畢後, 會將Message發送給Handler。
*/
android.os.Looper

/*
* 一個Thread可以有多個Handler。
* 負責將Message送往MessageQueue, 並且接收Looper丟出來的Message。
*/
android.os.Handler

/*
* 一個Thread只能有一個MessageQueue。
* 負責裝載Message的佇列, 對Message進行管理, 是一個無限制的鏈結串列。
*/
android.os.MessageQueue

//執行緒上要處理的訊息。

android.os.Message


如上圖, Handler負責派送訊息, 交給MessageQueue進行排隊,
再透過Looper將每一個Message Object丟給Handler處理。

也許上面這些東西會有點陌生, 是因為Android Main Thread 一開始就先幫你綁定好了,
你不需要自訂初始化Looper並且綁定Handler,
下面的做法都是開啟Thread運算完後, 去呼叫Main Thread進行畫面更新。

new Thread(new Runnable() {
    public void run() {
        //這邊是背景thread在運作, 這邊可以處理比較長時間或大量的運算

        ((Activity) mContext).runOnUiThread(new Runnable() {
            public void run() {
                //這邊是呼叫main thread handler幫我們處理UI部分                

            }
        });
    }
}).start();
//或者


view.post(new Runnable(){
    public void run(){
        //更新畫面

    }
});

//又或者另外一種寫法

private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch(msg.what){
            case 1:
                //處理少量資訊或UI

            break;
        }
    }
};

new Thread(new Runnable(){
    public void run(){
        Message msg = new Message();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }
});

第一種寫法是實作Runnable介面讓Main Thread的Handler進行callback(請參考android的消息处理机制)
一般是用來處理大量數據或者長時間運算,
最後再利用Activity內建的方法runOnUiThread呼叫main thread handler幫忙處理UI部分。
第二種寫法是自己定義一個Message物件透過Hanlder去進行處理。

你也可以拿Main Thread的Handler來處理比較少量資料。

new Handler(mContext.getMainLooper()).post(new Runnable(){
   public void run(){
       //處理少量資訊或UI

   }
});
   

甚至你可以使用Delay的方法來延後Thread處理。

new Handler().postDelayed(new Runnable(){
   public void run(){
       //處理少量資訊或UI

   }
}, 3000);

上面是在3秒後處理少量資訊。

以上都是利用MainThread上面的Looper進行處理,
實際上你也可以自己定義自己的Looper。

new Thread(new Runnable() {
    public void run() {
        Log.e(TAG, "!!<A");
        Looper.prepare();
        new Handler().post(new Runnable(){

            @Override
            public void run() {
                Log.e(TAG, "B1");
            }
        });
        new Handler().post(new Runnable(){

            @Override
            public void run() {
                Log.e(TAG, "B2");
                Looper.myLooper().quit();
            }
        });

        Looper.loop();
                
        Log.e(TAG, "C");
        ((Activity) mContext).runOnUiThread(new Runnable() {
            public void run() {
                Log.e(TAG, "D");
            }
        });
    }
}).start();

輸出為A、B1、B2、C、D
由上面的程式碼可以看到我們自己定義的Looper,
對於Thread內的Handler會變成跟自己定義的Looper進行綁定,
也就是說這邊是屬於Background Thread部分, 不行拿來更新UI,
而直到呼叫Looper內的quit, 則會將該Looper以及MessageQueue移除, Thread才會繼續往下執行,
注意: 假設這邊如果還有Message正在處理, 則會將該Message處理完畢,
再將後面未處理的Message全部移除。
如果要更新UI則會再透過Main Thread的handler去處理UI部分。
當然你也可以使用HandlerThread

講了那麼多訊息,訊息究竟是一個怎樣的物件?

android.os.Message

只要是透過Handler派送的訊息最後都會被包成Message,
送進MessageQueue屠宰等待派送,
每一個Message都會認得把自己送進來的Handler(記仇)。

Message m = Message.obtain(handler, runnable);
m.sendToTarget();

Message有幾種參數

參數名稱    型別         用途
what       int         標記識別符號
obj        Object      物件, 必須Parcelable
data       Bundle      Bundle物件
callback   Runnable    實作Runnable的callback

以上是比較常用的, 還有一些請參考官網

參考:
Android 中的 MessageQueue 機制
android的消息处理机制

如何判斷資料夾內有相同檔案

| Comments

File dir = new File(YOUR PATH);

File[] matches = dir.listFiles(new FilenameFilter(){
    public boolean accept(File dir, String name){
        return name.startsWith("a")&&name.endsWith(".png");
    }
});
for(File f : matches){
        System.out.println(f.getName());
}

這樣就可以抓出該資料夾以a開頭、png結尾的所有檔案了

如何呈現目錄下的所有資料夾

| Comments

如果想要做一個只顯示資料夾的檔案總管, 該怎麼做?

    public ArrayList<File> filter(File[] fileList) {
    ArrayList<File> files = new ArrayList<File>();
    if(fileList == null){
        return files;
    }
    for(File file: fileList) {
        if(!file.isDirectory() && file.isHidden()) {
            continue;
        }
        files.add(file);
    }
    Collections.sort(files);
    return files;
}

就可以把所有的檔案以及資料夾全部列出來。
可以看到我們把第一層檔案陣列丟進來以後, 就可以利用file的一些屬性對這些檔案進行整理,
其中可以看到中間判斷檔案是否為資料夾以及是否為隱藏檔,
若是則不處理, 最後把抓到的file全部存成陣列回傳出去。

簡單做個範例,首先在layout內宣告一個listview

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/path"
        android:layout_width="match_parent"
        android:layout_height="30dp" />
    <View
        android:layout_below="@id/path"
        android:id="@+id/divider"
        android:background="#909090"
        android:layout_width="match_parent"
        android:layout_height="1px"/>
    <ListView
        android:layout_below="@id/divider"
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

再來就是宣告一個類別來安排我們的adapter。

    private class MyAdapter extends BaseAdapter {

    @Override
    public int getCount() {
        return files.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        Holder holder;
        if(v == null){
            v = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);
            holder = new Holder();
            holder.textView = (TextView) v.findViewById(R.id.text);
            v.setTag(holder);
        } else{
            holder = (Holder) v.getTag();
        }
        String filePath = files.get(position).getPath();
        String fileName = FilenameUtils.getName(filePath);
        holder.textView.setText(fileName);
        return v;
    }

    private class Holder{
        TextView textView;
    }
}

這邊有用到一個第三方,記得在gradle內加入

compile files('libs/commons-io-2.4.jar')

那麼我們的主程式怎麼寫呢?

        private ListView mListView;

    private MyAdapter mAdapter;

    private Context mContext;

    private ArrayList<File> files;

    private File mDir;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        mDir = Environment.getExternalStorageDirectory();
        files = filter(mDir.listFiles());

        mListView = (ListView) findViewById(R.id.list);
        mAdapter = new MyAdapter();
        mListView.setAdapter(mAdapter);
    }

這樣就可以把第一層的目錄全部抓出來。

程式碼

如何取得螢幕解析度

| Comments

因為太常用了, 所以筆記一下

    DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
    int height = metrics.heightPixels;
    int width = metrics.widthPixels;

更精確地可以參考這個
ScreenInfo