Android2017.06.08 13:49


https://android-developers.googleblog.com/2017/05/android-instant-apps-is-open-to-all.html


몇몇 선택된 개발자들에게만 허용되었던 안드로이드 인스탄트앱개발이 드디어 모든 개발자들에게 오픈되었다.


몇몇 선택된 개발자들에게만 허용되었던 안드로이드 인스탄트앱개발이 드디어 모든 개발자들에게 오픈되었다.


올초부터 안드로이드 인스탄트앱을 테스트하기 시작했다. 설치없이 실행가능한 앱이다. 우리 놀라운 개발자커뮤니티에 감사한다. 그들은 수많은 피드백을 우리에게 주었다. 그로 인해 end-to-end 제품경험을 개선하는데 큰 도움이 되었다.


오늘 우리는 모든 안드로이드 개발자들에게 오픈한다. 누구든 빌드하고 배포할 수 있다. 이미 50여개이상의 새로운 경험을 하는것이 가능하다. HotPads, Jet, 뉴욕타임즈, Vimeo그리고 One Football등이다. 이들은 아주 짧은 기간동안 서비스되고 있지만 아주 긍정적인 결과를 보여준다. 예를 들어 Jet과 HotPads는 두자리수의 주문이 증가했다.


초기파트너들로부터의 피드백은 오늘 제공하게 되는 개발툴을 직접 형성했다. 


인스탄트앱을 개발하려면 먼저 developer.android.com에 들어가서 Android Studio 3.0 preview와 Android Instant Apps SDK를 다운로드하라. 계속해서 단일 코드배이스를 사용하게 될것이다. Android Studio는 필요에 따라 기능을 다운로드 할 수 있도록 앱을 모듈화하는 데 필요한 도구를 제공합니다. 모든 앱들이 다르지만 우리는 초기파트너들과 최종 툴로 인스탄트앱을 개발하는데 보통 4~6주정도의 시간이 소요되는것을 보았다.


앱을 한번 개발해 놓으면 Play Console는 인스탄트앱의 배포에 대한 지원을 제공한다. 당신은 그저 설치가능한 APK와 함께 인스탄트앱 APK들을 업로드하면 된다.


40개국이상의 최신 안드로이드 디바이스에서 계속 인스탄트앱이 증가하고 있다. 그리고 Android O에서 인스탄트앱을 위한 더 향상퇸 런타임 샌드박스, 앱용량 감소를 위한 라이브러리의 공유, 런처통합 지원을 구축했다.


더 알고 싶으면 g.co/instantApps를 방문해라. 이미 "Introduction to Android Instant Apps" 세션을 올려두었고 Google I/O Youtube channel에서도 시청할 수 있다.


신고
Posted by 삼스
Android/Wear2015.08.24 16:43


안드로이드 웨어는 HTTP통신 같은 인터넷 억세스를 지원하지 않는다고 한다. 이에 어떻게 데이터 전송을 지원하는지 알아보기 위해 아래 문서를 정리해 보았다.  실제로 이 API를 wrapper하여 http get을 지원하는 lib를 만들어서 공개한 개발자도 있다.

좀더 정확히 얘기 하면 Bluetooth로 연결이 되지 않은 상태에서 wifi에 연결이 되어 있다면 인터넷 억세스가 다이렉트로 가능하다. 하지만 bluetooth가 연결이 되는 순간 먹통이 된단다. 동시에는 안된다는데... 왜 안될까??? 단말과 워치간의 데이터 동기화를 위해서는 이 때는 Data Layer API로 bluetooth를 통해서 전송하는 방법을 사용해야 한다고 한다. 이건 맞다고 보는데 이 때 wifi가 진짜 안될까????? 워치가 없으니 확인할 방법은 없고.. 궁금하긴 하다. 워치 구매하면 제일먼저 해야할 일이다.


http://developer.android.com/training/wearables/data-layer/index.html


Data Layer API for Wearable은 Google Play Service의 일부로 폰과 웨어러블간의 통신채널을 제공한다.

데이터오브젝트의 셋을 제공하는데 전송 및 동기화 그리고 관련 콜백리스너를 제공한다.


Data Items

  DataItem한개는 단발과 웨어러블간에 자동으로 동기되는 데이터 스토리지를 제공한다.


Messages

  MessageApi클래스는 메시지들을 전송할 수 있으며 RPC에 기반하여 웨버러블에서 단말의 미디어플레이어를 제어한다거나 단말에서 웨어러블에 인텐트를 시작하거나 할 수 있다.

  Message는 단방향 및 양방향 통신을 모두 제공한다.

  단말과 웨어러블이 연결되면 메세지를 전달하기 위해 시스템은 큐잉하고 성공코드를 반환한다. 디바이스가 연결이 되지 않으면 에러를 반환한다. 


Asset

  Asset객체는 이미지 같은 바이너리데이터를 전송하기 위한 객체이다. Data item에 asset을 첨부하면 시스템은 자동으로 잘 전송해준다. 


WearableListenerService(for services)

  WearableListenerService를 확장하여 서비스에서 중요한 이벤트를 처리할 수 있다. 시스템은 lifecycle을 관리하고, data item이나 message를 전송해야 하면 서비스에 바인딩하며, 서비스가 더이상 필요없으면 언바인딩한다.


DataListener(for foreground activities)

  액티비티에서 DataListener를 구현하여 액티비티가 포그라운드로 진입히 중요한 이벤트를 처리할 수 있다. WearableListenerService대신에 사용하면 앱이 사용중에만 변경을 감지할 수 있다.


Channel

  ChannelApi클래스를 사용하여 큰 data item들을 전송할 수 있다. 음악이나 동여상파일들을 단말에서 웨어러블에 전송이 가능하다. ChannelApi는 다음과 같은 잇점이 있다.

  • 하나이상의 단말간에 대용량 데이터 파일을 전송한다(DataItem에 Asset으로 자동동기화를 하지 않고 가능). 디스크용량을 절약이 가능하다. DataApi클래스의 경우 동기화하기 전에 Asset을 복사한 후 수행하기 때문에 디스크용량 절약에 불리하다.
  • MessageApi 클래스를 사용하여 아주 큰사이즈를 안정적으로 전송이 가능하다.
  • network server나 micorophone같은 스트림데이터를 전송할 수 있다.


주의) 단말과 웨어러블간의 통신을 위해서 설계가 된 API이기 때문에 두 단말간에 셋업해야 하는 API가 있다. 예를 들어 low-level 소켓통신을 시도해서는 안된다.


안드로이드 웨어는 여러개의 디바이스와 동기화를 지원한다. 한단말에서 노트를 편집하면 연결된 모든 디바이스에 동기화가 된다. 동기화를 위해 구글 서비스에 클라우드 노드가 생성되고 이 노드를 통해서 동기화가 된단다.



신고
Posted by 삼스
Android2015.05.29 11:21


사내에서 공통으로 작성된 라이브러리들을 안드로이드 스튜디오에서 임포트하여 사용하고자 하는 경우가 있을 것이다. 이 경우에 대한 가이드이다.

nexus repository가 구축되어 있다고 가정한다.

절차는 다음과 같다.

1. 라이브러리 프로젝트에서 라이브러리를 사내 메이븐레포지트리에 등록한다.

uploadArchives {

    repositories {

        apply plugin: 'maven'

        mavenDeployer {

            repository(url: "http://localhost:8000/nexus/content/repositories/snapshots") {

                authentication(userName: "userid", password:"userpassword")

            }

            pom.version = "1.0-SNAPSHOT"

            pom.artifactId = "mylib"

            pom.groupId = "com.t5online"

        }

    }

} 


2. 라이브러리를 사용할 프로젝트에서 사내 레포지트리를 추가한다.


buildscript {

    repositories {

        jcenter()

        maven {

            url "http://localhost:8000/nexus/content/groups/public"

            credentials {

                username 'userid'

                password 'userpassword'

            }

        }

    }

    dependencies {

        classpath 'com.android.tools.build:gradle:1.1.0'

    }

}

allprojects {

    repositories {

        jcenter()

        maven {

            url "http://localhost:8000/nexus/content/groups/public"

        }

    }


3. 라이브러리를 사용할 프로젝트에서 등록한 라이브러리의 디펜던시를 추가한다.

dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:appcompat-v7:22.0.0'

    compile 'com.t5online:mylib:1.0-SNAPSHOT@aar'


위와 같이 진행하면 내부적으로 관리되는 메이븐 저장소의 라이브러리를 등록하고 다른 프로젝트에서 스크립트 수정만으로 라이브러리 디펜던시를 해결할 수 있다.

안드로이드 스튜디오에서 다른 라이브러리를 모듈로 임포트를 하면 소스까지 프로젝트로 추가되어 버리는 바람에 라이브러리 수정시 원에 위치의 소스가 관리가 안되는 문제가 있는데 이렇게 하면 원래의 라이브러리는 그 위치에 두고 관리가 되기 때문에 편리한것 같다. 다만 라이브러리 디버깅시가 문제인데 이는 라이브러리를 제작하는 측에서 알아서 지원해주어야 할 문제인 걸로...


* aar은 Android Stdio가 만들어내는 라이브러리형식인데 기존의 ADT에서 생성하는 jar과 다른 점은 resource파일들을 함께 포함한다는것이다. 기존에 jar로 만든 라이브러리가 참조하는 resource가 있는 경우 배포시 jar과 함께 리소스파일을 배포했던 불편함을 해소할 수 있다.




신고
Posted by 삼스
Android2015.04.15 16:50



  • 객체 생성을 피하라. 객체를 생성하는 작업은 다른 작업들에 비해 로드가 많이 걸리는 작업이며 가비지컬렉션대상이 많아지는것도 성능에 장애요소이다.
  • Native method를 활용하라. 자바코드보다 동일작업시 10~100배 빠르다. 따라서 복잡하고 오래걸리는 연산작업은 native method를 사용하라.
  • 인터페이스 타입참조보다 클래스타입참조를 더 선호하라. 임베디드환경에서는 인터페이스타입참조를 통해 virtual method를 호출하는것이 본래 객체에 대한 class type을 참조하는것보다 2배 느리다.

Map m = new HashMap();  // 인터페이스로 참조하여 참조연산이 더 일어남

HashMap m = new HashMap();  // 명시적으로 참조하므로 추가연산발생안함

  • 가상연결보다 정적연결을 선호하라. 객체필드에 접근할 필요가 없다면 메소드를 static으로 하면 더 빠르다. 가상메서드테이블에 접근하지 않기 때문.
  • 내부에서 getter/setter의 사용은 피하라.
  • 필드참조들을 캐시하라. 멤버변수에 접근하는것이 지역변수에 접근하는것보다 훨씬 속도가 느림. 메소드내에서 멤버젼수에 접근을 많이 하는 경우 로컬변수로 가져와서 처리하면 개이득
  • 상수를 final로 선언하라. static 멤버변수가 초기값을 가지고 있고 값이 변경될 필요가 없다면 final키워드를 사용하면 값을 참조할때 field lookup을 사용하지 않으므로 더 빠르다.
  • Enhanced for loop를 사용하라. for(type n : array) {} 형식의 구문을 말하며 iterator를 사용하는것 보다 일반적으로 빠르다.
  • Enum자료형을 피하라. 코드가독성이나 잘못값을 할당하는 경우를 컴파일시 에러가 발생함으로써 매우 편리하나 코드 크기와 속도면에서는 손해다. 코드사이즈의 크기도 성능에 악영향을 미친다.
  • 내부클래스를 갖는 클래스의 경우 멤버변수를 private대신에 package 접근자를 사용하는것이 좋다. private으로 하면 내부적으로 컴파일러가 접근메서드를 생성하여 해당 메서드를 통해서 접근하기 때문에 접근시마다 함수호출이 발생하여 성능에 영향을 미친다. 

  • Float을 피하라. 일부 모바일 단말의 경우 부동소수점 연산의 지원이 미비할 수 있다.
  • 안드로이드에서 제공하는 개선된 성능의 데이터구조

LruCache

SparseArray

SparseBooleanArray

SparseIntArray

Pair

  • 메인스레드에서 많은 작업을 하지 않아야 한다.

키/터치 이벤트 처리

뷰그리기

라이브사이클 관리

  • 레이아웃 단순 화 및 지연초기화 이용
  • LinearLayout보다는 RelativeLayout이 더 단순한 처리를 하게 됨.
  • ViewStub를 사용해서 객체생성을 지연시킨다.
  • onCreate, onResume등 lifecycle관련 메소드에서는 최소한의 작업만 수행해야 ANR을 피할 수 있으며 사용자의 성능에 대한 민감도를 피해갈 수 있다. 100ms~200ms가 사용지연에 대한 임계치라는 조사가 있다.
  • StrictMode 활용

앱플리케이션 개발동안 StrictMode를 사용함으로써 느려지거나 느려질 가능성이 큰 코드를 검출할 수 있다.

Policy와 penaly를 랩에서 코드레벨로 설정하여 사용가능하다. Policy는 정책을 penalty는 위반시 표시방법을 나타낸다.

Policy는 ThreadPolicy와 VmPolicy 두가지를 설정할 수 있다.

ThreadPolicy: 특정 스레드에 file, network 접근 부분을 감지하여 위반되는 위치를 잡아준다.

VmPolicy : Memory leak의 위치를 감지해 Log, Dialog, DropBox, Splash, 강제종료와 같은 다양한 방식으로 알린다.

ThreadPolicy

detectNetwork() : 네트워크 사용

detectDiskRead() : disk read

detectDiskWrite() : disk write

detectAll() : all

detectCustomSlowCell() : api11이수 StrictMode.noteSlowCall()가 선언된 부분을 감지함.

VmPolicy

detectActiveLeaks() : Activity 서브클래스의 릭감지

detectLeakedClosableObjects(): File/Network IO관련 API사용시 close를 명시적으로 호출안하고 종료할 경우 나오는 리소스 leak감지

setClassInstanceLimit() : 특정클래스가 힘에 생성되는 최대 개 수를 설정하여 object leak현상을 모니터링 할 수 있다.

Panalty

penaltyLog() : logcat으로 표시

panaltyDropBox() : DropBoxManagerService에 로그가 저장되며 정보가 detail하여 추천된다. adb shell dumpsys dropbox data_app_strict_mode —print > strict.txt 로 추출 후 꺼내서 별것을 추천한다.

panaltyDialog() : dialog창으로 표시

penaltyFlashScreen() : 화면상으로 빨간색 테두리의 사각형 splash로 보여준다.

penaltyDeathOnNetwork() : Main스레드에서 network access가 진행되면 무조건 어플리케이션이 강제종료된다. detectNetwork()가 활성화되어 있어야 한다.

신고
Posted by 삼스
Android2015.04.13 18:04


http://developer.android.com/training/displaying-bitmaps/index.html 의 요약임을 밝힌다.

앱사용성에 이슈를 발생시키지 않으면서 메모리제한까지 피할 수 있는 방법에 대해 정리한다. 피하지 않으면 그 유명한 OOM(Out of memory)에 손도 못쓰고 당하게 될것이다.

모바일디바이스는 시스템리소스가 제한적인데 16MB정도의 적은 메모리만이 애플리케이션에 허용되는데 반해 비트맵은 많은 메모리를 사용하게 된다. 그리고 안드로이드 UI는 종종 여러개의 비트맵을 동시에 사용하는 경우가 있다. 이런것들이 어쩔 수 없이 메모리 이슈를 발생시킨다.


아주큰 비트맵을 효율적으로 로딩하기

애플리케이션의 메모리 제한에 문제없이 비트맵을 로딩하는 방법.

이미지를 표시할 UI콤포넌트는 대체적으로 이미지보다 작게 표시된다. 이 때는 이미지 원본사이즈가 아니라 표시될 콤포넌트의 크기에 맞게 로드하는것이 이득이다.

BitmapFactory클래스가 비트맵을 디코딩하기 위한 여러 메소드(decodeByteArray(), decodeFile(), decodeResource())들을 제공한다. 이 메소드들은 비트맵을 디코딩할때 메모리할당을 시도하기 때문에 OOM이 아주 쉽게... 간단하게 발생될수 있다. 이 메소드들에는 BitmapFactory.Options 인자를 받을 수 있는데 inJustDecodeBounds를 true를 주어서 디코딩시 메모리 할당을 피하면서 이미지의 가로/세로 사이즈를 알아낼 수 있다. 이를 이용해서 메모리 할당을 조절할 수 있다.


BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

int imageHeight = options.outHeight;

int imageWidth = options.outWidth;

String imageType = options.outMimeType;


인자 스케일다운된 버전을 메모리에 로드해보자

이미지의 사이즈를 알기 때문에 이미지을 풀사이즈를 사용할지 사이즈를 다운할지 결정할 수 있다. BitmapFactory.Options의 inSampleSize를 true로 하며 디코더가 더작은 샘플로 디코딩하도록 할 수 있다.가령 2048x1536사이즈의 이미지를 inSampleSize 4를 주면 512x384사이즈로 로딩한다. 이는 12MB의 메모리 사용량을 0.75MB로 줄이는 효과가 있다.

다음 코드는 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;

}


다음은 스케일을 적용한 비트맵로딩 예이다. inJustDecodeBounds를 통해 원본의 크기를 얻은 후 필요한 사이즈에 맞도록 스케일하여 비트맵을 생성하여 반환한다.

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


다음 메소드 호출은 100, 100사이즈에 맞도록 비트맵 이미지를 스케일하여 적용하는 예이다.

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


비동기로 비트맵 로딩하기


BitmapFactory.decodeXXX 류의 메소드들은 main UI스레드에서 호출되면 안된다. 성능이슈를 발생시킨다. 버벅거리게 될거고 앱 사용자들은 모두 떠날것이다. 

AsyncTask로 백그라운드에서 UI스래드와 분리되어 로딩할 수 있으며 이 때 동시에 수행시 발생하는 이슈에 대해서도 살펴보겠다.

AsyncTask는 별도의 스레드로 어떤 작업을 하고 그 결과를 UI thread에 반환할 수 있도록 해준다. 이 클래스를 extends하고 메소드를 오버라이드하면 되는데 다음 에제를 보자.

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를 처리하는 이유는 해당 메모리릭을 염두에 둔것인데 일반참조를 사용하게 되면 imageView의 참조가 유지되어 GC대상에서 제외될수 있기 때문이다.

** WeakReference로 참조하는 객체는 언제든 메모리에서 해제될수 있기 때문에 사용할 때 get()메소드로 참조를 얻어온 후 null인지 체크한 후 사용해야 한다.

다음 예제는 아주 단순하게 비트맵을 비동기로 새로운 테스크를 만들어서 실행하는 예이다.


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


ListView나 GridView는 childview를 재사용하는 로직으로 구현되어 있다. 이런 경우 AsyncTask로 로딩하게 되면 로딩이 완료되었을 때 view가 사용자의 스크롤로 인해 다른 뷰가 될 수 있다.

이 문제를 해결하기 위해서 Worker task를 갖는 Drawable객체를 사용하게 된다. 여기서는 BitmapDrawable를 사용하여 task가 완료되면 imageView에 이미지를 표시하게 된다.


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


AsyncDrawable은 WorkerTask를 WeakReference로 갖고 있고 getter를 노출하고 있다. BitmapWorkerTask를 실행하기 전에 AsyncDrawable를 생성하여 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);
        task
.execute(resId);
   
}
}


cancelPotentialWork메소드는 다른 테스크가 이미  imageview에 연관되어 있다면 이전 task를 취소한다. 동일한 테스크로 시도하게 되면 아무것도 하지 않는다.


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


getBitmapWorkerTask는 ImageView에 연결되어 있는 WorkerTask를 반환한다.


private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   
if (imageView != null) {
       
final Drawable drawable = imageView.getDrawable();
       
if (drawable instanceof AsyncDrawable) {
           
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           
return asyncDrawable.getBitmapWorkerTask();
       
}
   
}
   
return null;
}


마지막 작업은 onPostExecute에서 task가 취소되었는지 확인하고 ImageView가 유효한지 확인 후 표시한다.


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에 아주 적합한 솔루션이다. 또는 child view를 재활용하는 뷰컴포넌트들에서 사용할 수 있는 패턴이다. GridView의 경우 getView()메소드가 테스크와 뷰를 연계해주는 어댑터역할을 한다.


비트맵 캐싱하여 사용하기

아주 많은 이미지를 로딩해야 하며 빈번하게 사용되는 경우라면 캐싱을 고려해야 한다. 메모리캐싱과 파일캐싱이 해당되겠다.

메모리 캐시

메모리를 좀더 사용되겠지만 속도면에서 많은 이득이 있다. LruCache가 비트맵을 캐시하는데 아주 적당하다. LruCache가 사용되기 전에는 SoftReference와 WeakReference를 이용해서 캐싱을 구현하였었는데 이는 Android 2.3의 gc에서 비효율을 야기한다고 한다. 머 무튼 LruCache를 사용하면 나중에 추가한 오브젝트는 LinkedHashMap으로 참조되고 최대개수가 넘어가면 오래된걸 제거하도록 되어 있다.

LruCache의 적절한 사이즈를 결졍하기 위해서 고려해야 할것들이 여러가지가 있겠으나 정해진 크기나 공식은 없다. 전적으로 당신의 분석에 따라서 알아서 결정해야한다. 캐시크기가 크면 메모리가 부족해질거고 캐시크기가 작으면 앱에 로드가 걸릴것이니 적당한 선에서 합의를 해야 함. 참고할만한 가이드는 아래 참고하고

  • How memory intensive is the rest of your activity and/or application?
  • How many images will be on-screen at once? How many need to be available ready to come on-screen?
  • What is the screen size and density of the device? An extra high density screen (xhdpi) device like Galaxy Nexus will need a larger cache to hold the same number of images in memory compared to a device likeNexus S (hdpi).
  • What dimensions and configuration are the bitmaps and therefore how much memory will each take up?
  • How frequently will the images be accessed? Will some be accessed more frequently than others? If so, perhaps you may want to keep certain items always in memory or even have multiple LruCache objects for different groups of bitmaps.
  • Can you balance quality against quantity? Sometimes it can be more useful to store a larger number of lower quality bitmaps, potentially loading a higher quality version in another background task.

다음은 LruCache로 구현한 샘플임


private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
   
// Get max available VM memory, exceeding this amount will throw an
   
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
   
// int in its constructor.
   
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

   
// Use 1/8th of the available memory for this memory cache.
   
final int cacheSize = maxMemory / 8;

    mMemoryCache
= new LruCache<String, Bitmap>(cacheSize) {
       
@Override
       
protected int sizeOf(String key, Bitmap bitmap) {
           
// The cache size will be measured in kilobytes rather than
           
// number of items.
           
return bitmap.getByteCount() / 1024;
       
}
   
};
   
...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
   
if (getBitmapFromMemCache(key) == null) {
        mMemoryCache
.put(key, bitmap);
   
}
}

public Bitmap getBitmapFromMemCache(String key) {
   
return mMemoryCache.get(key);
}


비트맵을 로드하기전에 캐시에 비트맵이 있는지 확인하고 로드한다.

public void loadBitmap(int resId, ImageView imageView) {
   
final String imageKey = String.valueOf(resId);

   
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
   
if (bitmap != null) {
        mImageView
.setImageBitmap(bitmap);
   
} else {
        mImageView
.setImageResource(R.drawable.image_placeholder);
       
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task
.execute(resId);
   
}
}


BitmapWorkerTask는 로드가 완료되면 메모리캐시에 추가한다.


class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
   
...
   
// Decode image in background.
   
@Override
   
protected Bitmap doInBackground(Integer... params) {
       
final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources
(), params[0], 100, 100));
        addBitmapToMemoryCache
(String.valueOf(params[0]), bitmap);
       
return bitmap;
   
}
   
...
}


디스크 캐시 이용하기

메모리캐시만으로는 부족하다. 너무 많이 사용하는것 이외에도 전화가 온다던가 하여 앱이 백그라운드상태가 되었을 때 앱은 종료가 될 수 있고 그럴경우 캐시도 제거될것이다. 앱이 다시 로드되면 또다시 이미지를 로드해야 할것이다. 이를 개선하기 위해 디스크에 캐시를 하고 디스크에 캐시가 있으면 디스크에서 읽어들이는 방법이 있다. 메모리캐시보다는 느리지만 네트워크보다는 빠르며 메모리캐시와 함께 사용해도 될것이다.

** 갤러리앱에서 관리하는 이미지들을 읽어들일때는 ContentProvider를 이용하면 더 빠르게 접근이 가능하다.

안드로이드 소스에서 사용중인 DiskLruCache를 사용하는 샘플코드이다.


private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
   
// Initialize memory cache
   
...
   
// Initialize disk cache on background thread
   
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
   
new InitDiskCacheTask().execute(cacheDir);
   
...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
   
@Override
   
protected Void doInBackground(File... params) {
       
synchronized (mDiskCacheLock) {
           
File cacheDir = params[0];
            mDiskLruCache
= DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting
= false; // Finished initialization
            mDiskCacheLock
.notifyAll(); // Wake any waiting threads
       
}
       
return null;
   
}
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
   
...
   
// Decode image in background.
   
@Override
   
protected Bitmap doInBackground(Integer... params) {
       
final String imageKey = String.valueOf(params[0]);

       
// Check disk cache in background thread
       
Bitmap bitmap = getBitmapFromDiskCache(imageKey);

       
if (bitmap == null) { // Not found in disk cache
           
// Process as normal
           
final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources
(), params[0], 100, 100));
       
}

       
// Add final bitmap to caches
        addBitmapToCache
(imageKey, bitmap);

       
return bitmap;
   
}
   
...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
   
// Add to memory cache as before
   
if (getBitmapFromMemCache(key) == null) {
        mMemoryCache
.put(key, bitmap);
   
}

   
// Also add to disk cache
   
synchronized (mDiskCacheLock) {
       
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache
.put(key, bitmap);
       
}
   
}
}

public Bitmap getBitmapFromDiskCache(String key) {
   
synchronized (mDiskCacheLock) {
       
// Wait while disk cache is started from background thread
       
while (mDiskCacheStarting) {
           
try {
                mDiskCacheLock
.wait();
           
} catch (InterruptedException e) {}
       
}
       
if (mDiskLruCache != null) {
           
return mDiskLruCache.get(key);
       
}
   
}
   
return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
   
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
   
// otherwise use internal cache dir
   
final String cachePath =
           
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                   
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context
.getCacheDir().getPath();

   
return new File(cachePath + File.separator + uniqueName);
}


** 위 예제에서 Lock 객체는 디스크 작업중에 캐시에 접근하는것을 막아준다.

메모리캐시가 UI thread에서 확인되는 반면 디스크캐시는 백그라운드에서 확인한다. 디스크작업은 절대 UI thread에서 구동되면 안된다. 이미지프로세싱이 끝나면 비트맵은 메모리와 디스크캐시 2군데 추가된다.


Configuration change에 대한 처리


런타입에 설정이 변경되게 되면 가령 스크린오리엔테이션 안드로이드는 액티비티를 제거하였다가 재시작한다. 이 때 이미지처리를 간결하게 하여 사용성을 해쳐서는 안될것이다.


Fragment를 사용하는 경우 Fragment에 setRetainInstance(true)를 호출함으로써 메모리캐시의 참조를 통해 캐시를 유지하고 참조할 수 있다.


private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
   
RetainFragment retainFragment =
           
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache
= retainFragment.mRetainedCache;
   
if (mMemoryCache == null) {
        mMemoryCache
= new LruCache<String, Bitmap>(cacheSize) {
           
... // Initialize cache here as usual
       
}
        retainFragment
.mRetainedCache = mMemoryCache;
   
}
   
...
}

class RetainFragment extends Fragment {
   
private static final String TAG = "RetainFragment";
   
public LruCache<String, Bitmap> mRetainedCache;

   
public RetainFragment() {}

   
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
       
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
       
if (fragment == null) {
            fragment
= new RetainFragment();
            fm
.beginTransaction().add(fragment, TAG).commit();
       
}
       
return fragment;
   
}

   
@Override
   
public void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
       
setRetainInstance(true);
   
}
}





신고
Posted by 삼스
Android2015.04.02 16:25


Eclipse에서는 MAT를 플러그인으로 설치 및 사용이 가능하였지만 Android Studio로는 앱을 별도로 배포하는것을 받아서 사용할 수 있다.

아래 그 과정을 나열하였다.


1. MAT 다운로드

http://www.eclipse.org/downloads/download.php?file=/mat/1.4/rcp/MemoryAnalyzer-1.4.0.20140604-macosx.cocoa.x86_64.zip

2. Android Studio에서 DDMS실행

3. Update hprof

4. dump hprof

5. hprof-conv로 변환(SDK의 platform-tools에 있음)

6. 변환된 파일 MAT로 open(변환 안하면 안열림)


MAT에 대한 활용은 다음 포스트에...

신고
Posted by 삼스
Android2015.04.02 16:17


엄청 잘 설명되어 있음..

http://www.openeg.co.kr/289

신고
Posted by 삼스
Android2015.04.02 14:37



Map(HashMap)은 자바에서 굉장히 많이 사용되는 자료형이다. 이는 키로 값을 얻을 수 있는 것인데 HashMap은 키로 문자열등도 취할수 있기 때문에 사실상 무한대로 볼수 있다. 하지만 HashMap은 내부적으로 int범위의 bucket을 가지고 구현이 되기 때문에 계속 추가하다보면 충돌이 발생할 수 있다.

즉 "aa"와 "bb"키로 다른 값을 등록하였는데 읽을 때 동일한 값이 잃혀질 수 있다는 것이다.

이 로인 해 key.hashCode()를 적절히 구현하여 linked list를 활용하는 방안이 있다. 이른 해당 키의 값이 없으면 linked list로 추적하여 그 값을 읽어오는 방법이다. 이렇게 하면 충돌은 피할 수 있겠으나 결국 linked list와 별반 다르지 않다.

안드로이드에서는 이를 보완하기 위해 java.android.util.SparseArray를 제공한다.

이는 bucket인덱스 기준으로 0~100까지 저장되고 10000~10010까지 저장된다고 가정할 때 실제로는 110개만 bucket를 생성한다. 저장할 데이터가많으면 LongSparseArray를 사용하면 된다.

한가지 제약은 키값이 정수이어야 한다는 것이다. 이 부분만 뺴면 HashMap이나 linked list를 활용하는 것보다 빠르게 처리할 수 있다.

속도가 문제가 된다면 HashMap을 SparseArray로 변경하는것을 고민해볼때이다.


신고
Posted by 삼스
Android/App개발2014.07.16 13:31


http://developer.android.com/guide/practices/screens-distribution.html

모든 스크린사이즈에 적절하게 표시되도록 화면을 디자인할것을 권고하지만 태블릿만, 폰만 또는 특정크기이상의 스크린만 지원하고자 하는 경우가 있다. 그러려면 지원할 스크린에 대한 설정을 manifest에 추가하여 구글플레이같은 외부서비스에 의해 필터링이 되도록 할 수 있다.

그전에 멀티스크린지원에 대한 정확한 이해가 필요하고 그에 따라 구현해야 한다.

폰만 지원하기

일반적으로 큰스크린에 잘 맞도록 시스템이 동작하기 때문에 큰스크린에 대한 필터는 필요치 않다. Best Practies for Screen Independence에 잘 따랏다면 태블릿에서 아주 잘 동작할것이다.  하지만 스케일업이 생각처럼 동작하지 않는것을 발견할 수 있으며 애플리케이션을 다른 스크린설정으로 두개의 버전을 배포하려고 할 수 있다. 이 경우 <compatible-screens> 로 screen size와 density의 조합으로 조정할 수 있다. Google Play는 이 정보를 참조하여 애플리케이션을 필터링한다. 그리하여 단말에 적합한경우에만 다운로드 설치가 가능하도록 한다.

<screen> 엘리먼트를 포함해야 하는데 각 <screen>은 스크린설정정보(스크린크기, 스크린밀도)를 담는다. 두가지(screenSize, screenDensity)정보를 모두 담아야 한다. 하나라도 빠져있다면 Google Play는 그 필드를 무시할것이다.

만일 small, normal size screen을 지원하고 density는 무시하고자 한다면 총 8개의 <screen> 엘리먼트가 필요하다. 각 사이즈별로 density가 4가지이기 때문이다. 여기서 정의되지 않은 조합은 호환되지 않는것으로 간주된다. 다음은 이 경우에 대한 manifest예이다.

<manifest ... >

    <compatible-screens>

        <!-- all small size screens -->

        <screen android:screenSize="small" android:screenDensity="ldpi" />

        <screen android:screenSize="small" android:screenDensity="mdpi" />

        <screen android:screenSize="small" android:screenDensity="hdpi" />

        <screen android:screenSize="small" android:screenDensity="xhdpi" />

        <!-- all normal size screens -->

        <screen android:screenSize="normal" android:screenDensity="ldpi" />

        <screen android:screenSize="normal" android:screenDensity="mdpi" />

        <screen android:screenSize="normal" android:screenDensity="hdpi" />

        <screen android:screenSize="normal" android:screenDensity="xhdpi" />

    </compatible-screens>

    ...

    <application ... >

        ...

    <application>

</manifest> 


주의) 하지만 2014년 6월 현재 하나가 더 있다. xxhdpi 따라서 이제 10개가 필요하게 되겠다.

Note) <compatible-screens>엘리먼트는 리버스를 통해 호환 여부를 작은사이즈만 지원하는지 알수 있으나 <supports-screens>를 사용하면 density를 요구하지 않기 때문에 피할 수 있다.


태블릿만 지원하기

폰을 지원하지 않겠다면(즉 큰스크린만 지원하겠다면) 또는 작은 스크린에 대해 최적화하는데 시간이 필요하다면 작은스크린 디바이스는 설치안되도록 할 수 있다. 이 방법은 <support-screens>를 통해 가능하다.

<manifest ... >

    <supports-screens android:smallScreens="false"

                      android:normalScreens="false"

                      android:largeScreens="true"

                      android:xlargeScreens="true"

                      android:requiresSmallestWidthDp="600" />

    ...

    <application ... >

        ...

    </application>

</manifest>

두가지 방법이 있다.

  • "small"과 "normal"사이즈를 미지원으로 설정, 일반적으로 이런 디바이스는 태블릿이 아니다
  • 앱이 허용하는 영역이 최소 600dp이상으로 설정

첫번째는 android 3.1과 그 이 하의 디바이스들을 위한것이다. 왜냐하면 그 디바이스들은 일반화된 스크린사이즈에 기반하여 사이즈를 정의하였기 때문이다. requiresSmallestWidthDp속성은 android 3.2와 그이상을 위한것이다. 이는 dip의 최소값에 기반하여 요구되는 사이즈를 정할 수 있다. 예를 들면 600dp로 설정하면 일반적으로 7인치 이상의 스크린을 갖는 디바이스들이 해당된다.

당신이 필요로 하는 사이즈는 당현히 다를수 있다. 만인 9인치 이상의 스크린을 지원하고자 한다면 720dp로 설정할 수 있을 것이다.

앱은 requiresSmallestWidthDp속성을 빌드하기 위해 Android 3.2이상으로 빌드를 해야 한다. 이전버전은 에러가 날것이다. 가장 안전한 방법은 필요로 하는 API level에 맞게 minSdkVersion을 설정하는 것이다. 최종배포 질드시 빌드타겟을 3.2로 변경하고 requeiresSmallestWidthDp를 추가한다. 3.2이전버전은 런타임에 이 값을 무시할것이기 때문에 문제가 없을 것이다.

왜 가로사이즈만으로 스크린을 구분하는지 궁금하면 다음 링크를 참조해라. -> New Tools for Managing Screen Sizes.

여러화면을 지원하기 위해 필요한 모든 기술을 적용하여 가능한 많은 장치에 앱을 사용할 수 있도록 하기 위해 <compatible-screens>를 사용하거나 모든 화면구성에 호환성을 제공할 수 없을 경우에만 <support-screens>을 사용하거나 또는 특정한 화면구성의 다른 세트에 대한 다른 버전의 앱을 제공할 수 있다.


다른 화면을 위한 여러개의 APK제공

하나의 APK만을 제공하는것이 권장사항이긴 하지만 구글플레이는 여러 화면설정에 따라 여러개의 APK를 동일 앱에 대하여 제공하는것을 허용한다. 예를 들어 폰과 태블릿버전을 모두 제공하고자 하는데 동일 APK로 만들수 없는 경우에 실제로 2개의 APK를 게시할 수 있다. 구글 플레이는 디바이스 화면에 맞춰서 APK를 배포할것이다.

하지만 하나로 게시하는제 좋을거다. 라는게 구글의 여전한 권장사항이다. 

더 자세한 정보를 원하면 다음 링크를 더 살펴봐라. Multiple APK Support.



신고
Posted by 삼스
Android/App개발2014.05.21 21:57


Bluetooth Low Energy

4.3부터 BLE central role을 지원하기 시작했다. 그리고 디바이스 검색과 서비스 검색 그리고 속성들의 읽기/쓰기를 지원한다. BLE를 통해 저전력으로 설계된 다양한 디바이스와통신이 가능해졌다.

Key Terms and Concepts
  • Generic Attribute Profile (GATT) - BLE link상에서 송수신 가능한 일반적인 사양을 말하며 모든 LE 애플리케이션프로파일은 GATT에 기반한다.
    • 블루투스 SIG는 많은 프로파일을 정의하였는데 하나의 프로파일은 앱에서 어떻게 동작할지에 대한 사양을 의미한다. 하나의 장치는 여러 프로파일을 구현할 수 있다. 
  • Attribute Protocol (ATT) - GATT는 ATT의 최상위 구현체이며 GATT/ATT로 참조되기도 한다. ATT는 BLE장치에서 동작하도록 최적화 되어 있다. 개개의 속성(Attribute)은 UUID를 가지며 128비트로 구성된다. ATT에 의해 부여된 속성은 특성과 서비스를 결정한다.
  • Characteristic - 하나의 특성(characteristic)은 하나의 값과 n개의 디스크립터를 포함하는데 디스크립터는 특성 값을 기술한다. 하나의 특성은 클래스와 유사한 타입으로 생각하면 된다. 
  • Descriptor - 디스크립터는 특성의 값을 기술한다.
  • Service - 하나의 서비스는 특성드의 집합니다. 예를 들어 "Heart Rate Monitor"라고 불리는 서비스를 가지고 있다면 그 서비스는 "heart rate measurement"같은 특성을 포함한다. GATT-based profile의 리스트와 서비스를 확인하려면 bluetooth.org를 방문하라.
Roles and Responsibilities
이 문서에서 설명하는 역할과 책임은 Android장치가  BLE장치와 연동하는데 적용되는것들이다.
  • Central vs. peripheral - BLE 연결에 적용된다. central 역할은 scan, 게시검색(looking for advertisement), 그리고 peripheral역할은 게시를 만든다.
  • GATT server vs. GATT client - 디바이스가 연결된 이 후 서로 어떻게 대화하는지에 대해 정의한다. 
차이점을 이해하려면 안드로이드폰하나와 움직임을 감지하는 BLE장치를 가지고 있다고 가정 해보자. 폰은 central역할을 한다. BLE장치는 peripheral역할을 한다. 

폰과 BLE장치가 한번 연결이 되면 두 장치는 서로  GATT metadata를 주고 받는다. 주고받는 데이터에 따라 하나이상의 서버 액션이 있을 수 있다. 예를 들어 BLE장치가 폰에 센서정보를 전달하려고 할 수 있다. 이 때 장치는 서버로 동작한다. 장치가 폰으로부터 업데이트를 받고자 할수 있다 이 때는 폰이 서버로 동작한다.

이 문서에서 제공하는 샘플은 안드로이드 디바이스로 동작하는 앱으로 GATT client이다. 앱은 GATT server로부터 데이터를 가져오는데 그것은 Heart Rate Profile을 지원하는 BLE heart rate monitor이다.
반면에 앱을 GATT 서버역할을 하도록 구현할 수 도 있다. BluetoothGattServer를 참조하라.

BLE permissions
블루투스통신(연결요청, 연결수락, 데이터 전송)을 하려면 BLUETOOTH 퍼미션을 추가해야 함다.
블루투스장치를 검색하고 설정을 조작하려면 BLUETOOTH_ADMIN 퍼미션을 추가해야 한다.
BLUETOOTH_ADMIN은 BLUETOOTH퍼미션과 함께 정의되어야 한다.

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
블루투스 BLE만 사용할거라면 아래와 같이 manifest에 포함시켜야 한다.

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
BLE를 지원하지 않는 앱으로 만들고자 한다면 여전히 동일한  feature를 추가해야 하며 다만 required="false"로 하면 된다. 런타임에 BLE활성화 여부를 PackageManager.hasSystemFeature로 알아낼 수 있다.

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
   
Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish
();
}

Setting Up BLE

BLE로 통신하기 전에 디바이스가 BLE를 지원하는지 확이해야 한다. 지원한다면 활성화되어야 한다. 
만일  BLE를 지원하지 않는다면 BLE feature들을 비활성화해야 한다. 지원한다면 사용자에게 BT를 앱을 떠나지 않고 활성화하도록 유도해야 한다. 이 과정은 BluetoothAdapter를 사용하여 2단계로 가능하다.

1. BluetoothAdapter얻기

BluetoothAdapter는 블루투스관련 일부 또는 모든 블루투스 동작들을 필요로 한다. BluetoothAdapter는 디바이스자체의 BluetoothAdapter를 나타낸다.  전체시스템을 위한 하나의 어댑터가 있고 앱은 이 객체를 통해서 상호작용을 한다. 다음 코드조각은 어댑터를 얻는 방법을 보여준다.

    1. // Initializes Bluetooth adapter.
      final BluetoothManager bluetoothManager =
             
      (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
      mBluetoothAdapter
      = bluetoothManager.getAdapter();
    2. Bluetooth 활성화.
    다음에 블루투스를 활성화해야 한다. isEnabled()로 활성화여부를 확인 가능하다. false면 비활성화이다. 다음은 그 샘플이다.
    1. private BluetoothAdapter mBluetoothAdapter;
      ...
      // Ensures Bluetooth is available on the device and it is enabled. If not,
      // displays a dialog requesting user permission to enable Bluetooth.
      if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
         
      Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
          startActivityForResult
      (enableBtIntent, REQUEST_ENABLE_BT);
      }

    BLE 장치 찾기
    BLE장치를 찾으려면 startLeScan() 메소드를 호출한다. LeScanCallack이 파라메터로 호출된다. 이 메소드를 구현해야 하고 스캔결과를 받을 수 있다. 배터리 소모가 심심하기 때문에 다음의 가이드라인을 잘 지켜야 한다.
    • 원하는 디바이스를 찾으면 바로 스캔을 중단해야 한다.
    • 하나의 루프에서 스캔하지 말고 타임아웃을 적용해라. 디바이스가 범위안에서 벗어났을 수도 있고 그럴경우 배터리 는 골로 간다.
    다음 코드는 스캔을 시작하고 중단하는 예이다.
    /**
     * Activity for scanning and displaying available BLE devices.
     */

    public class DeviceScanActivity extends ListActivity {

       
    private BluetoothAdapter mBluetoothAdapter;
       
    private boolean mScanning;
       
    private Handler mHandler;

       
    // Stops scanning after 10 seconds.
       
    private static final long SCAN_PERIOD = 10000;
       
    ...
       
    private void scanLeDevice(final boolean enable) {
           
    if (enable) {
               
    // Stops scanning after a pre-defined scan period.
                mHandler
    .postDelayed(new Runnable() {
                   
    @Override
                   
    public void run() {
                        mScanning
    = false;
                        mBluetoothAdapter
    .stopLeScan(mLeScanCallback);
                   
    }
               
    }, SCAN_PERIOD);

                mScanning
    = true;
                mBluetoothAdapter
    .startLeScan(mLeScanCallback);
           
    } else {
                mScanning
    = false;
                mBluetoothAdapter
    .stopLeScan(mLeScanCallback);
           
    }
           
    ...
       
    }
    ...
    }

    특정타입의 페리퍼렁만 스캔하고자 한다면 startLeScan(UUID[], BluetoothAdapter.LeScanCallback)을 사용할 수 있다. UUID[]에는 앱에서 지원하고자 하는  GATT 서비스목록이 들어간다.
    다음코드 예를 보아라.

    private LeDeviceListAdapter mLeDeviceListAdapter;
    ...
    // Device scan callback.
    private BluetoothAdapter.LeScanCallback mLeScanCallback =
           
    new BluetoothAdapter.LeScanCallback() {
       
    @Override
       
    public void onLeScan(final BluetoothDevice device, int rssi,
               
    byte[] scanRecord) {
            runOnUiThread
    (new Runnable() {
               
    @Override
               
    public void run() {
                   mLeDeviceListAdapter
    .addDevice(device);
                   mLeDeviceListAdapter
    .notifyDataSetChanged();
               
    }
           
    });
       
    }
    };
    BLE또는 Class BT장치를 검색할 수 는 있지만 두가지를 동시에 검색은 할 수 없다.

    GATT 서버에 연결하기
    첫번째 연동을 위한 과정은 BLE디바이스에 연결하는것인데 좀더 자세히 말하면 디바이스의 GATT서버에 연결하는 것이다. connectGatt()메소드로 하면 된다. 이 메소드는 3개의 파라메터가 있는데 context, autoConnect, 그리고 BluetoothGattCallback이 있다. autoConnect는 검색되었을 때 자동으로 연결할지에 대한 파라메터다.

    mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

    이 코드는 BLE device를 GATT 서버 호스트로 연결한다. 그리고 BluetoothGatt 인스턴스를 반환한다. 이 인스턴스로 GATT client를 운영한다.
    BluetoothGattCallback은 클라이언트에 연결상태나 client 운영에 대한 결과를 전달한다.

    이 예제에서 BLE 앱은 하나의 액티비티가 연결하고 데이터를 표시하고 GATT 서비스와 특성들을 표시한다. 사용자 입력에 기반하여 BluetoothLeService로 불리는 서비스와 통신을 수행하도 Android BLE API를 통해 장치와 상호연동을 한다.


    // A service that interacts with the BLE device via the Android BLE API.
    public class BluetoothLeService extends Service {
       
    private final static String TAG = BluetoothLeService.class.getSimpleName();

       
    private BluetoothManager mBluetoothManager;
       
    private BluetoothAdapter mBluetoothAdapter;
       
    private String mBluetoothDeviceAddress;
       
    private BluetoothGatt mBluetoothGatt;
       
    private int mConnectionState = STATE_DISCONNECTED;

       
    private static final int STATE_DISCONNECTED = 0;
       
    private static final int STATE_CONNECTING = 1;
       
    private static final int STATE_CONNECTED = 2;

       
    public final static String ACTION_GATT_CONNECTED =
               
    "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
       
    public final static String ACTION_GATT_DISCONNECTED =
               
    "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
       
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
               
    "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
       
    public final static String ACTION_DATA_AVAILABLE =
               
    "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
       
    public final static String EXTRA_DATA =
               
    "com.example.bluetooth.le.EXTRA_DATA";

       
    public final static UUID UUID_HEART_RATE_MEASUREMENT =
                UUID
    .fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

       
    // Various callback methods defined by the BLE API.
       
    private final BluetoothGattCallback mGattCallback =
               
    new BluetoothGattCallback() {
           
    @Override
           
    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                   
    int newState) {
               
    String intentAction;
               
    if (newState == BluetoothProfile.STATE_CONNECTED) {
                    intentAction
    = ACTION_GATT_CONNECTED;
                    mConnectionState
    = STATE_CONNECTED;
                    broadcastUpdate
    (intentAction);
                   
    Log.i(TAG, "Connected to GATT server.");
                   
    Log.i(TAG, "Attempting to start service discovery:" +
                            mBluetoothGatt
    .discoverServices());

               
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    intentAction
    = ACTION_GATT_DISCONNECTED;
                    mConnectionState
    = STATE_DISCONNECTED;
                   
    Log.i(TAG, "Disconnected from GATT server.");
                    broadcastUpdate
    (intentAction);
               
    }
           
    }

           
    @Override
           
    // New services discovered
           
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
               
    if (status == BluetoothGatt.GATT_SUCCESS) {
                    broadcastUpdate
    (ACTION_GATT_SERVICES_DISCOVERED);
               
    } else {
                   
    Log.w(TAG, "onServicesDiscovered received: " + status);
               
    }
           
    }

           
    @Override
           
    // Result of a characteristic read operation
           
    public void onCharacteristicRead(BluetoothGatt gatt,
                   
    BluetoothGattCharacteristic characteristic,
                   
    int status) {
               
    if (status == BluetoothGatt.GATT_SUCCESS) {
                    broadcastUpdate
    (ACTION_DATA_AVAILABLE, characteristic);
               
    }
           
    }
         
    ...
       
    };
    ...
    }
    콜백이 호출되면 broadcastUpdate()를 호출한다. 여기서 데이터 파싱은 Bluetooth Heart Rate Measurement profile 사양에 맞춰서 수행하고 있다.
    private void broadcastUpdate(final String action) {
       
    final Intent intent = new Intent(action);
        sendBroadcast
    (intent);
    }

    private void broadcastUpdate(final String action,
                                 
    final BluetoothGattCharacteristic characteristic) {
       
    final Intent intent = new Intent(action);

       
    // This is special handling for the Heart Rate Measurement profile. Data
       
    // parsing is carried out as per profile specifications.
       
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
           
    int flag = characteristic.getProperties();
           
    int format = -1;
           
    if ((flag & 0x01) != 0) {
                format
    = BluetoothGattCharacteristic.FORMAT_UINT16;
               
    Log.d(TAG, "Heart rate format UINT16.");
           
    } else {
                format
    = BluetoothGattCharacteristic.FORMAT_UINT8;
               
    Log.d(TAG, "Heart rate format UINT8.");
           
    }
           
    final int heartRate = characteristic.getIntValue(format, 1);
           
    Log.d(TAG, String.format("Received heart rate: %d", heartRate));
            intent
    .putExtra(EXTRA_DATA, String.valueOf(heartRate));
       
    } else {
           
    // For all other profiles, writes the data formatted in HEX.
           
    final byte[] data = characteristic.getValue();
           
    if (data != null && data.length > 0) {
               
    final StringBuilder stringBuilder = new StringBuilder(data.length);
               
    for(byte byteChar : data)
                    stringBuilder
    .append(String.format("%02X ", byteChar));
                intent
    .putExtra(EXTRA_DATA, new String(data) + "\n" +
                        stringBuilder
    .toString());
           
    }
       
    }
        sendBroadcast
    (intent);
    }
    이 이벤드들은 B roadcastReceiver에 의해 처리된다.
    // Handles various events fired by the Service.
    // ACTION_GATT_CONNECTED: connected to a GATT server.
    // ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
    // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
    // ACTION_DATA_AVAILABLE: received data from the device. This can be a
    // result of read or notification operations.
    private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
       
    @Override
       
    public void onReceive(Context context, Intent intent) {
           
    final String action = intent.getAction();
           
    if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
                mConnected
    = true;
                updateConnectionState
    (R.string.connected);
                invalidateOptionsMenu
    ();
           
    } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
                mConnected
    = false;
                updateConnectionState
    (R.string.disconnected);
                invalidateOptionsMenu
    ();
                clearUI
    ();
           
    } else if (BluetoothLeService.
                    ACTION_GATT_SERVICES_DISCOVERED
    .equals(action)) {
               
    // Show all the supported services and characteristics on the
               
    // user interface.
                displayGattServices
    (mBluetoothLeService.getSupportedGattServices());
           
    } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
                displayData
    (intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
           
    }
       
    }
    };

    BLE 속성 읽기
    앱이 GATT 서버에 연결하고 서비스를 찾게 되면 속성을  읽고/쓸수 있게 된다. 다음 코드는 서비스와 특성들을 나열하여 표시해준다.
    public class DeviceControlActivity extends Activity {
       
    ...
       
    // Demonstrates how to iterate through the supported GATT
       
    // Services/Characteristics.
       
    // In this sample, we populate the data structure that is bound to the
       
    // ExpandableListView on the UI.
       
    private void displayGattServices(List<BluetoothGattService> gattServices) {
           
    if (gattServices == null) return;
           
    String uuid = null;
           
    String unknownServiceString = getResources().
                    getString
    (R.string.unknown_service);
           
    String unknownCharaString = getResources().
                    getString
    (R.string.unknown_characteristic);
           
    ArrayList<HashMap<String, String>> gattServiceData =
                   
    new ArrayList<HashMap<String, String>>();
           
    ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                   
    = new ArrayList<ArrayList<HashMap<String, String>>>();
            mGattCharacteristics
    =
                   
    new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

           
    // Loops through available GATT Services.
           
    for (BluetoothGattService gattService : gattServices) {
               
    HashMap<String, String> currentServiceData =
                       
    new HashMap<String, String>();
                uuid
    = gattService.getUuid().toString();
                currentServiceData
    .put(
                        LIST_NAME
    , SampleGattAttributes.
                                lookup
    (uuid, unknownServiceString));
                currentServiceData
    .put(LIST_UUID, uuid);
                gattServiceData
    .add(currentServiceData);

               
    ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                       
    new ArrayList<HashMap<String, String>>();
               
    List<BluetoothGattCharacteristic> gattCharacteristics =
                        gattService
    .getCharacteristics();
               
    ArrayList<BluetoothGattCharacteristic> charas =
                       
    new ArrayList<BluetoothGattCharacteristic>();
               
    // Loops through available Characteristics.
               
    for (BluetoothGattCharacteristic gattCharacteristic :
                        gattCharacteristics
    ) {
                    charas
    .add(gattCharacteristic);
                   
    HashMap<String, String> currentCharaData =
                           
    new HashMap<String, String>();
                    uuid
    = gattCharacteristic.getUuid().toString();
                    currentCharaData
    .put(
                            LIST_NAME
    , SampleGattAttributes.lookup(uuid,
                                    unknownCharaString
    ));
                    currentCharaData
    .put(LIST_UUID, uuid);
                    gattCharacteristicGroupData
    .add(currentCharaData);
               
    }
                mGattCharacteristics
    .add(charas);
                gattCharacteristicData
    .add(gattCharacteristicGroupData);
             
    }
       
    ...
       
    }
    ...
    }

    GATT 통지 수신
    디바이스의 특성이 변경에 대한 통지를 앱이 알수 있다. 다음 코드는 setCharacteristicNotification()메소드로 어떻게 통지를 설정하는지 보여준다.
    private BluetoothGatt mBluetoothGatt;
    BluetoothGattCharacteristic characteristic;
    boolean enabled;
    ...
    mBluetoothGatt
    .setCharacteristicNotification(characteristic, enabled);
    ...
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID
    .fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
    descriptor
    .setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt
    .writeDescriptor(descriptor);
    하나의 특성에 대해 통지가 활성화되면 onCharacteristicChanged()가 장치에서 해당 특성정보가 변경이 되면 호출된다.
    @Override
    // Characteristic notification
    public void onCharacteristicChanged(BluetoothGatt gatt,
           
    BluetoothGattCharacteristic characteristic) {
        broadcastUpdate
    (ACTION_DATA_AVAILABLE, characteristic);
    }

    클라이언트 앱의 종료
    BLE 장치를 사용하는 앱이 종료되면 반드시 close()를 호출하여 시스템이 관련 리소스를 반환하도록 해야 한다.
    public void close() {
       
    if (mBluetoothGatt == null) {
           
    return;
       
    }
        mBluetoothGatt
    .close();
        mBluetoothGatt
    = null;
    }


    신고
    Posted by 삼스

    티스토리 툴바