Ringster's Techblog

Tech, Mobile, Internet

Posts Tagged ‘안드로이드 개발

Volley를 이용해 Network Data 전송하기 (3) – Standard Request 생성하기

with one comment

지난 포스팅(여기)에서 Volley를 이용하여 custom RequestQueue를 생성하는 방법을 알아보았다.
이번에는 volley가 지원하는 common request 의 종류와 사용법에 대해 정리해 보자.
1) StringRequest – URL을 지정해주고 Raw string으로 된 응답을 받는다. 지난 두번의 포스팅에서 계속해서 사용한 Request이다.
2) ImageRequest – URL을 지정해주고 이미지 응답을 받는다.
3) JsonObjectRequest, JsonArrayRequest – URL을 지정해주고 JsonObject나 JsonArray를 응답으로 받는다.
App에서 사용되는 대부분의 http 요청은 위의 3가지 범주에 포함되므로 그 경우에는 단순히 해당 Request를 사용하면 되고, 그렇지 않을 경우에는 다음 포스팅에서 진행할  Custom Request 생성하기를 참조하자.


1. ImageRequest 사용하기

먼저 ImageRequest를 사용해보자.
지난번 포스팅에서 생성한 Singleton class와 StringRequest를 생성한 코드에 추가하여 ImageRequest를 생성해 보았다.

public class MainActivity extends ActionBarActivity {
	private final String url = "http://www.google.com";
	private final String imgurl = "http://www.google.co.kr/logos/doodles/2014/anna-freuds-119th-birthday-5664856720015360-hp.jpg";
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		final TextView mTextView = (TextView) findViewById(R.id.text);
		final ImageView mImageView = (ImageView) findViewById(R.id.imageView);
		
		// Get a RequestQueue
		RequestQueue queue = MySingleton.getInstance(this.getApplicationContext()).
		    getRequestQueue();
		
		// Formulate the request and handle the response.
		StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
		        new Response.Listener<String>() {
		    @Override
		    public void onResponse(String response) {
		    	mTextView.setText("Response is: "+ response.substring(0,500));
		    }
		},
		    new Response.ErrorListener() {
		        @Override
		        public void onErrorResponse(VolleyError error) {
		        	mTextView.setText(error.toString());
		    }
		});
		
		// Retrieves an image specified by the URL, displays it in the UI.
		ImageRequest imageRequest = new ImageRequest(imgurl,
		    new Response.Listener<Bitmap>() {
		        @Override
		        public void onResponse(Bitmap bitmap) {
		            mImageView.setImageBitmap(bitmap);
		        }
		    }, 0, 0, null,
		    new Response.ErrorListener() {
		        public void onErrorResponse(VolleyError error) {
		            mImageView.setImageResource(R.drawable.ic_launcher);
		        }
		    });
		// Access the RequestQueue through your singleton class.
		MySingleton.getInstance(this).addToRequestQueue(imageRequest);

		// Add a request (in this example, called stringRequest) to your RequestQueue.
		MySingleton.getInstance(this).addToRequestQueue(stringRequest);
		
	}
}

2. ImageLoader 및 NetworkImageView 이용하기

Volley에서는 ListView와 같이 view가 재사용되는 컴포넌트에서 여러장의 이미지를 효과적으로 나타내기 위해 NetworkImageView라는 imageView의 subclass를 지원한다.
ImageRequest를 viewholder에 저장하고, view를 재활용 할 시에 해당 이미지 요청들을 취소할 수도 있지만
NetworkImageView를 사용하면 해당 View가 detach 될때 알아서 요청을 취소해준다.
그럼 위에서 작성한 코드를 NetworkImageView를 이용하여 다시 작성해보자.

<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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.bitmaphandlingtest.MainActivity$PlaceholderFragment" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <com.android.volley.toolbox.NetworkImageView
            android:id="@+id/networkImageView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#000000" />

        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Test String" />

    </LinearLayout>

</RelativeLayout>

ImageView의 subclass인 만큼 사용방법은 동일하다. 이제 ImageLoader를 사용하는 방법을 알아보자.
ImageLoader는 image Loading 및 caching을 담당하는 helper class로써 다음과 같이 자체적으로 사용할 수도 있고,

ImageLoader mImageLoader;
ImageView mImageView;
// The URL for the image that is being loaded.
private static final String IMAGE_URL = "http://developer.android.com/images/training/system-ui.png";
...
mImageView = (ImageView) findViewById(R.id.regularImageView);

// Get the ImageLoader through your singleton class.
mImageLoader = MySingleton.getInstance(this).getImageLoader();
mImageLoader.get(IMAGE_URL, ImageLoader.getImageListener(mImageView,
         R.drawable.def_image, R.drawable.err_image));

아래와 같이 NetworkImageView의 인자로 넘겨주면서 사용 또한 가능하다.
그럼 위에서 작성한 xml과 imageLoader를 이용하여 코드를 수정해 보자.

public class MainActivity extends ActionBarActivity {
	private final String url = "http://www.google.com";
	private final String imgurl = "http://www.google.co.kr/logos/doodles/2014/anna-freuds-119th-birthday-5664856720015360-hp.jpg";
	ImageLoader mImageLoader;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		final TextView mTextView = (TextView) findViewById(R.id.text);
		final NetworkImageView mNetworkImageView = (NetworkImageView) findViewById(R.id.networkImageView);

		// Get a RequestQueue
		RequestQueue queue = MySingleton.getInstance(
				this.getApplicationContext()).getRequestQueue();

		// Formulate the request and handle the response.
		StringRequest stringRequest = new StringRequest(Request.Method.GET,
				url, new Response.Listener<String>() {
					@Override
					public void onResponse(String response) {
						mTextView.setText("Response is: "
								+ response.substring(0, 500));
					}
				}, new Response.ErrorListener() {
					@Override
					public void onErrorResponse(VolleyError error) {
						mTextView.setText(error.toString());
					}
				});

		// Get the ImageLoader through your singleton class.
		mImageLoader = MySingleton.getInstance(this).getImageLoader();
		mNetworkImageView.setImageUrl(imgurl, mImageLoader);

		// Add a request (in this example, called stringRequest) to your RequestQueue.
		MySingleton.getInstance(this).addToRequestQueue(stringRequest);
	}
}

NetworkImageView 및 ImageLoader를 이용하여 이미지를 네트워크 상에서 불러오는 예제가 완성되었다.
여기에서도 중요한점은 ImageLoader를 single instance로 application의 lifetime동안 존재하도록 해야한다는 것이며, 만약 imageLoader를 activity에서 생성하게되면 user가 activity를 재생성시키는 동작을 할때 (예를 들어 화면 회전) flicker를 유발하게 된다.

3. LRU cache 사용하기

Volley toolbox는 DiskBasedCache 클래스를 통해서 standard cache구현을 제공한다. 이 클래스는 지정된 디렉토리에 파일들을 캐쉬한다. 하지만 ImageLoader를 사용하기 위해서는 ImageLoader.ImageCache 인터페이스를 구현하는 custom in-memory LRU bitmap cache를 제공해야한다.

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import android.util.DisplayMetrics;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap>;
        implements ImageCache {

    public LruBitmapCache(int maxSize) {
        super(maxSize);
    }

    public LruBitmapCache(Context ctx) {
        this(getCacheSize(ctx));
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }

    // Returns a cache size equal to approximately three screens worth of images.
    public static int getCacheSize(Context ctx) {
        final DisplayMetrics displayMetrics = ctx.getResources().
                getDisplayMetrics();
        final int screenWidth = displayMetrics.widthPixels;
        final int screenHeight = displayMetrics.heightPixels;
        // 4 bytes per pixel
        final int screenBytes = screenWidth * screenHeight * 4;

        return screenBytes * 3;
    }
}

해당 코드를 Singleton class에 적용해보자.

private MySingleton(Context context) {
		mCtx = context;
		mRequestQueue = getRequestQueue();

		/* 기존 LruCache
		mImageLoader = new ImageLoader(mRequestQueue,
				new ImageLoader.ImageCache() {
					private final LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(
							20);

					@Override
					public Bitmap getBitmap(String url) {
						return cache.get(url);
					}

					@Override
					public void putBitmap(String url, Bitmap bitmap) {
						cache.put(url, bitmap);
					}
				});
				*/
                // 새로운 LruCache 초기화
		mImageLoader = new ImageLoader(mRequestQueue, new LruBitmapCache(
	            LruBitmapCache.getCacheSize(context)));
	}

4. Json Request 생성하기

Volley는 JSONArray를 위한 JsonArrayRequest와, JSONObject를 위한 JsonObjectRequest를 제공한다.
이중 JsonObjectRequest를 이용하여 Json Request를 요청하고, 해당 결과를 parsing해서 보여주는 예제를
기존 예제를 변경하여 작성해 보겠다.

public class MainActivity extends ActionBarActivity {
	private final String imgurl = "http://www.google.co.kr/logos/doodles/2014/anna-freuds-119th-birthday-5664856720015360-hp.jpg";
	private final String jsonurl = "http://pipes.yahooapis.com/pipes/pipe.run?_id=giWz8Vc33BG6rQEQo_NLYQ&_render=json";
	ImageLoader mImageLoader;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		final TextView mTextView = (TextView) findViewById(R.id.text);
		final NetworkImageView mNetworkImageView = (NetworkImageView) findViewById(R.id.networkImageView);

		// Get a RequestQueue
		RequestQueue queue = MySingleton.getInstance(
				this.getApplicationContext()).getRequestQueue();

		// Formulate the JSON request and handle the response.

		JsonObjectRequest jsObjRequest = new JsonObjectRequest(
				Request.Method.GET, jsonurl, null,
				new Response.Listener<JSONObject>() {

					@Override
					public void onResponse(JSONObject response) {
						Log.i("Response", response.toString());
						String stringFromJson = parseJSON(response);
						mTextView.setText(stringFromJson);
					}
				}, new Response.ErrorListener() {

					@Override
					public void onErrorResponse(VolleyError error) {
						mTextView.setText(error.toString());
					}
				});

		// Get the ImageLoader through your singleton class.
		mImageLoader = MySingleton.getInstance(this).getImageLoader();
		mNetworkImageView.setImageUrl(imgurl, mImageLoader);

		// Add a JsonObjectRequest to your RequestQueue.
		MySingleton.getInstance(this).addToRequestQueue(jsObjRequest);

	}

	private String parseJSON(JSONObject json) {
		String mText = "";
		try {
			JSONObject value = json.getJSONObject("value");
			JSONArray items = value.getJSONArray("items");
			for (int i = 0; i < items.length(); i++) {
				JSONObject item = items.getJSONObject(i);
				mText = "Title: "+item.optString("title")+"\n";
				mText += "Description: "+item.optString("description")+"\n";
				mText += "Link: "+item.optString("link")+"\n";
				mText += "Pub date: "+item.optString("pubDate");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return mText;
	}
}

2014-12-04-15-42-02

StringRequest와 동일하게 url을 제공하고, 받아온 response를 처리하면 된다.
Log로 올라온 source response와 좌측 Application의 TextView에 출력된 결과를 비교해보면 정상적으로 Json이 parsing되었음을 확인할 수 있다.

물론, Custom LruCache를 이용한 이미지 출력또한 정상적으로 동작하고 있다.

Written by Ringster

2014/12/04 at 6:52 am

Volley를 이용해 Network Data 전송하기 (2) – Request Queue 설정하기

leave a comment »

지난 포스팅(여기)에서 Volley를 이용하여 Network Data 전송을 하기 위해서는
1) RequestQueue 생성, 2) Request Object 생성,  3) 생성한 Object를 RequestQueue로 전달의 3단계가 필요하다고 하였다.
지난 포스팅에서는 RequestQueue를 생성하지 않고, Volley의 편의 메소드를 이용하여 RequestQueue를 생성하였는데,
이번에는 직접 RequestQueue를 생성하여 custom action을 가능하도록 하는 법을 알아보겠다.


1. Network 및 Cache 설정하기

RequestQueue는 작업을 위해 Request를 처리할 network 와 cache가 필요하다. volley는 toolbox에
one-file-per-response cache를 제공하는 DiskBasedCache와 개발자의 선택에 따라 AndroidHttpClient나
HttpURLConnection을 이용하는 BasicNetwork의 기본 구현을 제공한다. BasicNetwork는 HttpClient로
초기화되어야 하는데, 보통 HttpURLConnection이나 AndroidHttpClient가 쓰인다.

HttpURLConnection은 API Level 9 이하에서 안정적이지 않으므로 API Level 9 이하에서는 AndroidHttpClient를
사용하고, 이후에는 HttpURLConnection을 사용하도록 하려면 다음과 같이 기기가 어떤 Android Version에서
구동되는지 확인하는 코드를 이용한다.

HttpStack stack;
...
// If the device is running a version >= Gingerbread...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
    // ...use HttpURLConnection for stack.
} else {
    // ...use AndroidHttpClient for stack.
}
Network network = new BasicNetwork(stack);

 Volley에서는 기본적으로 API Level 9 이하에서는 AndroidHttpClient를 그 이후에는
HttpURLConnection을 이용한다.
그럼 Customized된 Request Queue를 이용해  SimpleRequest 전송 시와 동일한 동작을 하는 코드를 보자

public class MainActivity extends ActionBarActivity {
	private final String url = "http://www.google.com";
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		final TextView mTextView = (TextView) findViewById(R.id.text);
		
		RequestQueue mRequestQueue;

		// Instantiate the cache
		Cache cache = new DiskBasedCache(getCacheDir(), 1024 * 1024); // 1MB cap

		// Set up the network to use HttpURLConnection as the HTTP client.
		Network network = new BasicNetwork(new HurlStack());
		

		// Instantiate the RequestQueue with the cache and network.
		mRequestQueue = new RequestQueue(cache, network);

		// Start the queue
		mRequestQueue.start();

		// Formulate the request and handle the response.
		StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
		        new Response.Listener<String>() {
		    @Override
		    public void onResponse(String response) {
		    	mTextView.setText("Response is: "+ response.substring(0,500));
		    }
		},
		    new Response.ErrorListener() {
		        @Override
		        public void onErrorResponse(VolleyError error) {
		        	mTextView.setText(error.toString());
		    }
		});

		// Add the request to the RequestQueue.
		mRequestQueue.add(stringRequest);
	}
}

1회성 네트워크 작업을 할때는 RequestQueue를 생성한 후 RequestQueue.stop() 을 통해 작업을 종료시킬 수
있지만, 이러한 단순한 작업은 volley.newRequestQueue()를 이용하고 커스톰 RequestQueue는 Singleton으로
생성하여 App의 Lifetime 동안 존재하도록 구현하는 것이 일반적이다.

2. Singleton Pattern 사용하기

Application이 지속적으로 네트워크 작업을 한다면,  App의 Lifetime동안 존재할 RequestQueue 인스턴스가 있는
것이 효과적이다. 이는 여러가지 접근 방법을 통해 구현 가능한데 하나의 Singleton class를 생성하여
RequestQueue 및 사용할 volley function 들을 캡슐화하거나, Application의 Subclass를 생성하여 Application의
onCreate()메소드에서 requestQueue를 설정하면 된다.
두번째 방법보다는 첫번째 방법이  S/W의 모듈화에 좋으므로 첫번째 방법을 사용하기로 하며,
요점은 RequestQueue를 Activity context가 아닌 Application의 context를 통해 초기화하여
계속해서 재생성되지 않도록 하는 것이다.

 RequestQueue와 ImageLoader를 제공하는 Singleton Class를 생성해보자. (ImageLoader는 다음편에 설명)

public class MySingleton {
    private static MySingleton mInstance;
    private RequestQueue mRequestQueue;
    private ImageLoader mImageLoader;
    private static Context mCtx;

    private MySingleton(Context context) {
        mCtx = context;
        mRequestQueue = getRequestQueue();

        mImageLoader = new ImageLoader(mRequestQueue,
                new ImageLoader.ImageCache() {
            private final LruCache<String, Bitmap>
                    cache = new LruCache<String, Bitmap>(20);

            @Override
            public Bitmap getBitmap(String url) {
                return cache.get(url);
            }

            @Override
            public void putBitmap(String url, Bitmap bitmap) {
                cache.put(url, bitmap);
            }
        });
    }

    public static synchronized MySingleton getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new MySingleton(context);
        }
        return mInstance;
    }

    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            // getApplicationContext() is key, it keeps you from leaking the
            // Activity or BroadcastReceiver if someone passes one in.
            mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
        }
        return mRequestQueue;
    }

    public <T> void addToRequestQueue(Request <T> req) {
        getRequestQueue().add(req);
    }

    public ImageLoader getImageLoader() {
        return mImageLoader;
    }
}

이제 작성한 Singleton Class를 이용해 위해서 한 작업과 동일한 코드를 작성해보자.

public class MainActivity extends ActionBarActivity {
	private final String url = "http://www.google.com";
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		final TextView mTextView = (TextView) findViewById(R.id.text);
		
		// Get a RequestQueue
		RequestQueue queue = MySingleton.getInstance(this.getApplicationContext()).
		    getRequestQueue();
		
		// Formulate the request and handle the response.
		StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
		        new Response.Listener<String>() {
		    @Override
		    public void onResponse(String response) {
		    	mTextView.setText("Response is: "+ response.substring(0,500));
		    }
		},
		    new Response.ErrorListener() {
		        @Override
		        public void onErrorResponse(VolleyError error) {
		        	mTextView.setText(error.toString());
		    }
		});

		// Add a request (in this example, called stringRequest) to your RequestQueue.
		MySingleton.getInstance(this).addToRequestQueue(stringRequest);	
	}
}

이로서 Singleton pattern을 이용해 위에서 작성한 코드와 동일한 동작을 구현하였다.
다음 포스팅에서는 지금까지 사용해왔던 StringRequest를 비롯하여 ImageRequest, JSONObjectRequest 등
Standard Request를 사용하는 법을 알아보고, Singleton class에 추가한 ImageLoader의 사용법 또한 알아보겠다.

Written by Ringster

2014/12/03 at 11:03 am

Volley를 이용해 Network Data 전송하기 (1) – Simple Request 전송

leave a comment »

안드로이드 App을 제작하다보면 이미지 파일 업로드/다운로드, Json 형식의 응답 주고 받기 등
네트워크를 통한 데이터 전송을 빈번하게 사용한다. 이를 위해 가장 많이 쓰이는 방법은 AsyncTask를 생성하여
DoitBackground 메소드 내에서 네트워크 작업을 처리하는 것인데, 이러한 작업들을 좀더 쉽고 빠르게 하기 위해
Google은 2013년 Google I/O에서 volley라는 Http 라이브러리를 공개했다.

Google Volley에 대한 자세한 설명은 Naver의 정상혁 님이 쓰신 글(여기)이나
Google I/O 발표자료 (110 – Volley- Easy,Fast Networking for Android)를 참조하기로 하고,
Google 개발자 튜토리얼(여기)을 참조하여 Volley를 이용해 네트워크 데이터를 처리하는 방법을 알아보도록 하자.


1. Volley Library Download 받기 및 프로젝트에 추가하여 사용준비 하기

먼저 다음 명령어를 실행하여 git repository에서 volley를 다운로드 받는다.

git clone https://android.googlesource.com/platform/frameworks/volley

다음 명령어를 실행하여 build 환경을 재설정하고,

/Applications/adt-bundle-mac-x86_64-20140321/sdk/tools/android update project -p .

volley를 다운받은 폴더에서 lib파일을 생성한다.

/Applications/apache-ant-1.9.4/bin/ant jar;

그 이후 Project의 libs 폴더 안에 생성된 volley.jar파일을 붙여넣는다. (Eclipse 기준)

마지막으로 AndroidMenifest.xml 파일에 android.permission.INTERNET 퍼미션을 추가한다.

2. newRequestQueue 사용하기

Volley를 사용하는 방법은 크게 3단계로 나뉜다.
1) RequestQueue를 생성한다. 2) Request Object를 생성한다. 3) 생성한 Object를 RequestQueue로 넘긴다.
이번 포스팅에서는 RequestQueue를 생성하는 편의 메소드인 newRequestQueue를 이용하여
간단한 네트워크 데이터 전송을 수행하는 방법을 알아보도록 하자.

final TextView mTextView = (TextView) findViewById(R.id.text);
...

// Instantiate the RequestQueue. 
// 1) 편의 메소드를 이용하여 RequestQueue 생성 및 초기화
RequestQueue queue = Volley.newRequestQueue(this);
String url = "http://www.google.com";

// Request a string response from the provided URL. 
// 2) Request Obejct인 StringRequest 생성
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
            new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        // Display the first 500 characters of the response string.
        mTextView.setText("Response is: "+ response.substring(0,500));
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        mTextView.setText("That didn't work!");
    }
});
// 3) 생성한 StringRequest를 RequestQueue에 추가
queue.add(stringRequest);

Volley를 이용한 네트워크 응답은 언제나 main Thread로 전송되므로
response Handler를 통해 바로 Main UI에 접근할 수 있다.

add() 메소드를 수행하면 volley는 하나의 cache thread와 network dispatch thread pool을 실행시키고,
cache dispatcher를 통해 cache가 이용 가능한 Request는 cache 쓰레드로 보내고, 그렇지 않은 Request는
network thread로 보내 round robin 방식으로 처리한다.
처리된 결과는 쓰레드에 상관없이 모두 main Thread로 보내진다 (구조도 보기)

2. Request 취소하기

Request를 취소하기 위해서는 생성한 Request Object에 대하여 cancel() 메소드를 수행하면 된다.
volley는 cancel() 된 Object의 Response Handler가 수행되지 않음을 보장한다.
이는 Activity의 onStop() 메소드에서 모든 pending Request를 취소할 수 있고 onSaveInstanceState()가
호출되었든지 아니든지 간에 response Handler에서 getActivity()==null 체크나 기타 방어코드를 사용해가며
노력을 낭비할 필요가 없음을 의미한다.

이러한 장점을 얻기위해서는 모든 request를 적절한 시기에 취소하기 위해 추적해야 하는데,
이를 위한 간단한 방법이 있다.

바로 tag를 이용하여 모든 request를 tagging 하고 그 request의 적절한 취소 시기에 requestQueue.cancelAll(this)
메소드를 호출하는 것이다. 예를 들어서 다음과 같이 모든 request에 Tag를 추가하고,

public static final String TAG = "MyTag";
StringRequest stringRequest; // Assume this exists.
RequestQueue mRequestQueue;  // Assume this exists.

// Set the tag on the request.
stringRequest.setTag(TAG);

// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);

Activity의 onStop 메소드에서 해당 Tag를 이용하여 모든 Request를 취소하는 것이다

@Override
protected void onStop () {
    super.onStop();
    if (mRequestQueue != null) {
        mRequestQueue.cancelAll(TAG);
    }
}

이와 유사하게 ViewPager 는 page가 swipe될때 mRequestQueue.cancelAll(this)를 호출하여
기존 Request들을 취소하고 새로운 Request를 추가할 수 있다.

Written by Ringster

2014/12/03 at 8:51 am

Bitmap을 UI Thread 외부에서 처리하기 (2) – ListView, GridView

leave a comment »

지난번에 포스팅한 Bitmap을 UI Thread 외부에서 처리하기 (1) (여기)에 이어서 이번에는 Bitmap을 ListView와 GridView 등에서 다루는 법을 포스팅한다.. 이 역시 Google 개발자 튜토리얼(여기)를 참조하여 작성하였고,  지난번 포스팅과 내용이 연결되므로 기존 포스팅을 먼저 읽은 후 읽기를 추천한다.


App 제작 시 흔히 쓰이는 ListView나 GridView의 경우  AsyncTask와 함께 사용 시 새로운 문제를 발생시킨다. 이들 컴포넌트들은 메모리의 효율적 사용을 위해 유저 scroll이 있을 경우 child view 들을 재사용하는데 이 때문에 각각의 child view가 AysncTask를 시작할 경우 해당 task가 종료되었을 때 그 child view가 이미 재사용되었을 수도 있고, 또한 async task가 시작된 순서대로 끝나는 것도 보장하지 않는다.

1. Dedicated Drawable subclass 생성하기

이 문제를 해결하기 위해 기존에 생성한 BitmapWorker Task로부터 reference를 저장할 수 있는 subclass를 하나 생성한다. 여기서 BitmapDrawable은 task 가 종료되기까지 출력될 임시 이미지를 보여주기 위해 사용된다.

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

2. ImageView와 AsyncDrawable class binding 하기

BitmapWorkerTask를 execute하기 전에 AsyncDrawable 인스턴스를 생성하여 Bitmap을 보여줄 imageView와 묶는다

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable); // imageView에 AsyncDrawable을 묶어줌
        task.execute(resId);
    }
}

cancelPotentialWork 메소드는 imageView가 이미 running task와 연관되있는지 확인한다. 만약 연결되어있다면 cancel() 메소드 호출을 통해 이전 task를 종료시킨다. cancelPotentialWork는 다음과 같다

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == 0 || bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

cancelPotentialWork 메소드에서는 imageView와 연관된 task를 가져오기위해 getBitmapWorkerTask라는 help 메소드가 사용되었다. getBitmapWorkerTask 메소드는 imageView와 imageview의  drawable이 null이 아닐 경우 Workertask를 반환한다.

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) { //drawable 이 null일 경우 false 반환
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

3.  기존 BitmapWorker Task의 onPostExecute 업데이트 하기

마지막으로 task가 cancel되었는지 혹은 이미 imageView와 연계되어 있는지를 확인하기 위한 코드를 추가하여 onPostExecute를 업데이트 한다.

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

이로써 ListView나 GridView와 같이 view를 재활용하는 컴포넌트에서도 loadBitmap 메소드가 문제없이 동작할 수 있게 되었다. 해당 컴포넌트들의 adapter 구현 시 getView() 메소드 내에서 간단히 loadBitmap을 호출하기만 하면 된다.

Written by Ringster

2014/11/27 at 10:14 am

Bitmap을 UI Thread 외부에서 처리하기 (1) – 일반 View

leave a comment »

지난번에 포스팅한 ImageView에 대용량 Bitmap 효과적으로 로딩하기(여기)에 이어서 이번에는 Bitmap을 UI Thread 외부에서 처리하는 법에 대해 정리해 보았다. 이 역시 Google 개발자 튜토리얼(여기)를 참조하여 작성하였고,  지난번 포스팅과 내용이 연결되므로 기존 포스팅을 먼저 읽은 후 읽기를 추천한다.


만약 Bitmap 이미지의 리소스를 메모리가 아닌 네트워크나, 디스크를 통해서 가져와야 한다면 해당 이미지를 절대 main Thread(UI Thread)에서 가져와서는 안된다. 이는 앱의 반응성을 늦추고, 심한 경우 시스템은 앱이 반응하지 않는다고 판단하여 ANR(Android Not Responding) 메시지를 띄우고 앱을 죽여버린다.

이번 포스팅에서는 이를 방지하기 위해 AsyncTask를 이용하여 background thread에서 작업을 실행한 후 작업 결과를 UI Thread로 돌려주는 방법에 대해 정리한다.

1. AsyncTask 생성하기

AsyncTask가 받아들이는 세 인자는 <Params, Progress, Result>의 순이다. Params의 경우 doInBackground 메소드를 통해 background 작업을 할때 넘겨줄 반환형을, Progress의 경우 onProgressUpdate 메소드를 통해 백그라운드 작업의 진행상황을 출력할 때 필요한 반환형을 Result의 경우 onPostExecute 메소드에서 백그라운드 작업이 끝난 후 결과를 돌려줄 반환형을 의미한다. 자세한 내용은 Android developer 사이트의 (여기)를 참조하자.

 백그라운드에서 이미지 작업을 실행하고, 그 작업한 이미지를 UI 쓰레드에서 출력할 것이므로 인자는 Integer, Void, Bitmap 순으로 넘겨주었고, doInBackground 메소드 안에서는 지난번 포스팅때 작성한 decodeSampledBitmapFromResource 메소드를 사용하였다.

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

여기서 주의할 점은 WeakReference로 선언된 ImageView가 Asynctask가 imageView를 Garbage collecting을 방지하는 것을 막으므로(imageview가 Garbage collecting 되게끔 보장하므로) onPostExecute시에 imageView가 존재하는지 확인해줘야 한다. (유저가 Activity를 떠나거나 configuration change등이 발생할 가능성이 있음.)

2. AsyncTask 사용하기

1번의 과정을 통해 AsyncTask를 작성하였다면 새로운 Task를 생성하고 execute(Params…params)를 통해 실행시키면 된다.

여기서는 loadBitmap이라는 메소드를 작성하여 Task를 생성하고 실행하였다.

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

이 메소드의 호출을 통해서 이미지를 UI Thread에서 벗어나 비동기적으로 출력할 수 있다.

Written by Ringster

2014/11/27 at 8:57 am

ImageView에 대용량 Bitmap 효과적으로 로딩하기

with 6 comments

하나의 안드로이드 앱에서 다룰 수 있는 메모리의 한계 때문에 이미지를 처리하다보면 OutofMemory exception이나 퍼포먼스 문제에 부딪치기 쉽다. 또한 이미지를 처리하는 도중 상대적으로 작은 뷰의 크기에 맞지 않는 고해상도의 이미지를 불러오는 것은 괜한 리소스를 낭비하게 만든다.
이 포스팅은 Google 개발자 튜토리얼(여기)을 참조하여 효과적으로 비트맵을 핸들링 하는 방법에 대해 정리해 보았다.

1. inJustDecodeBounds 설정하기

BitmapFactory class는 여러가지 리소스로부터 Bitmap 이미지를 만들어내기 위한 여러 decoding 메소드를 제공한다.
이중 options.inJustDecodeBounds를 true로 설정하면 이미지를 decoding할때 이미지의 크기만을 먼저 불러와 OutofMemory Exception을 일으킬만한 큰 이미지를 불러오더라도 선처리를 가능하도록 해준다.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options); // inJustDecodeBounds 설정을 해주지 않으면 이부분에서 큰 이미지가 들어올 경우 outofmemory Exception이 발생한다.
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

2. Scaled Down된 버전의 이미지 메모리에 로딩하기

1번의 과정을 통해 이미지의 크기를 알게되었다면 이제 적절한 크기로 이미지를 리사이징할 차례이다.
inSampleSize 설정을 통해 이미지를 작은 사이즈로 리사이즈 할 수 있다.
inSampleSize는 픽셀수 기준으로 1/inSampleSize 크기로 이미지를 줄여준다. (가로,세로 기준)

리사이징을 위한 decoder가 inSampleSize를 2의 배수에 가까운 수로 버림하여 계산하기 때문에 2의 배수로 값을 계산하도록 하고, 선처리된 높이와 폭을 바탕으로 이미지를 표시할 뷰의 크기보다 작지 않은 크기로 가장 큰 inSampleSize값을 산출한다.

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;
}

3.이미지 전처리 메소드 생성하기

이제 1, 2번 과정을 통합하여 출력하고자 하는 이미지 뷰에 표시할 차례이다.
1번 과정과 2번 메소드를 통합한 decodeSampledBitmapFromResource 메소드를 생성한다.
해당 메소드는 어떠한 크기의 이미지가 소스로 들어오더라도 outofmemory exception 을 발생시키지 않는다.

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

4. ImageView에 출력하기

이제 3번에서 만든 메소드를 이용해 이미지 비트맵을 출력해주면 된다.

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

바로 setImageBitmap을 사용했을 때 outofmemory exception이 발생하는 이미지에 대해서도
decodeSampledBitmapFromResource 메소드를 통해서는 적절하게 리사이징해서 출력함을 확인할 수 있다.

Written by Ringster

2014/11/27 at 7:37 am

Yurim Jin, Programmer&Designer

Fork my brain because I'm ready to commit

jamesjungkuelee's biotech story

Biotech, entrepreneur, life

Communications as Ikor

기업 위기관리의 모든 것

Charles Pyo Ventures

시도와 실패, 성장의 기록. 2막에서도 계속되는 모험들.

techNeedle 테크니들

글로벌 테크 소식을 인사이트와 함께 전달합니다

Open API, Cloud, DevOps 와 eBook

Open API, eBook, Cloud, DevOps

Economics of almost everything

Tech, Mobile, Internet

cylee

Tech, Mobile, Internet

gorekun.log

고어쿤로그

Google Developers Korea Blog

Tech, Mobile, Internet

Android Developers Blog

Tech, Mobile, Internet

최피디의 앱스 개발기

기술, 앱스, SNS, 창업

D2 Blog

Tech, Mobile, Internet

All of Software

Tech, Mobile, Internet

'Startup's Story Platform’

'Startup's Story Platform’