레이블이 성능향상인 게시물을 표시합니다. 모든 게시물 표시
레이블이 성능향상인 게시물을 표시합니다. 모든 게시물 표시

2018년 4월 17일 화요일

[안드로이드] Architecture Component 3 - Paging Library 공식문서 번역

Paging Library

페이징 라이브러리는 앱이 데이터 소스로부터 필요한 정보를 점진적으로 읽어오는 작업을 쉽게 만들어주는 라이브러리로, 디바이스에 과부하가 걸리거나, 큰 데이터베이스로부터의 쿼리를 기다리지 않도록 만들어준다.

개요

대부분의 앱들은 많은 양의 데이터를 이용하여 동작하지만, 사실은 화면에 보여야 할 필요가 있는 일부분의 데이터만 불러와서 보여지면 된다. 예를들어, 어떤 앱에서 잠재적으로 보여줄 수 있는 데이터를 몇천개 가지고 있더라도, 한번에 몇십개씩만 엑세스하면 된다. 이부분을 앱이 신경쓰고 있지 않다면, 앱에서 불필요한 데이터까지 모두 요청하게 되고, 이는 불필요한 네트워크 트래픽의 발생과, 디바이스의 성능저하를 초래한다. 데이터가 원격저장소에 저장되거나 동기화되면, 이 또한 앱이 느려지는 원인이 되고, 사용자의 데이터요금을 낭비하게 된다.
기존의 Android API는 콘텐츠의 페이징을 허용했지만 중요한 제약 조건과 단점이 있었다.
  • CursorAdapter는 데이터베이스의 쿼리결과를 ListView의 항목들에 쉽게 매핑시켜주었지만, 데이터베이스 쿼리를 UI 스레드에서 하는것과 Cursor를 이용하기 때문에 데이터의 페이징을 비효율적으로 하는 단점이었다. 더 많은 정보를 확인하려면 Large Database Queries on Android 블로그 글을 확인한다.
  • AsyncListUtil은 RecyclerView에서 포지션 기반의 페이징을 허용하지만, 포지션 기반이 아닌 페이징은 허용하지 않는다. 또한, 셀수 있는 데이터셋에서 null을 플레이스 홀더로 강제로 사용한다.
새로운 페이징 라이브러리는 이러한 문제들을 해결한다. 이 라이브러리는 필요한 데이터를 요청하는 프로세스를 간소화하는 몇몇 클래스들을 포함한다. 이 클래스들은 Room과 같은 기존의 아키텍쳐 컴포넌트 라이브러리들과도 원활하게 잘 동작하도록 구성되어 있다.

기능

페이징 라이브러리의 클래스들을 사용하면, 이 섹션에서 소개되는 작업들을 수행하도록 도와준다.

데이터를 어떻게 가져오는지 정의하기

DataSource클래스를 이용하여 페이징된 데이터를 얻어올 데이터 소스를 정의한다. 데이터 접근 방법에 따라, 이 클래스를 상속받아 서브클래스로 구성할 수도 있다.
  • 로드할 데이터가 다음 / 이전 키를 포함하고 있다면, PageKeyedDataSource 클래스를 사용한다. 예를들어, 네트워크로부터 소셜미디어의 게시물을 가져올 경우, 연속적인 다음페이지를 로드하기 위해서, 다음페이지의 토큰을 전달해야 할 수도 있다.
  • 항목 N의 데이터를 이용해 N+1의 데이터를 가져와야 한다면, ItemKeyedDataSource클래스를 사용한다. 예를들어, 스레드된 코멘트들을 가져와야 한다면, 다음 코멘트를 가져오기 위해, 하나의 코멘트의 아이디를 전달해야 할 수도 있다.
  • 데이터 저장소에서 선택한 위치로부터 데이터의 페이지를 가져와야한다면, PositionalDataSource클래스를 사용한다. 이 클래스는 "1200번 위치로부터 20개의 데이터 반환"과 같이, 선택된 위치로부터 시작하는 데이터의 집합을 요청할 수 있다.
만약 Room 라이브러리를 사용하여 데이터를 관리하고 있다면, 이 Room 라이브러리는 자동적으로 PositionalDataSource인스턴스를 생성하기 위해 DataSource.Factory를 발생시킬 수 있다.
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);

메모리로부터 데이터 로드

PagedList클래스는 DataSource로부터 데이터를 불러옵니다. 개발자는 데이터가 한번에 얼마나 로드가 되어야 하고, 미리 패치되어야 하는지 설정하여, 사용자가 데이터로드가 완료될 때까지 기다려야 하는 시간을 최소화할 수 있다. 이 클래스는 RecyclerView.Adapter와 같은 다른 클래스에게 업데이트 신호를 제공하여, 페이지가 로드되었을 때 RecyclerView의 컨텐츠를 업데이트하도록 도와준다.
앱의 구조에 따라, PagedList 클래스 사용을 위한 몇몇 옵션들이 있다. 더 많은 내용을 보려면, 다음의 데이터 로딩 구조를 참고한다.

UI에 데이터 나타내기

PagedListAdapter 클래스는 PagedList로부터 데이터를 UI에 나타내기 위한 RecyclerView.Adapter의 구현체이다. 예를들어, 새로운 페이지가 로드되었을 때, PagedListAdapter는 데이터가 갱신되었음을 RecyclerView에게 알려준다. 이는 RecyclerView가 실제 항목으로 플레이스홀더를 대체하도록 하여 적절한 애니메이션을 수행할 수 있다.
또한 PagedListAdapter는 백그라운드 스레드를 사용하여 한 PagedList에서 다른 PagedList 로의 변경 사항을 계산하고 (예 : 데이터베이스 변경이 업데이트 된 데이터로 새 PagedList를 생성하는 경우) 필요에 따라 목록 내용을 업데이트하기 위해 notifyItem ... () 메소드를 호출한다. 그리고나서 RecyclerView는 필요한 변화를 수행한다. 예를들어, PagedList 버전사이에 항목들의 위치가 변경되면, RecyclerView는 해당 항목을 새로운 위치로 애니메이션으로 움직인다.

데이터 갱신 관찰하기

페이징 라이브러리는 실시간 업데이트가 가능한 PagedList 컨테이너를 생성하기 위한 클래스들을 제공하고 있다.
  • LivePageListBuilder : 이 클래스는 개발자가 제공한 DataSource.Factory를 통해 LiveData<PagedList>를 생성한다. 다음의 샘플코드에서 보는것처럼, 만약 데이터베이스를 관리하기 위해 Room 라이브러리를 사용한다면, DAO는 PositionalDataSource를 사용하여 DataSource.Factory를 생성할 수 있다.
LiveData<PagedList<Item>> pagedItems =
        LivePagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchExecutor(myNetworkExecutor)
                .build();
  • RxpagedListBuilder : 이 클래스는 LivepagedListBuilder와 유사하게, RxJava2 기반의 기능을 제공한다. 이 클래스는 아키텍쳐 라이브러리의 RxJava2(android.arch.paging:rxjava2:1.0.0-alpha1)기반으로 제공된다. 다음의 코드에서 볼 수 있듯이, 이 클래스를 사용하여, PagedList를 구현할 때 Flowable과 Observable을 생성할 수 있다.
Flowable<PagedList<Item>> pagedItems =
        RxPagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchScheduler(myNetworkScheduler)
                .buildFlowable(BackpressureStrategy.LATEST);

데이터 흐름 생성하기

페이징 라이브러리의 컴포넌트는 백그라운드 스레드의 생산자로부터 UI스레드의 화면 출력까지의 데이터 흐름을 구성한다. 예를들어, 새로운 항목이 데이터베이스에 추가되면, DataSource는 갱신될 것이고, LiveData<PagedList> / Flowable<PagedList> 는 백그라운드 스레드에서 새로운 PagedList를 생산한다.
paging-threading.gif
Figure1 : 페이징 라이브러 컴포넌트들은 대부분의 작업을 백그라운드 스레드에서 수행한다. 그래서 UI스레드에 큰 부담이 되지 않는다.
새로 생성된 PagedList는 UI스레드에서 PagedListAdapter로 보내진다. PagedListAdapter는 백그라운드 스레드에서 DiffUtil을 이용하여, 기존의 리스트와 새로운 리스트 사이의 차이를 계산한다. 비교가 끝났으면, PagedListAdapter는 리스트의 차이점을 이용하여 RecyclerView.Adapter.notifyItemInserted() 를 호출하여 UI에 항목들을 삽입한다.
UI스레드의 RecyclerView는 삽입된 아이템을 확인하여 적절하게 애니메이셔닝 한다.

데이터베이스 예제

다음의 코드조각들은 모든 라이브러리들이 함께 동작하도록 하는 몇몇 방법을 보여준다.

LiveData를 이용하여 페이징된 데이터 관찰하기

다음의 코드는 모든 컴포넌트들이 함께 동작하는 것을 보여준다. 사용자가 데이터베이스에 추가, 삭제, 변경할 때, RecyclerView의 내용은 자동적으로, 효율적으로 갱신된다.
@Dao
interface UserDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract DataSource.Factory<Integer, User> usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = new LivePagedListBuilder<>(
                userDao.usersByLastName(), /* page size */ 20).build();
    }
}

class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        mAdapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList ->
                mAdapter.submitList(pagedList));
        recyclerView.setAdapter(mAdapter);
    }
}

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<User>() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}

RxJava2를 이용하여 페이징된 데이터 관찰하기

LiveData대신 RxJava2를 사용하려고 한다면, Observable이나 Flowable 객체를 생성한다.
class MyViewModel extends ViewModel {

    public final Flowable<PagedList<User>> usersList;

    public MyViewModel(UserDao userDao) {
        usersList = new RxPagedListBuilder<>(userDao.usersByLastName(),
                /* page size */ 50).buildFlowable(BackpressureStrategy.LATEST);
    }
}
그리고, 다음의 코드와 같이 해당 데이터의 관찰을 시작 / 중지할 수 있다.
class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;
    private final CompositeDisposable mDisposable = new CompositeDisposable();

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        mAdapter = new UserAdapter();
        recyclerView.setAdapter(mAdapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        myDisposable.add(mViewModel.usersList.subscribe(flowableList ->
                mAdapter.submitList(flowableList)));
    }

    @Override
    protected void onStop() {
        super.onStop();
        mDisposable.clear();
    }
}
UserDaoUserAdapter 클래스의 코드는 LiveData의 예제와 같으므로 생략한다.

데이터 로딩 구조 선택하기

페이징 라이브러리를 이용해 데이터를 페이징하는 두가지의 기본적인 방법이 있다.

네트워크 or 데이터베이스

첫번째로, 하나의 소스(로컬저장소이든 네트워크이든)로부터 페이징을 할 수 있다. 이 경우 다음 샘플과 같이 LiveData를 이용하여 로드된 데이터를 UI에 전달한다.
데이터의 소스를 지정하려면 LivePagedListBuilder에 DataSource.Factory를 전달한다.
paging-network-or-database.png
Figure2 : 하나의 데이터소스는 DataSource.Factory를 제공하여 컨텐츠를 로드한다.
데이터베이스를 관찰할 때, 컨텐츠의 변화가 발생하면, 데이터베이스는 새로운 PagedList에 푸쉬한다. 네트워크 페이징인 경우에(백엔드가 업데이트를 보내지 않을 때), 스와이프 리프레시 같은 시그널은 현재 데이터소스를 무효화함으로써 새로운 PagedList를 받아올 수 있다. 이는 모든 데이터를 비동기적으로 새로고침한다.
PagingWithNetworkSample의 메모리 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크 DataSource.Factory를 구현했는지를 보여준다.

네트워크 and 데이터베이스

두번째로, 로컬저장소로부터 데이터를 페이지할 수 있고, 로컬저장소는 네트워크로부터 추가 데이터를 페이징한다. 이는 종종 네트워크 연결을 최소화하고, 백엔드의 캐시로 데이터베이스를 사용하여 더 나은 연결지양 경험을 제공한다.
이 경우, LiveData를 이용하여 데이터베이스로부터 컨텐츠를 페이징하고, 최신 데이터의 시그널을 관찰하기 위해, LivePagedListBuilder에 BoundaryCallback을 전달한다.
paging-network-plus-database.png
Figure3 : 데이터베이스는 네트워크 데이터의 캐시로 이용된다. UI는 데이터베이스로부터 데이터를 로드하고, 데이터가 없을 때, 네트워크로 신호를 보내서 네트워크로부터 데이터베이스로 데이터를 로드한다.
이후, 이 콜백을 네트워크 요청에 연결하면 데이터가 데이터베이스에 바로 저장된다. UI는 데이터베이스의 갱신을 구독하고 있으므로, 새로운 데이터의 변경은 자동적으로 UI로 전달된다.
PagingWithNetworkSample의 데이터베이스 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크의 BoundaryCallback을 구현하는지 보여준다.

원문 : https://developer.android.com/topic/libraries/architecture/paging.html


2017년 10월 22일 일요일

[안드로이드] 스레드2 - 안드로이드에서의 스레드 기초

스레드 통신

파이프

특징

  • 자바에서 제공하는 java.io 패키지의 API. 블로킹 IO를 지원한다.
  • 같은 프로세스 내의 두 스레드간의 단방향 데이터 전송을 지원
    • 리눅스의 파이프 통신 : 프로세스간 통신
    • 자바의 파이프 통신 : 동일 프로세스내의 스레드간 통신
  • 일반적인 사용케이스 : 하나의 태스크에서 다른 태스크로 계속해서 데이터를 옮길 때
  • 데이터 전송 유형
    • Binary : PipedInputStream / PipedOutputStream
    • String : PipedReader / PipedWriter
  • 파이프의 수명
    • 연결설정(connect()) : 시작
    • 연결종료(close()) : 종료

예제

데이터 생산자(UI 스레드)

  • EditText로부터 글자를 입력받아서, 처리할 스레드로 전송
public class TestActivity extends Activity implements TextWatcher {
    private EditText editText;
    private PipedWriter writer;
    private TextHandlerThread workerThread;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentVie(R.layout.activity_test);
        editText = (EditText) findViewById(R.id.edit);
        editText.addTextChangedListener(this);
        
        initializeConnection();
    }
    
    private void initializeConnection() {
        writer = new PipedWriter();
        workerThread = new TextHandlerThread();
        try {
            workerThread.connect(writer);
            workerThread.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        
        workerThread.interrupt();
        try {
            writer.close();
            workerThread.disconnect();
        } catch (IOExcepton e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        //추가되는 문자만 처리
        try {
            if (count > before) {
                writer.write(s.subSequence(before, count).toString());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void afterTextChanged(Editable s) {
    }
}

데이터 소비자(Worker 스레드)

  • UI 스레드로부터 전달받은 글자를 처리
public class TextHandlerThread extends Thread {
    private PipedReader reader;
    
    public TextHandlerThread() {
        reader = new PipedReader();
    }
    
    public void connect(PipedWriter writer) throws IOException {
        writer.connect(reader);
    }
    
    public void disconnect() throws IOException {
        reader.close();
    }
    
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                int i;
                while ((i = reader.read()) != -1) {
                    char c = (char) i;
                    //TODO : 문자처리 로직 추가
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

공유메모리

  • 공유 메모리 : 같은 프로세스내의 모든 스레드가 접근할 수 있는 메모리 영역(Heap 영역)
    • 인스턴스 멤버변수
    • 스태틱 멤버변수
    • 객체
  • 공유메모리를 이용해 스레드간 데이터 Read/Write - 각 스레드간의 동기화 문제가 발생할 여지가 있다!
    • 시그널링을 통해 해결

시그널링

  • 시그널링 : 특정 스레드의 상태를 다른 스레드로 전파
  • notify() / signal() - 무작위로 하나의 메서드만 꺠움
  • notifyAll() / signalAll() - 기다리는 모든 스레드를 깨움
동기화타입synchronizedReentrantLockReentrantReadWriteLock
Block & WaitObject.wait()
Object.wait(timeout)
Condition.await()
Condition.await(timeout)
Condition.await()
Condition.await(timeout)
NotifyObject.notify()
Object.notifyAll()
Condition.signal()
Condition.signalAll()
Condition.signal()
Condition.signalAll()
  • 안드로이드의 UI스레드에서는 사용할 수 없다.
    • UI스레드가 block 상태로 지속되면, ANR 발생

블로킹 큐

  • java.util.concurrent.BlockingQueue 클래스 API
  • 저수준 메커니즘인 시그널링 기법을 추상화하여 만든 고수준 메커니즘
    • 스레드 시그널링 + 리스트구현 래핑
  • 생산자 - 소비자 패턴을 이용해 문제 해결
    • put() : 큐에 넣는 메서드. 큐가 가득차면 block
    • take() : 큐에서 빼는 메서드. 큐가 비면 block

메시지 전달

  • Non-Blocking 생산자 - 소비자 패턴 메커니즘
  • android.os 패키지의 API를 이용
    • Message : 데이터 / 태스크를 포함하는 단위. 스레드간 주고받는 주체
    • MessageQueue : 소비자 스레드에서 처리할 메시지들을 담고있는 무제한의 연결리스트. 루퍼 당 하나의 스레드만 연결될 수 있다.
    • Looper : 메시지 발송담당. 스레드 당 하나의 루퍼만 연결될 수 있다.
    • Handler : 생산자 스레드에서의 메시지 큐잉, 소비자 스레드에서의 메시지 처리를 담당.

Message

  • 스레드간 통신을 위해 주고받는 주체
  • 데이터 or 태스크 중 하나만을 옮기는 컨테이너 객체
    • 데이터 : 소비자 스레드에서 처리됨
    • 태스크 : 소비자 스레드에서 동작하는 태스크. 현재 진행중인 태스크가 없을 때 실행됨
  • 소비자 스레드에서 메시지의 처리
    • 데이터가 포함된 메시지 : Handler의 handleMessage(Message) 콜백메서드에서 수신된 데이터 메시지를 처리
    • 태스크가 포함된 메시지 : 스레드에서 자동적으로 태스크를 실행. handleMessage() 콜백메서드에서는 태스크 메시지를 수신하지 않는다.
Message 내부 필드
필드명자료형설명
whatint메시지의 식별자
arg1, arg2int정수값을 전달하는 경우에 사용되는 간단한 데이터 필드
objObject임의의 객체. 이 객체가 다른 프로세스의 스레드로 전달될 때는 반드시 Parcelable 로 구현해야 한다.
dataBundle임의의 데이터값을 넣는 컨테이너
replyToMessanger다른 프로세스의 핸들러 참조. 프로세스간 통신을 위해 존재한다.
callbackRunnable스레드에서 실행할 태스크. Handler의 postXXX() 메서드에서 보낸 Runnable 객체
Message 생명주기
  • 4가지의 상태가 존재.
  • 앱에서 메시지를 다룰 때, 메시지의 상태에 어떠한 가정도 하지 않아야 한다.
  • 일단 메시지가 메시지큐에 추가되면, 메시지 안의 내용은 변경되면 안된다.

MessageQueue

  • 처리될 메시지들로 구성된 무제한의 단방향 연결리스트
  • 스레드에서는 핸들러를 이용하여 메시지큐 처리
    • 생산자 스레드 : 메시지큐에 메시지 삽입
    • 소비자 스레드 : 메시지큐에서 메시지 추출하여 처리
  • 메시지는 타임스탬프를 기준으로 정렬된다.
    • 가장 낮은 타임스탬프값을 가진 메시지가 맨 앞에 놓인다.(가장 빨리 처리된다.)
    • 현재시간보다 타임스탬프값이 작을 때만 해당 메시지를 소비자 스레드로 전달한다. 현재시간보다 타임스탬프값이 크다면(미래의 메시지), 해당 시각이 될 때까지 기다린 후 메시지를 전달한다.

Looper

  • 메시지큐에 있는 메시지를 정확한 핸들러로 보내는 역할을 담당
  • 소비자 스레드에서는 메시지큐에 직접접근을 하지 않고, 루퍼를 통해 메시지를 얻는다.
  • 루퍼는 스레드당 하나만 연결된다.
    • 이미 스레드와 연결된 루퍼를 다른스레드에 다시 연결하려고 하면 RuntimeException을 던진다.
    • 결과적으로, 스레드는 하나의 메시지큐만 가지는 것이 된다.
  • UI스레드를 제외한 모든 스레드는 메시지처리를 하기 위해서 명시적으로 루퍼를 얻어야 하고, 메시지 처리를 끝내려는 스레드는 명시적으로 루퍼를 종료해주어야 한다.
Looper 연결
public class ConsumerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        
        //TODO : 루퍼와 핸들러 연결
        
        Looper.loop();
    }
}
  • Looper.prepare() : 루퍼를 생성하고, 루퍼와 현재 스레드를 연결한다. 아직 이 단계에서 메시지를 전송하지는 않는다.
  • Looper.loop() : run()메서드가 완료되지 않았음을 보장하는 Blocking 메서드. run()메서드가 차단된 동안, 루퍼는 이 스레드로 메시지를 전달한다.
Looper 종료
  • quit() / quitSafely() 메서드
    • quit() : 현재시각보다 낮은 타임스탬프의 메시지를 포함하여, 모든 대기중인 메시지를 폐기
    • quitSafely() : 현재시각보다 낮은 타임스탬프의 메시지는 제외하고, 높은 타임스탬프의 대기메시지들을 폐기. 남은 메시지들은 루퍼가 종료되기 전에 처리된다. API레벨 18에서 추가된 메서드.
  • 루퍼를 종료한다고 스레드가 종료되지는 않는다. 단지, blocking 중이었던 loop() 메서드를 빠져나오는 것이다.
UI스레드의 루퍼
  • UI스레드는 앱이 초기화될 때 기본적으로 루퍼를 연결하기 때문에, 따로 연결할 필요가 없다.
  • UI스레드 루퍼는 종료시킬 수 없다. quit() / quitSafely() 메서드가 호출되면 RuntimeException을 던진다.
  • UI스레드 루퍼를 다른 스레드에 부착할 수 없다.
  • Looper.getMainLooper() 메서드를 이용하면 어디서든 UI스레드 루퍼를 접근할 수 있다.

Handler

  • 스레드에서 메시지 삽입/처리를 직접적으로 담당.
  • 핸들러는 내부적으로 메시지큐와의 상호작용을 하기위해 루퍼를 이용한다. 그러므로, 핸들러를 이용하는 스레드는 반드시 루퍼를 연결해야 한다.
Handler 생성
  • 생성시점에 루퍼와 바인딩된다. 루퍼와의 바인딩에 실패하면 RuntimeException을 던진다.
  • 한번 바인딩된 루퍼는 변경될 수 없다.
// 현재 스레드의 루퍼와 바인딩하는 생성자
new Handler();
new Handler(Handler.Callback);

//명시적으로 선언된 루퍼와 바인딩하는 생성자
new Handler(Looper);
new Handler(Looper, Handler.Callback);
메시지 삽입
//데이터 메시지 (sendXXX())
//메시지 객체 직접 전달
boolean sendMessage(Message msg)
boolean sendMessageAtFirstOfQueue(Message msg)
boolean sendMessageAtTime(Message msg, long uptimeMillis)
boolean sendMessageDelayed(Message msg, long delayMillis)
//메시지 의도만 전달
boolean sendEmptyMessage(int what)
boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
boolean sendEmptyMessageDelayed(int what, long delayMillis)

//태스크 메시지 (postXXX())
boolean post(Runnable r)
boolean postAtFrontOfQueue(Runnable r)
boolean postAtTime(Runnable r, Object token, long uptimeMillis)
boolean postAtTime(Runnable r, long uptimeMillis)
boolean postDelayed(Runnable r, long delayMillis)
  • 메시지 전달순서 조절 - 앱이 유일하게 핸들링할 수 있는 방법이다.
    • 메시지의 처리시간은 불확정이다 - 기존에 처리중인 메시지 / 운영체제의 스케쥴링에 영향을 받기 때문
    • default : 전달이 즉시 가능
    • at_front : 타임스탬프값 0으로 전달되어, 다른메시지가 끼어들지 않는 한. 다음에 전달될 메시지가 된다.
    • delay : 이 지연시간 후에 메시지가 전달될 수 있다.
    • uptime : 이 절대시간에 메시지가 전달될 수 있다.
메시지 처리
  • 태스크 메시지
    • 따로 핸들링할 필요 없음. 전송된 태스크 Runnable의 run 메서드가 자동적으로 실행된다.
  • 데이터 메시지
    • 소비자 스레드에서 Handler.handleMessage(Message) 메서드를 오버라이드 하여 받은 메시지를 처리한다.
    • 소비자 스레드에서 Handler에 콜백 인터페이스를 등록한다.
//Handler.handlerMessage() 오버라이드
public class ConsumerThread extends Thread {
    public Handler handler;
    @Override
    public void run() {
        Looper.prapare();
        // 핸들러는 반드시 Looper가 설정된 다음에 생성해야 한다!!
        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                //메시지 처리
            }
        };
        Looper.loop();
    }
}

//Handler에 콜백 인터페이스 등록
public class ConsumerThread extends Thread implements Handler.Callback {
    public Handler handler;
    @Override
    public void run() {
        Looper.prapare();
        // 핸들러는 반드시 Looper가 설정된 다음에 생성해야 한다!!
        handler = new Handler(this);
        Looper.loop();
    }
    
    @Override
    public boolean handleMessage(Message msg) {
        //메시지 처리
        //더이상 처리할 메시지가 없을때 true 리턴. false를 리턴하면 Handler.handleMessage() 메서드가 이어서 호출된다.
        return true;
    }
}
메시지 제거
  • Handler의 메서드를 이용하면 큐에 넣어진 메시지를 제거할 수 있다.
    • 단, 메시지가 루퍼에 의해 큐에서 꺼내지기 전에만 가능하다.
    • 메시지를 정확하게 식별할 수 있어야 한다.
식별자 타입설명적용되는 메시지 타입
Handler메시지 수신자태스크 & 데이터 메시지
Object메시지 태그태스크 & 데이터 메시지
int메시지의 what 매개변수데이터 메시지
Runnable실행될 태스크태스크 메시지
// 메시지 큐에서 태스크 제거
void removeCallbacks(Runnable r)
void removeCallbacks(Runnable, Object token)

//메시지 큐에서 데이터 제거
void removeMessages(int what)
void removeMessages(int what, Object object)

//메시지 큐에서 태스크 / 데이터 동시에 제거(Object로 식별할 경우만 가능)
void removeCallbakcsAndMessages(Object token)