Thread with Handler & AsyncTask



reference: 

https://developer.android.com/reference/android/os/Handler?hl=ko

https://developer.android.com/reference/android/os/Looper?hl=ko

https://academy.realm.io/kr/posts/android-thread-looper-handler/

https://developer.android.com/reference/android/os/AsyncTask?hl=ko

https://stackoverflow.com/questions/12797550/android-asynctask-for-long-running-operations?noredirect=1&lq=1





Android 앱에서의 GUI 작업은 메인스레드(UI스레드)에서만 가능하도록 제한되어있습니다. 그 이유는 메인스레드에서는 GUI에 관련한 작업을 하도록 강제하여 오래 걸리는 작업은 다른 스레드를 이용하도록 하기 위함입니다. 만약 다른 스레드에서도 허용한다면 같은 View 요소에 대한 동기화 이슈가 생길 것입니다. 이를 해결하기 위해 Android에서는 스레드에 Handler & Looper 클래스를 도입하였습니다.


 Looper & Handler

Android의 메인스레드는 내부적으로 message queue와 Looper를 가집니다. Message queue의 역할은 이름 그대로 message를 전달받아 선입선출 방식으로 하나씩 해결하기 위한 자료구조입니다. Message는 다른 스레드나 현재 스레드로부터 받을 수 있습니다. Looper 클래스는 그 message queue로부터 message를 하나씩 꺼내어 Handler에게 처리하도록 전달하는 역할을 합니다. 그럼 Handler는 전달 받은 message를 처리하고, 반대로 message를 받아 message queue에 집어 넣는 역할도 합니다.


Handler

모든 Handler 객체는 하나의 스레드와 그 스레드의 message queue에 연결됩니다. Handler 객체를 하나 생성하면, 생성하고 있는 스레드에 바인드됩니다. 그 순간부터 Handler는 message나 Runnable 객체를 받아 처리하기 시작합니다.

Handler는 2가지 목적으로 사용됩니다.

1. 미래 언젠가에 처리될 message와 runnable을 스케쥴

2. 자기 자신이 아닌 다른 스레드에게 처리하도록 전달

1번의 경우 post...() 및 sendMessage...() 메소드로 이루어집니다. post가 붙은 메소드들은 runnable 객체를 message queue에 전달하고, send message류의 메소드들은 handleMessage() 메소드에 의해 처리될 message들을 전달합니다.

Handler에 post나 send를 할 땐, 준비가 됐을 때 바로 처리하도록 하게 하거나, 지연되어 처리하도록 할 수 있습니다. 지연된 처리의 경우 timeout, tick과 같은 timing 관련 처리도 지원합니다.


Looper

스레드의 message 관련 loop을 돌게 해주는 클래스입니다. 기본적인 스레드는 message loop 관련 처리를 하지 않습니다. 따라서 스레드 안에서 message loop을 돌게 하기 위해서는, 스레드 안에서 Looper의 prepare() 정적 메소드를 호출한 뒤 loop() 정적 메소드를 호출해야 합니다. 메인스레드는 기본적으로 Looper를 갖지만, 사용자가 정의한 스레드에서는 갖지 않기 때문에 message 처리를 하려고 한다면 필수적으로 이 과정이 수행되어야 합니다. Looper는 무한히 loop을 돌며 message queue의 message나 runnable을 차례로 꺼내 처리할 Handler에게 전달합니다.


다음은 전형적인 Looper와 Handler를 갖는 스레드의 예시입니다.

mHandler에 원하는 message나 runnable을 전달할 수 있습니다.

class LooperThread extends Thread { public Handler mHandler; public void run() { Looper.prepare(); mHandler = new Handler() { public void handleMessage(Message msg) { // process incoming messages here } }; Looper.loop(); } }

이런 이유로 편의를 위해 만들어진 클래스가 HandlerThread 클래스입니다. Java의 기본 스레드는 Android의 Looper를 갖지 않기 때문에 이것을 미리 가지도록 처리한 것이 HandlerThread입니다.

아래 그림은 위의 설명을 이해하는데 도움이 됩니다.

android looper message에 대한 이미지 검색결과


 AsyncTask

AsyncTask 클래스는 메인스레드를 적절하고 쉽게 사용할수 있도록 만들어진 클래스입니다. 백그라운드 작업을 가능하게 해주고, Handler나 Looper가 없이도 그 결과에 대한 UI 작업을 메인스레드에게 수행하도록 하게합니다.

AsyncTask는 최대 수초 정도의 비교적 작은 작업을 하는 것이 이상적입니다. 만약 긴 작업을 해야한다면, Executor, ThreadPoolExecutor나 FutureTask 등의 java.util.concurrent 패키지를 사용하는 것이 권장됩니다.

여기에서 비동기 작업이란, 백그라운드 스레드에서 작업을 한 뒤 그 결과를 메인스레드에 전달하는 것을 의미합니다. AsyncTask는 Params, Progress, Result의 세가지 generic type을 갖습니다. 또한 4개onPreExecute() -> doInBackground -> onProgressUpdate -> onPostExecute 의 순서로 전개됩니다. 아래는 전형적인 AsyncTask의 사용 예입니다.

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     
protected Long doInBackground(URL... urls) {
         
int count = urls.length;
         
long totalSize = 0;
         
for (int i = 0; i < count; i++) {
             totalSize
+= Downloader.downloadFile(urls[i]);
             publishProgress
((int) ((i / (float) count) * 100));
             
// Escape early if cancel() is called
             
if (isCancelled()) break;
         
}
         
return totalSize;
     
}

     
protected void onProgressUpdate(Integer... progress) {
         setProgressPercent
(progress[0]);
     
}

     
protected void onPostExecute(Long result) {
         showDialog
("Downloaded " + result + " bytes");
     
}
 
}
 

URL을 Param으로 가지고, 정수를 Progress로, 그 결과 값을 Long으로 택했습니다. doInBackground() 메소드에서 URL로부터 파일을 다운 받으면서, 중간에 publicProgress()를 호출해서 그 progress를 UI에 업데이트 하도록 하고있습니다. onProgressUpdate()에서 그 것을 수행합니다. 또한 작업이 완료 된 후 onPostExecute()가 호출되어 다운로드한 최종 사이즈를 표시합니다.

이 모든 과정에서 Handler에 message를 전달하거나 처리하는 과정이 없이, 자연스럽게 GUI 작업을 한 것을 확인할 수 있습니다.


Conclusion

Android의 메인스레드에서는 GUI 작업만하고, 그외의 작업은 GUI가 블락되는 것을 막기 위해 다른 스레드에서 작업하도록 권장됩니다. 이를 수행하기 위해 Looper와 Handler 클래스가 제공됩니다.

스레드에서 Looper를 동작 시키면 내부의 message queue를 돌면서 하나씩 Handler에게 전달합니다. Handler는 전달받은 message나 runnable을 처리하거나, 반대로 message나 runnable을 전달 받아 message queue에 넣습니다.

AsyncTask는 위의 Handler나 Looper의 직접 구현 없이도 간단하게 사용되도록 고안된 클래스입니다. 다만 수초 정도의 비교적 작은 작업을 수행하면서 GUI 업데이트를 하는 데에 이상적이기 때문에, 더 오래걸리는 작업은 다른 방법을 생각해야 합니다.

결론적으로, 두가지 방법 모두 익힌 상태에서 상황에 따라 Handler와 Looper 혹은 AsyncTask를 선택할 수 있어야 겠습니다.

'programming > android' 카테고리의 다른 글

View - RecyclerView  (0) 2018.08.26
ViewGroup  (0) 2018.08.24
View - AppWidget  (0) 2018.08.23
View - Widget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08

View - RecyclerView



reference:

https://developer.android.com/guide/topics/ui/layout/recyclerview 





 RecyclerView 개요

RecyclerView는 양이 많거나 자주 바뀌는 데이터의 목록을 표시하는 데에 사용합니다. ListView의 개선된 버전이라고 볼 수 있습니다. 

RecyclerView에서 데이터를 표현하기 위해서는 몇가지 요소들이 같이 작동해야 합니다. 

RecyclerView 

기본적으로 ListView와 마찬가지로 뷰를 표시하는 컨테이너로서, layout에 추가되는 요소입니다.

Layout manager

RecyclerView에 제공하여 목록에 아이템을 채우는 역할을 합니다. LinearLayoutManager, GridLayoutManager 등 기존의 layout manager를 사용하거나 직접 만들 수 있습니다.

View holder

목록 안의 아이템들은 view holder 객체로서 표현됩니다. 이 객체들은 RecyclerView.ViewHolder 클래스를 상속 받은 객체입니다. 하나의 아이템을 표현하는 역할을 합니다. RecyclerView는 목록의 아이템 갯수 만큼 view holder 객체를 생성합니다. 사용자가 스크롤 하면 ReyclerView는 화면에서 가려진 아이템을 회수해서 새로 보여질 아이템을 보여주는데 사용합니다.

View holder 객체는 RecyclerView.Adapter 클래스를 상속받은 adpater에 의해 관리됩니다. Adpater 클래스는 필요에 의해 view holder를 만들고, 또한 view holder에 표시할 데이터를 바인드합니다. 이것은 view holder를 아이템의 위치(position)에 할당한 후 adpater의 onBindViewHolder() 메소드를 호출하는 것으로 이루어집니다. 위치에 따라 데이터의 어떤 컨텐츠를 표시할 것인지 결정하는 것입니다.

RecyclerView에서는 다음과 같은 최적화가 되어있습니다:

    • 목록이 처음 결정되면, 그 목록 일부의 대한 view holder를 생성하고 바인드합니다. 예를 들어, 0부터 9 위치의 목록이 표시된다면 RecyclerView는 9까지 view holder를 만들고 바인드합니다. 그리고 10번의 view holder를 만들고 바인드할 수도 있는 것입니다. 이 방법으로, 스크롤 되었을 때 표시할 아이템을 준비시켜놓는 것입니다.

    • 유저가 목록을 스크롤하면, RecyclerView는 필요에 따라 새로운 view holder를 만듭니다. 그리고 화면 바깥으로 밀려난 아이템에 대한 view holder들을 저장하여 다시 사용될 수 있게 합니다. 만약 스크롤 중에 반대방향으로 스크롤하면 저장되어있던 view holder를 바로 불러와서 표시하게 됩니다. 반면에 계속 같은 방향으로 스크롤하면 밀려난 위치의 view holder가 새롭게 표시될 아이템에 다시 바인드 됩니다. View holder는 새로 생성되거나 새로운 view를 inlfate 시킬 필요가 없습니다. 대신 새로운 아이템에 대해서 바인드를 새로하는 것입니다.

    • 표시된 아이템이 변한다면, adapter에 알리면 됩니다(RecyclerView.Adapter.notify...() 메소드). Adpater의 코드가 영향을 미치는 아이템에 대해 다시 바인드시킵니다. 


 RecyclerView 사용하기

Activity에서 다음과 같이 layout에 추가된 RecyclerView를 사용하도록 합니다.

@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
        setContentView
(R.layout.my_activity);
        mRecyclerView
= (RecyclerView) findViewById(R.id.my_recycler_view);

       
// use this setting to improve performance if you know that changes
       
// in content do not change the layout size of the RecyclerView
        mRecyclerView
.setHasFixedSize(true);

       
// use a linear layout manager
        mLayoutManager
= new LinearLayoutManager(this);
        mRecyclerView
.setLayoutManager(mLayoutManager);

       
// specify an adapter (see also next example)
        mAdapter
= new MyAdapter(myDataset);
        mRecyclerView
.setAdapter(mAdapter);
   
}

LinearLayoutManager로 설정해놓아 일렬로 표시되도록 하고, setAdapter()를 통해 adapter를 설정했습니다.

다음은 adapter 클래스입니다.

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
   
private String[] mDataset;

   
// Provide a reference to the views for each data item
   
// Complex data items may need more than one view per item, and
   
// you provide access to all the views for a data item in a view holder
   
public static class MyViewHolder extends RecyclerView.ViewHolder {
       
// each data item is just a string in this case
       
public TextView mTextView;
       
public MyViewHolder(TextView v) {
           
super(v);
            mTextView
= v;
       
}
   
}

   
// Provide a suitable constructor (depends on the kind of dataset)
   
public MyAdapter(String[] myDataset) {
        mDataset
= myDataset;
   
}

   
// Create new views (invoked by the layout manager)
   
@Override
   
public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
                                                   
int viewType) {
       
// create a new view
       
TextView v = (TextView) LayoutInflater.from(parent.getContext())
               
.inflate(R.layout.my_text_view, parent, false);
       
...
       
MyViewHolder vh = new MyViewHolder(v);
       
return vh;
   
}

   
// Replace the contents of a view (invoked by the layout manager)
   
@Override
   
public void onBindViewHolder(MyViewHolder holder, int position) {
       
// - get element from your dataset at this position
       
// - replace the contents of the view with that element
        holder
.mTextView.setText(mDataset[position]);

   
}

   
// Return the size of your dataset (invoked by the layout manager)
   
@Override
   
public int getItemCount() {
       
return mDataset.length;
   
}
}

위 adpater은 TextView를 하나 사용하는 아이템에 대한 행동을 정의하고 있습니다. 표시할 아이템들의 목록을 필드로 갖고, 바인드되었을 때 어떻게 아이템을 표시할 것인지 onBindViewHolder()에 정의했습니다. 또한 처음 bind되었을 때 어떻게 view holder를 만들 것인지 onCreateViewHolder()에 정의했습니다. 이 예시에서는 아이템에 대한 layout을 inflate하고 그 view를 가지고 view holder를 만들도록 했습니다. 이 메소드 안에서는 어떻게 아이템을 표시할 것인지 정의하지 않습니다.


추가적으로, 다음과 같은 기본 제공 layout manager를 사용할 수도 있습니다.

LinearLayoutManager : 1차원의 목록을 일렬로 표시, ListView와 같이 표시됨

GridLayoutManager : 2차원의 grid로 표시함.

StaggeredGridLayoutManger : 아이템의 offset이 살짝 서로간에 다른 2차원 grid로 표현함(예를 들어미국 국기의 별들)

 

Conclusion

RecyclerView은 여러 최적화를 거친 ListView입니다. 데이터를 view holder에 바인드 시켜 어떻게 표시할 것인지 정의합니다. 또한 layout manager를 통해 어떤 식으로 목록을 표시할 것인지 customize할 수 있습니다. 

View holder는 아이템이 바인드될 view를 갖는 클래스입니다. 기존에 ListView에서 방식은 findView하는 과정이 반복되었는데, view holder를 통해 이를 개선한 효과도 있습니다.

표시할 데이터가 변했을 때 adpater에 이를 알리면 onBindViewHolder() 메소드가 호출되어 다시 표시하도록 합니다. 


아이템 목록

-> Adpater에 연결

-> Adpater가 position에 따라 해당 위치의 데이터를 표시가능한 형태(View holder)로 연결

-> 목록이 변했음을 adapter에 알리면, 목록을 view holder에 다시 바인드

-> 새롭게 bind된 목록을 다시 RecyclerView에 표시

'programming > android' 카테고리의 다른 글

Thread with Handler & AsyncTask  (0) 2018.08.27
ViewGroup  (0) 2018.08.24
View - AppWidget  (0) 2018.08.23
View - Widget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08

ViewGroup



reference: 

https://developer.android.com/reference/android/view/ViewGroup

https://developer.android.com/guide/topics/ui/layout/linear?hl=ko

https://developer.android.com/guide/topics/ui/layout/relative

https://developer.android.com/reference/android/support/constraint/ConstraintLayout#DimensionConstraints




 ViewGroup 개념

ViewGroup이란 다른 View를 자식으로 가질수 있는 특별한 형태의 View입니다. Layout, View containers의 부모 클래스이기도 합니다. ViewGroup은 LayoutParam 클래스를 포함하고 있는데, 이는 ViewGroup이 자식 View들에게서 어떤 속성을 받아 사용할 것인지 정의하는 역할을 합니다. 흔히 "android:layout_" 으로 시작하는 속성들이 이것에 해당합니다.  


 ViewGroup 중 대표적인 Layout들

Android를 개발하면서 흔히 접할 수 있는 layout은 다음과 같습니다.

LinearLayout

RelativeLayout

FrameLayout

ConstraintLayout

위 layout들의 차이점을 분명히 인식하고 있어야 어느 상황에서 적절히 사용할 수 있는지 알 수 있을 것입니다. 구체적으로 어떻게 작성하는지는 참조 링크로 대체하겠습니다.


 LinearLayout

가로 혹은 세로 방향의 orientation("android:orientation" 속성)이 주어지고 그 단일 방향으로 모든 하위 View를 그립니다. 모든 항목은 순차적으로 배치되기 때문에 가로 방향의 경우 한 행에 한 View만 놓이게 됩니다. layout_weight 속성을 통해 가중치를 부여할 수 있습니다. 세로 방향인 경우 하위 View의 height를 0dp로 부여하고 weight를 1로 적은후, 나머지 View에는 weight를 부여하지 않으면 부여된 View가 남은 화면을 채우도록 확장됩니다.


RelativeLayout

하위 View들의 위치가 형제 View나 부모의 위치를 기준으로 설정될 수 있는 layout입니다. 주의할 점은 서로 상충되는 속성값을 갖도록 부여해서는 안된다는 점입니다. 예를들어 RelativeLayout의 height가 wrap_content인데, 자식 View가 ALIGN_PARENT_BOTTOM 속성을 갖도록 해서는 안됩니다. 아래는 주요하게 사용되는 주요 속성들입니다.


   android:layout_alignParentTop
   If "true", makes the top edge of this view match the top edge of the parent.
   android:layout_centerVertical
   If "true", centers this child vertically within its parent.
   android:layout_below
   Positions the top edge of this view below the view specified with a resource ID.
   android:layout_toRightOf
   Positions the left edge of this view to the right of the view specified with a resource ID.
참고로 이름에 right/left가 들어가는 속성은 RTL(Right to Left) 언어에서 의도치않게 동작할 수 있으므로 start/end로 대체하는 것이 권장됩니다. 실제로 Android Studio에서도 그렇게 권장합니다.

FrameLayout

화면의 부분 혹은 전체를 하나의 View로 덮어 씌우도록 의도된 layout입니다. 일반적으로 하나의 자식을 갖도록 하는데, 자식들의 위치를 조절하기가 쉽지 않기 때문입니다. 다만, "android:layout_gravity" 속성을 통해 자식들의 위치를 어느정도 지정할 수 있습니다. 자식들은 stack처럼 아래에서 부터 위로 올라가는 형태로 겹쳐집니다.


ConstraintLayout

자식 View들을 유연하게 배치할 수 있는 layout입니다. 아래와 같이 다양한 옵션을 제공합니다.

Relative positioning

Margins

Centering positioning

Circular positioning

Visibility behavior

Dimension constraints

Chains

Virtual Helpers objects

Optimizer


Relative positioning

left, right, top, bottom 혹은 baseline을 기준으로 형제간에 상대적으로 위치를 지정할 수 있습니다. 

 
Fig. 1 - Relative Positioning Example

A의 right를 기준으로 B의 left가 정해진 예시입니다. B의 "app:layout_constraintLeft_RightOf" 속성 값을 A의 id로 부여한 경우입니다. 같은 레벨의 형제 View나 부모만 값으로 가질 수 있습니다.


Margins

left, right, top, bottom를 기준으로 constraint target으로부터의 margin을 부여할 수 있습니다.

 
Fig. 3 - Relative Positioning Margins

위 예시는 B의 left가 A의 right로 relative positioning된 상태에서 marginLeft가 적용된 경우입니다. 또한 goneMargin을 부여할 수 있는데, 이는 constraint target의 visibility가 View.GONE이 된 경우의 margin을 뜻합니다. 위 경우에서 goneMarginLeft를 0dp로 한 상태에서 A를 gone 처리 하면 B가 A의 위치에 조정 될 것입니다.

Centering positioning and bias

Constraint target을 기준으로 위치에 대한 bias를 부여합니다. 

 
Fig. 4 - Centering Positioning

위는 A의 left와 right가 동시에 적용하면 기본적으로 그 중간에 위치하게 된다는 것을 보여줍니다.

 
Fig. 5 - Centering Positioning with Bias

horiziontal bias를 부여한 그림입니다. 0이면 left에 붙고 1이면 right에 붙게 됩니다. 즉, left와 right 사이 어느 위치에 오게될 지 bias로 정할 수 있습니다.


Circular positioning

Circular한 기준으로 위치를 정할 수 있습니다.

  
Fig. 6 - Circular Positioning   

constraintCircle 속성으로 기준을 정하고, constraintCircleRadius와 constraintCircleAngle을 정합니다. 위는 12시를 기준으로 45도 만큼의 angle이 주어진 예시입니다.


Visibility behavior

Constraint target이 View.GONE 처리 된 상태이더라도 여전히 그 자리에 있는 것처럼 동작합니다. 실제로 그 것이 보이지 않더라도 말이죠.

 
Fig. 7 - Visibility Behavior

위 예시는 A가 gone 처리 되었음에도 B의 left가 여전히 존재하는 것처럼 정상적으로 동작하는 것을 보여줍니다. 다만 A가 A의 left로 대체되었을 뿐입니다.


Dimension constraint

Layout의 크기를 minimum, maximum 값으로 부여할 수 있습니다. wrap_content와 함께 사용되어야 하겠죠. min(max)Width, min(max)Height로 지정합니다.


Layout의 높이와 너비는 세가지로 부여될 수 있습니다.  

1. 특정한 dimension 값

2. WRAP_CONTENT

3. 0dp (MATCH_CONSTRAINT와 같음)

주의할 점이 있다면, ConstraintLayout에의 자식들의 크기에는 MATCH_PARENT가 권장되지 않습니다.  비슷한 속성으로 MATCH_CONSTRAINT를 사용하는데, 이는 left, right, top, bottom의 constraint target이 부모로 지정되는 효과를 갖습니다.

1.1 버전 이후부터는 WRAP_CONTENT의 크기를 갖는 자식 View에 대해 constraint target을 침범할 것인지 여부를 정할 수 있습니다.

<app:layout_constrainedWidth="true"> 속성이 부여된 자식은, left와 right를 침범하지 않습니다. 반대로 height에 대한 속성도 있습니다.


Percent

또한 크기를 percent로 부여할 수 있습니다. 크기가 0dp(MATCH_CONSTRAINT)로 지정되고, 

<app:layout_constraintWidth_default="percent">

<app:layout_constraintWidht_percent="0.5">

위 속성들이 정의된다면, 이 View의 너비는 left와 right 사이에 너비의 정확히 절반을 가지게 될 것입니다.


Ratio

너비와 높이를 서로의 비율로 부여할 수 있습니다. 아래 Button은 높이가 너비에 맞춰 1:1이 되도록 설정한 예시입니다. (width:height)

<Button android:layout_width="wrap_content"

android:layout_height="0d"

app:layout_constraintDimensionRatio="1:1" />


Chain

양방향으로 서로 연결된 View들의 관계 Chain이라고 말합니다.

 
Fig. 9 - Chain

이 예시는 A와 B가 서로에게 chain 되어 있습니다.

 
Fig. 10 - Chain Head

가장 앞쪽(혹은 위쪽)에 있는 요소가 chain의 head가 됩니다.


Chain Style

Chain된 View들이 어떻게 배치될 것인가에 대한 style 입니다.


 
Fig. 11 - Chains Styles

어떻게 배치할 것인지 유용하게 사용될 수 있을 것 같습니다.


Virtual Helper objects

자세한 내용이 없어 원문으로 대체합니다.

In addition to the intrinsic capabilities detailed previously, you can also use special helper objects in ConstraintLayout to help you with your layout. Currently, the Guideline object allows you to create Horizontal and Vertical guidelines which are positioned relative to the ConstraintLayout container. Widgets can then be positioned by constraining them to such guidelines. In 1.1, Barrier and Group were added too.


Optimizer(in 1.1)

1.1 부터 app:layout_optimizationLevel 태그로 부여할 수 있습니다. 아직 실험 단계로 보여 지켜봐야할 것 같습니다. 이름 그대로 최적화에 관한 내용으로 추정됩니다.

    • none : no optimizations are applied
    • standard : Default. Optimize direct and barrier constraints only
    • direct : optimize direct constraints
    • barrier : optimize barrier constraints
    • chain : optimize chain constraints (experimental)
    • dimensions : optimize dimensions measures (experimental), reducing the number of measures of match constraints elements


Conclusion

Android에서 자주 사용되는 layout 4종류에 대해 간략히 알아봤습니다.


LinearLayout : 자식들이 단방향으로 일정한 순서로 배치될 때

RelativeLayout : 자식들의 위치가 서로에 관계가 있을 때

FrameLayout : 자식을 하나 정도 가지며 위치를 조정할 필요가 없고, 자식들이 서로 겹치도록 하고 싶을 때

ConstraintLayout : 복잡한 자식들의 위치나 크기 설정을 요구할 때


위와 같은 기준으로 적재적소에 layout을 사용할 수 있어야겠습니다.

'programming > android' 카테고리의 다른 글

Thread with Handler & AsyncTask  (0) 2018.08.27
View - RecyclerView  (0) 2018.08.26
View - AppWidget  (0) 2018.08.23
View - Widget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08

View - AppWidget



reference: 

https://developer.android.com/guide/topics/appwidgets/





 AppWidget 개념

흔히 홈(런처)에서 아이콘이 아닌 특별한 형태의 앱 모양을 보신 적이 있을 겁니다. 보통 날씨 등의 앱에서 사용되곤 하는데요. 앱에 진입하지 않고도 제공하는 내용을 표시하는데에 주로 사용됩니다. 아래 이미지를 보면 알 수 있습니다. 

appwidget에 대한 이미지 검색결과


AppWidget 사용법

AppWidget을 만들기 위해서는, 다음과 같은 것들이 필요합니다.

AppWidgetProviderInfo 

: AppWidget에 대한 metadata를 표현합니다. layout이나 얼마나 자주 업데이트 할 것인지, 어떤 AppWidgetProvider를 사용할 것인지 등을 XML 파일로 정의합니다.

AppWidgetProvider

: AppWidget에 대한 broadcast event를 다루는 interface입니다.  AppWidget이 updated, enabled, disabled, deleted 됐을 때에 대한 처리를 합니다.

View layout

: AppWidget의 layout이 어떻게 생겼는지 정의하는 XML 파일입니다.


추가적으로, AppWidget configuration Activity를 만들 수 있는데, 사용자가 AppWidget을 추가했을 때 실행되며, 그 때 어떤 설정 값들을 사용할 것인지 정할 수 있습니다.


    AppWidget을 Manifest에 선언하기

먼저, AppWidgetProvider를 AndroidManifest.xml 파일에 선언해야 합니다.

<receiver android:name="ExampleAppWidgetProvider" >
   
<intent-filter>
       
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
   
</intent-filter>
   
<meta-data android:name="android.appwidget.provider"
               
android:resource="@xml/example_appwidget_info" />
</receiver>

AppWidgetProvider는 앞서 설명한 바와 같이 broadcast receiver이기 때문에 <receiver> 태그로 선언됩니다. 또한, ACTION_APPWIDGET_UPDATE 인텐트를 받도록 필터링 해야함을 확인할 수 있습니다. <meta-data> 태그로 AppWidgetProviderInfo를 선언하였습니다.

AppWidgetProviderInfo metadata 추가하기

AppWidgetProviderInfo는 AppWidget의 필수적인 요소들을 정의합니다. 예를 들어 최소 layout dimension이나 최초의 layout resource 혹은 얼마나 자주 업데이트 할 것인지, configuration Activity를 사용할 것인지 등입니다. 아래와 같이 <appwidget-provider> 태그로 XML 파일에 정의됩니다.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   
android:minWidth="40dp"
   
android:minHeight="40dp"
   
android:updatePeriodMillis="86400000"
   
android:previewImage="@drawable/preview"
   
android:initialLayout="@layout/example_appwidget"
   
android:configure="com.example.android.ExampleAppWidgetConfigure"
   
android:resizeMode="horizontal|vertical"
   
android:widgetCategory="home_screen">
</appwidget-provider>

configure attribute에서 configuration Activity를 사용하였음을 확인할 수 있습니다.


AppWidget layout 만들기

res/layout 디렉토리에 반드시 AppWidget의 초기 layout을 만들어놔야 합니다. AppWidget은 RemoteView 클래스에 기반하기 때문에 아무 layout이나 사용할 수 없습니다. 다음 네가지 layout만 사용가능합니다.

FrameLayout

LinearLayout

RelativeLayout

GridLayout

그리고 다음 Widget 클래스들만 허용됩니다:

AnalogClock

Button

Chronometer

ImageButton

ImageView

ProgressBar

TextView

ViewFlipper

ListView

GirdView

StackView

AdapterViewFlipper

 위 클래스를 상속한 클래스는 사용할 수 없습니다.

RemoteView는 ViewStub도 지원하는데, ViewStub은 사이즈가 0인 상태에서 런타임에 inflate하는 layout resource입니다.


AppWidget 패딩

AppWidget은 스크린의 가장자리까지 차지하거나 다른 Widget과 섞이면 안됩니다. 그렇기 때문에 Widget frame의 모든 side에 margin이 있어야합니다. Android 4.0 에서부터는 자동적으로 Widget frame과 그 bounding box 사이에 padding을 부여합니다. 그러니 SDK 14버전 이상을 사용하는 것이 좀 더 안전할 것입니다.

그 말인즉, API 14 미만 타겟의 layout에서는 추가적인 margin을 주어야 하고, 그 이상에서는 줄필요가 없다는 이야기입니다.


AppWidgetProvider 사용하기

AppWidgetProvider는 언급한 바와 같이 AppWidget에 관련한 intent를 받는 BroadcastReceiver입니다. 다음과 같은 메소드들을 갖습니다.

onUpdate()

AppWidgetProviderInfo에 선언된 updatePeriodMillis  속성 값만큼의 주기마다 호출됩니다. 또한 사용자가 AppWidget을 추가해도 호출됩니다. 그러므로 이 메소드는 초기 세팅 관련 작업을 수행해야 합니다. 그러나 configuration Activity를 사용한다면 사용자가 추가할 때 이 메소드는 호출되지 않습니다. 그 역할은 configuration Activity가 대신하기 때문이죠.

onAppWidgetOptionsChanged()

AppWidget이 처음 자리 잡거나, 크기가 바뀔 때 호출됩니다. 바뀐 크기에 따라 내용을 감출지 표시할지 정할 수 있습니다. getAppWidgetOptions() 메소드는 AppWidget의 최소/최대 사이즈에 관련된 option 값들을 포함하는 Bundle 객체를 반환합니다. (API 16 이상)

onDeleted(Context, int[])

AppWidget host로부터 AppWidget에 삭제되었을 때 호출됩니다.

onEnabled(Context)

AppWidget 객체가 처음으로 만들어졌을 때 호출됩니다. 만약 사용자가 두개의 객체를 만들어 추가했다면 처음 한번만 호출됩니다. Database에 접근하는 등의 처음 한번만 필요한 세팅 작업을 하기에 적합합니다.

onDisabled(Context)

AppWidget host에 의해 AppWidget이 삭제되었을 때 호출됩니다. onEnabled()에서 수행된 모든 작업에 대한 청소 작업을 수행해야 합니다.

onReceive(Context, Intent)

위 메소드들이 호출되기 전에 호출되는 메소드입니다. BroadcastReceiver의 메소드입니다.

AppWidgetProvider의 메소드 중 가장 중요한 것은 onUpdate() 콜백입니다. 사용하기에 따라서 이 메소드의 구현만으로 나머지를 대체할 수도 있습니다.


Configuration Activity 만들기

<activity android:name=".ExampleAppWidgetConfigure">
   
<intent-filter>
       
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
   
</intent-filter>
</activity>

위와 같이 Activity 정보를 입력해야 합니다. ACTION_APPWIDGET_CONFIGURE intent를 받아야 합니다. 앞서 언급한 바와 같이, AppWidgetProviderInfo에 android:configure 속성 값에 이 Acitivity가 선언되어 있어야합니다. 주의할 점은, 외부에서 접근하는 Acitivity이기 때문에 namespace는 절대 경로로 적어야 합니다.

이 Activity는 AppWidget의 ID를 result로 돌려줘야 합니다. 자세한 내용은 참조 사이트 확인 하시면 됩니다.


Conclusion

AppWidget은 Activity에 진입하지 않고도 홈에서 중요한 정보를 표시하는데에 사용됩니다. AppWidgetProviderInfo, AppWigetProvider, layout resource 세가지를 구현합니다. Configuration Activity를 선언할 수도 있습니다.



'programming > android' 카테고리의 다른 글

View - RecyclerView  (0) 2018.08.26
ViewGroup  (0) 2018.08.24
View - Widget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08
어떤 Context를 써야 하나요?  (0) 2018.03.26

View - Widget



reference:

https://developer.android.com/reference/android/widget/package-summary 





 Android를 개발하면서 평소에 크게 의문갖지 않았던 부분들에 대해서 정리합니다. Widget에 대해 알아보겠습니다.


 Widget 개념

Widget이란 Android 앱에서 사용자와 상호작용하는 그래픽 요소입니다. 이 Widget은 View 클래스로 구현됩니다. View 클래스는 상호작용 요소로서 기본적인 block으로서 역할을 합니다. 네모난 영역을 차지하며, 그림을 그리고 이벤트 핸들링을 합니다.

android.widget 패키지는 보통 눈에 보이는 UI 요소를 포함하고 있습니다. 나만의 widget을 만들려면, View(혹은 그 하위클래스)를 상속받으면 됩니다. XML안에서 widget을 사용하려면 다음과 같은 파일들이 필요합니다 :

- Java implementation file : widget이 어떻게 동작할 것인지에 대한 java 파일입니다. XML layout으로부터 객체를 만들어낼 수 있다면, XML로부터 attribute 값들을 불러오는 생성자가 정의되어 있어야합니다.

- XML definition file : res/values/ 디렉토리 안의 XML 파일로, widget 및 그 widget의 attributes를 객체화하기 위해 사용되는 파일입니다. 다른 application에서 이 요소와 attributes를 또 다른 요소에서 사용하게 됩니다.

- Layout XML [optional] : res/layout/ 디렉토리 안의 optional한 XML 파일입니다. 이 파일은 widget의 layout을 표현합니다. Java 파일 안에서 이것을 정의할 수 있기 때문에 optional 합니다.

 

Widget 사용법

Widget을 만드는 예제입니다.

1. LableView.java (Java implementation file) : https://android.googlesource.com/platform/development/+/android-6.0.1_r46/samples/ApiDemos/src/com/example/android/apis/view/LabelView.java

코드가 긴 관계로 위 링크로 대체하였습니다. View의 생성자는 네가지가 오버로드 되어있습니다.


public View(Context context)

: 오로지 Java 코드안에서만 생성할때 쓰입니다. 이 생성자만 정의된 경우 XML 안에서 표현될 수 없습니다.

public View(Context context, @Nullable AttributeSet attrs)

: attrs 인자는 위에 설명된 XML definition file로 부터 attributes들을 객체화 시키는데 사용됩니다.

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)

: defStyleAttr 인자는 현재 테마안에서 View의 default 값을 제공하는 style resource에 대한 참조를 갖고있습니다. default 값을 갖지 않으려면 0을 넣어줍니다.

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)

: defStyleRes 인자는 뷰에대한 default 값을 제공하는 style 리소스의 identifier입니다. defSytleAttr이 0 일때 혹은 현재 테마에서 찾을 수가 없을 때 사용됩니다. default 값을 갖지 않으려면 0을 넣어줍니다.


보통 생성자에서는 attrs(AttributeSet)을 객체화하여 View를 그릴 때 참고할 수 있게 합니다. 

Context의 obtainStyeldAttributes 메소드를 이용해 XML 파일에 정의된 attribute들을 객체화하여 TypedArray에 담습니다. 이 TypedArray로 부터 각각의 attribute들을 가져온 후 View를 그릴 때 활용 하는 것입니다.

Attribute들을 객체화하는데 성공했다면, 이제 실제로 View를 그려야합니다. View는 그려지는 단계는 세가지 단계입니다: 

Measurement - onMeasure 메소드 : View의 가로와 세로의 크기를 결정합니다. MeasureSpec을 이용해서 bitwise로 연산된 int 값을 이용합니다.

Layout - onLayout 메소드 : 결정된 크기를 기준으로 어느 위치에 그릴 것인지 결정합니다.

Draw - onDraw 메소드 : Canvas 객체를 이용하여 실제로 무엇을 그릴 것인지 결정합니다. 

위 메소드 들을 단계적으로 실행함으로서 비로소 View가 그려집니다.


2. attrs.xml (XML definition file) 

https://android.googlesource.com/platform/development/+/android-6.0.1_r46/samples/ApiDemos/res/values/attrs.xml

위 설명한 바와 같이 Widget이 어떤 attribute들을 사용할 것인지 정의 합니다.  <declare-styleable> 태그를 사용하며,  name 속성 값에 Java 파일의 클래스 이름을 넣어줍니다. 하위 아이템들로 <attr> 태그를 사용하며 name을 정하고 default 값이나 format을 정의해놓을 수도 있습니다. "[클래스 이름]_[속성 이름]"의 스트링으로 TypedArray에서 값을 가져올 수 있습니다.


3. custom_view_1.xml (XML definition file) 

:https://android.googlesource.com/platform/development/+/android-6.0.1_r46/samples/ApiDemos/res/layout/custom_view_1.xml

1, 2번을 통해 만들어진 Widget을 다른 layout 파일에서 사용한 예시입니다. 


Widget을 왜 사용하나?

Android application 안에서 어떤 UI가 표시되기 위한 각각 요소 단위로서 이해할 수 있습니다. Android를 처음 접할 때 무심코 사용했던 TextView, Button 등의 Widget들이 도대체 어떤 식으로 객체화되어 Java 코드 안에서 동작하는지 이해하여야 나만의 Widget을 정의하고 그릴 수 있을 것입니다. 


'programming > android' 카테고리의 다른 글

ViewGroup  (0) 2018.08.24
View - AppWidget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08
어떤 Context를 써야 하나요?  (0) 2018.03.26
기본적인 메모리 관리  (0) 2018.03.06

Companion Objects in Kotlin



reference: 

https://blog.kotlin-academy.com/a-few-facts-about-companion-objects-37e18429b725





 Static in Kotlin

Kotlin에서는 Java의 static 키워드가 없습니다. Static과 유사하게 사용하기 위해서 아마 많은 분들이 companion object를 사용하고 계실 것 같습니다. 이 포스팅에서는 Java static과 Kotlin companion object와 간략하게 비교해보려 합니다.


 Companion objects

Companion object는 기본적으로 그 클래스 안에 존재하는 singleton 객체입니다. Java로 convert된 결과를 보더라도 companion object는 Companion이라는 이름을 가진 static singleton임을 알 수 있습니다. 이말인 즉, companion이라는 키워드는 사실 Companion이라는 이름을 갖는 클래스의 객체를 가리키는 shortcut 역할을 할 뿐입니다. 그리고 더 나아가 굳이 companion이 아니더라도 static 멤버들을 가질 수 있습니다. 아래 코드는 완벽히 동작합니다.

class TopLevelClass {

companion object {
fun doSomeStuff() {
...
}
}

object FakeCompanion {
fun doOtherStuff() {
...
}
}
}

fun testCompanion() {
TopLevelClass.doSomeStuff()
TopLevelClass.Companion.doSomeStuff()
TopLevelClass.FakeCompanion.doOtherStuff()

}


그러면 위의 companion과 FakeCompanion의 차이는 무엇일까요. 

Java로 decompile을 해보면 약간 다른 점이 있습니다. companion은 부모 클래스의 static 멤버 필드로 가지고 있는 반면, FakeCompanion은 INSTANCE라는 이름으로 본인의 객체 레퍼런스를 내부적으로 갖습니다. 즉, 위의 test코드가 Java에서는 아래와 같이 변형된다는 이야기입니다.

public void testCompanion() {
TopLevelClass.Companion.doSomeStuff();
TopLevelClass.FakeCompanion.INSTANCE.doOtherStuff();
}


 @JvmField, @JvmStatic

Annotation을 한번 살펴봅시다. 

@JvmField는 getter와 setter를 생성하지 않도록 명시하고, 그저 Java의 일반적인 필드로서 사용되도록 합니다. Companion 안에서 사용된다면 companion이 아닌 부모 클래스의 static field가 됩니다. 정리하자면, companion object 안의 일반 적인 멤버는 부모 클래스의 private 멤버가 되며 companion의 getter, setter를 통해서만 접근되고, @JvmField가 붙는다면 부모 클래스의 public 멤버가 되어 외부에서 자유롭게 접근됩니다. 물론 둘다 static인 건 같습니다.

@JvmStatic은 부모 클래스에게 해당 멤버에 대한 접근을 제공하는 용도로 사용됩니다. 특히 조심해야할 점은, companion의 멤버 필드는 부모 클래스의 static 멤버로 선언되지만, 멤버 메소드는 companion 안에서 선언된다는 점입니다. 즉, @JvmStatic을 붙여줘야 외부에서 companion object의 메소드를 접근할 수 있습니다. 예를 들어 Dagger를 사용할 때 provide 메소드에 @JvmStatic을 붙여줘야 하는 이유가 그것입니다. 아래는 companion의 getFoo1() 메소드에 @JvmStatic을 붙여줬을 때 생성되는 부모 메소드입니다. 이게 없다면 외부에서는 아예 접근조차 안되겠네요.

public static final int getFoo1() {
return Companion.getFoo1();
}

 

"object" keyword

object 키워드는 앞서 잠깐 언급된 바와 같이 클래스 안의 singleton 객체로서 동작합니다. 즉  예를 들어 Dagger에서 사용할 모듈은 굳이 companion일 이유가 없습니다.

@Module
object MyModule {

@Provides
@Singleton
@JvmStatic
fun provideSomething(anObject: MyObject): MyInterface {
return myObject
}
}

companion 또한 이름을 가지거나 interface를 구현할 수도 있습니다. 아래는 Kotlin에서 Parcelable을 만드는 전통적인 코드입니다. CREATOR가 companion object네요.

class ParcelableClass() : Parcelable {

constructor(parcel: Parcel) : this()

override fun writeToParcel(parcel: Parcel, flags: Int) {}

override fun describeContents() = 0

companion object CREATOR : Parcelable.Creator<ParcelableClass> {
override fun createFromParcel(parcel: Parcel): ParcelableClass = ParcelableClass(parcel)

override fun newArray(size: Int): Array<ParcelableClass?> = arrayOfNulls(size)
}
}


Conclusion

아무래도 Kotlin은 Java의 static을 별로 좋아하지 않는 느낌입니다. 코드 상으로 느껴지는 점은 static한 것들이 companion안에 다 모여있으니 한눈에 들어오지만.. 뭔가 코드가 더러워보이는 건 Java에 익숙해진 탓인 것 같습니다. Kotlin스럽게 코딩하기 어렵네요.

'programming > android' 카테고리의 다른 글

View - AppWidget  (0) 2018.08.23
View - Widget  (0) 2018.08.23
어떤 Context를 써야 하나요?  (0) 2018.03.26
기본적인 메모리 관리  (0) 2018.03.06
Splash 화면 구성하기  (3) 2018.02.20

어떤 Context를 써야 하나요?



reference:

https://medium.com/@ali.muzaffar/which-context-should-i-use-in-android-e3133d00772c 





 Context

안드로이드 앱 개발을 하다보면  Context에 접근할 일이 많습니다. 그런데, Activity가 아닌 다른 클래스에서 Context를 접근해야 한다면, (물론 인자, 필드 주입 등으로 해결 가능하지만) getContext() 같은 메소드를 호출해야만 합니다. 

Context가 무엇일까요?

Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It
allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.


위는 Context 클래스의 주석입니다. 어플리케이션의 환경, Intent, Activity 이동 등의 행위를 할 수 있는 클래스라고 합니다. 개발하면서 여러분들도 생각한 그런 역할과 크게 다르지 않은 것을 알 수 있습니다.

그런데 문제는 getContext() 뿐 아니라 getBaseContext(), getApplicationContext() 등 다양한 Context가 있다는 것이죠. 잘 못된 Context는 메모리 누수를 발생시킬 수 있습니다.


Context의 종류

Context
| — ContextWrapper
|— — Application
| — — ContextThemeWrapper
|— — — — Activity
| — — Service
|— — — IntentService

Context는 위와 같은 구조를 가집니다. 사실 우리가 부르는 Context는 부르기에 따라서 Application일 수도 있고, Activity일 수도 있고 Service일 수도 있는 것입니다. Context 자체는 사실상 제공해주는 것이 없고, 그것을 상속받은 자식이 모든 행위를 하게 됩니다.

한 예로, ContextThemeWrapper를 보면 다음과 같은데,  전부 theme이 적용된 리소스를 제공하는 것을 알 수 있습니다. 즉, theme과 관련된 작업을 하기 위해서는 이 클래스를 사용해야 한다는 의미입니다.

@Override
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}

@Override public Resources.Theme getTheme() {
if (mTheme != null) {
return mTheme;
}
mThemeResource = Resources.selectDefaultTheme(
mThemeResource,
getApplicationInfo().targetSdkVersion);
initializeTheme();
return mTheme;
}
@Override public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(
getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}

반대로 getApplicationContext() 등의 메소드로 화면의 theme 작업을 하면 적용되지 않습니다. 왜냐면 앞서 언급된 바와 같이 theme은 그 화면에 종속된 특성이며, application context는 말그대로 application-wide한 context이기 때문입니다.


 Memory Leak

잘못된 Context 호출이 어떻게 메모리 누수를 발생시킬까요. 

Context는 40여종의 직/간접적 children을 소유하고 있습니다. 잘못된 Context는 필요치 않은 children의 reference 까지 같이 딸려오게 되므로 메모리가 누수되는 것은 어찌보면 당연합니다. 그래서 우리는, 화면에 종속된 작업을 하는 경우에 getBaseContext() 같은 메소드 호출을 피해야 하는 것입니다. 당연히 해당 Activity or Fragment 등을 호출하는 것이 가장 좋겠죠.


 getApplicationContext()

어떤 경우에 getApplicationContext() 를 호출해야 하는지 생각해 봅시다. 말그대로 application 수준의 context 입니다. 스스로 체크해봅시다. (두 경우 모두 사실 같은 이야기 같네요)


1. 화면과 관계없는 long living object에서 접근하고자 하는가

예를 들어 Activity가 바뀌거나 죽은 뒤에도 계속 수행되는 Thread가 만약 ContextThemeWrapper(Activity or Fragment etc.)를 가지고 있는다면 당연히 메모리 누수.


2. Theme이 적용되어야 하는 작업을 하려고 하는가? 

예를 들어 View를 inflate하고 drawable에 tint를 먹인다던가 하는 따위의 작업은 application context가 사용되어선 안됨. 화면에 종속된 View 작업은 왠만하면 피하자.


getThemedContext()

이 메소드는 ActionBar와 연관이 있습니다. ActionBar에 연관된 theme을 적용한 View를 적용하고 싶을 때 사용합니다. ActionBar에 새로운 text나 icon등을 적용하고 싶을 때 사용된다고 생각할 수 있겠습니다. 


getContext()

ActionBar와 상관 없이 View를 조작하고자 할 때 사용됩니다. 심지어 long living object에서도 사용될 수 있지만, WeakRefernce로 사용되는 것을 권장합니다. 혹은 AlertDialog를 만들 때 쓰이기도 합니다. AlertDialog는 서로 다른 theme이 적용되기도 하는데, 이 경우에 사용됩니다. 위 언급된 바와 같이 theme과 연관이 깊습니다. 

Dark theme을 사용하다가 light theme을 적용하고 싶은 경우를 생각해 봅시다. 물론 매 생성마다 setColor 류의 메소드를 호출하여 일일히 구현해도 되지만, 아래 처럼 사용할 수도 있습니다.

ContextThemeWrapper contextThemeWrapper = 
new ContextThemeWrapper(getActivity(),
android.R.style.Theme_DeviceDefault_Light_Dialog);
LayoutInflater inflater = getActivity()
.getLayoutInflater()
.cloneInContext(contextThemeWrapper);

Context에 원하는 theme을 부여하여 inflate하는 예제입니다. 아래처럼 AlertDialog에 적용할수도 있겠지요.
AlertDialog.Builder builder = 
new AlertDialog.Builder(contextThemeWrapper);


Conclusion

이상으로 올바른 Context를 부르는 방법에 대해서 간략하게 알아봤습니다. 우리의 궁극적인 목표는 적절한 Context를 사용함으로써 불필요한 reference를 줄여 메모리 누수를 방지하는 것입니다. 

'programming > android' 카테고리의 다른 글

View - Widget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08
기본적인 메모리 관리  (0) 2018.03.06
Splash 화면 구성하기  (3) 2018.02.20
Kotlin Coroutines #2  (0) 2018.01.28

메모리 관리하기



reference: 

https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859





 GC?

JVM의 GC에 대해서 간략하게 정리를 해보겠습니다. 안드로이드 소스를 자바(혹은 코틀린)로 작성을 하지만, GC에 대해 자세히 공부해본 적이 없단 것은 살짝 부끄러운 부분입니다. 그래서 여기서 간략하게 정리하겠습니다.




어떤 자바 어플리케이션이든, 처음에 프로그램이 시작되고 객체를 생성하는 과정이 생길 것입니다. 그 처음 생성되는 객체는 GC의 root object가 됩니다. 그 root object로부터 또 다른 객체들이 생성되고 직/간접적으로 참조될 것입니다. 그 object들을 트리 구조로 관리하게 됩니다. 메모리 트리가 되는 것이죠.

GC는 이 트리의 root로 부터 순회하면서 방문되지 않는 객체들이 생기게 될텐데요, 그것이 바로 GC의 대상이 됩니다. 어떤 객체도 참조하고 있지 않은 메모리가 회수 대상이 되는 것이죠. 상당히 상식적이라고 생각할 수 있겠습니다.


 메모리 누수?

메모리가 누수된다는 의미는 더이상 쓸모가 없는 메모리를 계속해서 참조하고 있다는 것입니다. 사용 되지 않는 메모리임에도 불구하고 계속해서 어플리케이션으로부터 참조가 된다면, GC의 대상이 아니게 되기 때문에 불필요하게 메모리를 차지하겠죠. 바이트 단위로 메모리를 정말 타이트하게 메모리를 관리하기란 큰 프로젝트일수록 아무리 전문가라도 버거운 일일 것 같습니다. 하지만 기본적인 메모리 누수는 당연히 이루어져야 하는 일이겠죠? 

GC는 앱의 사용 메모리가 계속 증가하는 양상을 보인다면 short GC를 하게 되는데, 앱과 병렬로 진행되며 소요시간이 크지 않아 성능에 주는 영향이 미미합니다. 그럼에도 불구하고 정말 많은 메모리가 쓰이기 시작하면 "stop-the-world" GC가 수행됩니다. 이름이 말해주듯, 모든 동작을 멈추고 메모리를 회수하는 작업만 하게 됩니다. 어플리케이션 성능에 상당한 영향을 미칠 것입니다.


 어떻게 메모리 누수를 감지할까?

Android studio에서 기본적인 메모리 모니터를 제공하는데, 런타임에서 사용되는 메모리를 그래프 형식으로 시각화 해주기 때문에 상당히 유용합니다. 아마 거의 모든 개발자들이 이미 알고 있는 부분일 것입니다. 하지만 어떤 메모리가 많이 잡아먹고 있는지 분석하기 위해서는 메모리 덤프를 통해야 할텐데, 상당히 시간을 많이 소요하는 작업일 수 있습니다. 그런 부분을 도와주는 라이브러리가 있습니다. 

LeakCanary(https://github.com/square/leakcanary)가 그런 역할을 대신 수행해 줍니다. (역시 Square군요..)

이 라이브러리는 어플리케이션과 같이 동작하면서 필요할 때 메모리를 덤프 해주고, 메모리 누수가 일어날 것 같은 상황을 예측하고 원인 분석 및 스택 트레이스를 덤프 해주는 기능도 합니다. 굉장히 좋은 라이브러리 같네요. 조만간 사용해보고 새 블로그를 작성해보겠습니다.


흔한 메모리 관리 실패 사례

안드로이드(를 비롯해서 다른 플랫폼도 어쩌면 동일할 수 있는) 클라이언트에서 발생하는 메모리 실패 사례를 살펴보겠습니다. 기본적인 아이디어는 위에서 언급한 바와 마찬가지로, 변경되거나 더 이상 사용되지 않는 객체를 참조하는 일을 저지르는 사례들 입니다.

1. Unregistered Listeners

어떤 모듈로 부터 데이터를 얻어온 후 뷰에 적용하여 보여주는 역할이 클라이언트 대부분의 작업일 것입니다. 그럴 때 사용하는 것이 listener인데요, 그 데이터를 사용하는 액티비티가 죽거나 더이상 사용되지 않음에도 모듈이 계속 갖고 있는 때입니다. 굉장히 자주 일어나는 실수 중에 하나이며 놓치기 쉬울 뿐더러 발생하는 누수도 상당합니다.

해결법은 간단합니다. 액티비티의 onDestroy()에서 그 모듈로부터 자신을 listener 등록 해제 하는 것입니다.

2. Inner Classes

클래스 안의 클래스를 작성하는 경우는 많습니다. 액티비티에서 새로운 thread나 AsyncTask를 수행한다고 가정하면, 아래와 같은 코드가 되겠죠:

public class BadActivity extends Activity {
    private TextView mMessageView;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
        new LongRunningTask().execute();
}
    private class LongRunningTask extends AsyncTask<Void, Void, String> {
        @Override
protected String doInBackground(Void... params) {
return "Am finally done!";
}
        @Override
protected void onPostExecute(String result) {
mMessageView.setText(result);
}
}
}

문제가 딱히 발견되지 않을 수도 있겠습니다만, AsyncTask가 시간이 많이 걸리는 작업을 한다고 가정한다면? 위의 코드로는 액티비티보다 AsyncTask가 더 오래 살아있을 수도 있습니다. 그런 경우, AsyncTask는 더이상 데이터를 뿌려줄 액티비티가 없음에도 불구하고 불필요하게 액티비티의 TextView를 잡고있게 되는 것입니다. 상상만 해도 끔찍하네요.

이런 경우, 참조하는 객체를 WeakReference로 갖고있으면 간단히 해결할 수 있겠습니다. (GC가 수행되면 수거되는 메모리 대상)

3. Anonymous Class

2번 경우와 똑같습니다. 익명 클래스는 아주 유용하지만 메모리 누수에 쉽게 노출됩니다. 흔히 예를 들자면, Retrofit으로 API를 호출하고 그에 대한 응답 Callback을 작성할 것인데, 이를 익명 클래스로 작성하는 경우가 있을 것입니다.

public class MoviesActivity extends Activity {
    private TextView mNoOfMoviesThisWeek;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);
        MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();
repository.getMoviesThisWeek()
.enqueue(new Callback<List<Movie>>() {

@Override
public void onResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies = response.body().size();
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
                    @Override
public void onFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}

onResponse()가 호출되기 전에 액티비티가 무조건 살아야 있어야만 할 것입니다. 그래야 메모리가 누수되지 않겠죠? 2번과 똑같은 상황입니다. 

4. Wrong Contexts

안드로이드에서 Context는 매우 유용하게 여기저기서 사용되는 객체입니다. 그러나 어플리케이션, 액티비티, 서비스에서 모두 같은 Context를 사용하는 것이 아닙니다.

이를 잘 이해하고 적절한 Context를 참조하여 엉뚱한 메모리 누수가 발생하지 않을 필요가 있겠습니다.

이 부분 다음 포스트에서 작성해보도록 하겠습니다. 


Conclusion

위 사례들에서 관통되는 공통적인 이슈는, 액티비티와 그 상호작용하는 모듈의 lifecycle이 서로 일치하지 않는 데서 오는 것으로 볼 수 있습니다. AsyncTask나 Retrofit 모듈 모두, 액티비티에서 발생하는 상황을 모르고 있기 때문에 엉뚱한 메모리를 참조하거나 심지어 크래쉬까지 발생시키는 것입니다. 결론적으로, 액티비티가 회전되거나 파괴될 때 그 상황을 모듈에 적절히 알려야할 필요가 있습니다.

최근 구글에서 제안한 설계 라이브러리 중에서 ViewModel, LiveData 등은 모두 이러한 점을 보완하기에 적절한 것 같습니다. 위 언급과 마찬가지로 모든 문제는 모듈들이 액티비티의 lifecycle을 인지하지 못하는 것에서 비롯됩니다. 구글은 이 라이브러리들을 lifecycle-aware라는 표현으로 수식하였는데요, 말 그대로 모듈들에게 액티비티의 현재 lifecycle을 인지하도록 강제하여 위와 같은 사태를 좀 더 효율적으로 관리할 수 있을 것 같습니다.


'programming > android' 카테고리의 다른 글

Companion Objects in Kotlin  (0) 2018.04.08
어떤 Context를 써야 하나요?  (0) 2018.03.26
Splash 화면 구성하기  (3) 2018.02.20
Kotlin Coroutines #2  (0) 2018.01.28
Kotlin Coroutines #1  (1) 2018.01.27

Splash 화면 구성하기



reference: 

https://android.jlelse.eu/the-complete-android-splash-screen-guide-c7db82bce565




 방법

 1. Launcher Theme (The good)

  1. 2. Launcher Theme with a Dedicated Splash Activity (The okay)
  2. 3. Timers (The bad)
  3. 4. Smart Timers (The ugly)


 Launcher Theme

Activity가 onCreate() 조차도 불러지기 전, 

즉, 메모리에 앱이 올라가는 과정에다가 사용자가 지정한 화면을 넣는다는 것이 키포인트입니다.

앱 theme의 attributes 중에 windowBackground을 override 하는 것인데요, 

그 말인 즉, windowBackground라는 친구는 말그대로 activity 화면 뒤의 백그라운드를 뜻하는 것 같습니다.

windowBackground 위에 activity의 layout이 그려지기 때문에, 

그 백그라운드에 내가 원하는 화면을 그려넣겠다는 의미죠.

아주 단순합니다.


 Override windowBackground


<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Your AppTheme or other themes/styles here -->
<!-- The launcher theme. It sets the main window background to the launch_screen drawable -->
<style name=”AppTheme.Launcher”>
<item name=”android:windowBackground”>@drawable/launch_screen</item>
<!-- Optional, on Android 5+ you can modify the colorPrimaryDark color to match the windowBackground color for further branding-->
<!-- <item name="colorPrimaryDark">@android:color/white</item> -->
</style>
</resources>
view rawstyles.xml hosted with ❤ by GitHub

위 예제와 같이, AppTheme의 windowBackground를 지정해주고,

launcher activity의 테마를 위 테마로 지정해주기만 하면됩니다.

launch_screen의 이미지가 표시됩니다.



Conclusion


일반적이고 naive한 splash 구성 방법은 보통 timer나 animation을 통할 것 같은데요,

원본 포스트에서 위와 같이 좋은 방법, 나쁜 방법이라고 소개를 해놓은 점은 굉장히 자극적이라는 생각이 듭니다.

그래서 line-through 처리를 한 것이기도 하구요,

사실 원본 포스트에서도 밑 부분에는 각 방법마다 장단점을 작성해놓긴 했습니다.


프로젝트마다 splash 화면이 구성되는 동안, 

간단한 작업을 하거나 animation을 보여주는 경우가 있습니다.

launcher theme 방법의 경우 그것이 불가능합니다. 단순히 이미지를 표시하는 역할인 것이죠.

머무를 시간을 정한다던지,

animation을 표현한다든지,

login 등 간단한 request를 한다든지 그 어떤 행위랑은 연관이 없습니다.


따라서, splash 화면에서 간단한 작업(무거운 작업은 당연히 피해야겠죠)을 하고자 한다면, 

따로 화면을 구성하는 것이 더 적절하지 않나 생각합니다.


예를 들어, 

splash 화면을 뿌리는 동안 서버에 나의 상태를 요청하고, 

그상태에 따라 넘어가는 activity가 다른 경우가 있겠습니다.

물론 가능은 하겠지만, 하나의 activity에 splash가 종속되는 바람에 

launcher activity의 코드가 조금 장황해질 것 같다는 의견입니다.


따라서, 어떤 방법을 택할지는 본인의 판단하에 선택하면 될 것 같습니다 :)



'programming > android' 카테고리의 다른 글

어떤 Context를 써야 하나요?  (0) 2018.03.26
기본적인 메모리 관리  (0) 2018.03.06
Kotlin Coroutines #2  (0) 2018.01.28
Kotlin Coroutines #1  (1) 2018.01.27
Tools로 xml layoyt의 preview 제대로 표시하기  (0) 2018.01.22

Kotlin Coroutine #2



reference: 

https://hellsoft.se/simple-asynchronous-loading-with-kotlin-coroutines-f26408f97f46





 Coroutine Context

이전 블로그에서 했던 launch {}는 임의의 thread의 coroutine에서 작업을 수행합니다. 만약 작업이 수행될 coroutine을 명시하고 싶다면 어떻게 해야할까요. 여기서 제공되는 것이 coroutine context입니다.


 launch with context

아래 예제에서는 content resolver로부터 어떤 이미지를 불러온 후, view에 반영하는 작업입니다.

당연히 이미지를 불러오는 작업은 disk IO이기 때문에 main thread에서는 피하는 게 좋겠죠.

val job = launch(Background) {
val
uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,
launch(UI) {
imageView.setImageBitmap(bitmap)
}
}

launch(Background)에서 Background는 무엇일까요? 

바로 CoroutineContext입니다. 

아래처럼 만들어 주면 됩니다. 2개의 fixed size를 갖는 thread pool로부터 CoroutineContext를 가져왔습니다. 이름은 "bg"네요.

internal val Background = newFixedThreadPoolContext(2, "bg")


그렇다면 아래의 UI는 무엇인가요? 네, 물론 Android의 main thread입니다. 애초에 이렇게 제공을 해주네요. 선언은 아래와 같이 돼있었습니다.

val UI = HandlerContext(Handler(Looper.getMainLooper()), "UI")


Conclusion

Thread나 Executor 등을 통해 concurrent programming을 하는 것이 Java에서는 아주 일반적이지만, Kotlin에서는 다른 방식을 제안하려고 하는 것 같습니다. Coroutine은 light-weight thread로 설명할 수 있으며, block 대신 suspend를 하기 때문에 thread에 비해 switching 비용이 매우 적습니다. Continuation Passing Style을 통해 그것이 가능하다고 합니다.

 여기에서 참고한 글을 정리했습니다.


Q. Coroutine vs thread, 언제 어떤 것을 사용해야 하나?

A. Coroutine는 다른 작업을 기다리는 용도의 비동기 작업을, thread는 CPU-intensive한 작업을 할 때 사용하면 된다.


Q. Light-weight thread라는 설명은 적절치 않아 보인다, 왜냐면 coroutine은 thread pool로 부터 thread를 가져와서 그위에 실행을 하니까. 차라리 "task"라는 설명이 더 와닿지 않느냐? (Thread 위의 Runnable 정도의 의미인 것 같네요)

A. Light-weight thread라는 표현은 표면상의 의미고, 개발자 본인이 보기에 따라 thread도 마찬가지 이듯 다른 의미가 될 수 있을 것 같다.


Q. Coroutine이 thread와 비슷하다면, 서로 다른 coroutine 간에 공유되는 상태들에 대한 동기화 작업이 필요 할 것 같다.

A. Coroutine에서는 가급적이면 가변한 공유 상태를 가지기를 추천하지 않는다. 


'programming > android' 카테고리의 다른 글

기본적인 메모리 관리  (0) 2018.03.06
Splash 화면 구성하기  (3) 2018.02.20
Kotlin Coroutines #1  (1) 2018.01.27
Tools로 xml layoyt의 preview 제대로 표시하기  (0) 2018.01.22
Kotlin Standard Functions  (0) 2018.01.22

+ Recent posts