1. SpannableString

2. setPaintFlags

3. Html.fromHtml


SpannableString

String udata="SpannableString underline sample";
SpannableString content = new SpannableString(udata);
content.setSpan(new UnderlineSpan(), 0, udata.length(), 0);
mTextView.setText(content);



setPaintFlags

mTextView.setPaintFlags(mTextView.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
mTextView.setText("Paint flag underline sample");



Html.fromHtml

String htmlString="Html from underline sample";
mTextView.setText(Html.fromHtml(htmlString));


Genymotion을 이용해서 Android개발 시 기존에 제공해 주는 Emulator에 비해서 상당히 가볍고 빠르게 개발을 할 수 있습니다. 그런데 SDK를 21로 업데이트 후 Genymotion이 계속 ADB연결이 끊기는 현상이 생기곤 합니다. 너무 빈번하게... android studio나 eclipse를 통해서 개발 할 때 restart를 하거나 adb kill-server 후 adb start-server를 하라고도 나오기도 하구요.

adb server is out of date.  killing...
cannot bind 'tcp:5037'
ADB server didn't ACK
* failed to start daemon *
error:

이와 같이 보이면 상당히 난감하죠..


근본적으로 해결 방법을 제시하면

Genymotion -> Setting -> ADB -> Use custom Android SDK tools 를 선택 후 현재 android sdk가 위차한 path를 지정해 주면 됩니다.


간단히 해결!!

Android 개발할 때 eclipse를 통하여 개발을 많이 했는데, AndroidStudio를 통하여 개발을 해보게 되었습니다.

그중에 Build상태에서는 오류가 나오진 않았지만 Runtime시에 

Excution failed for task ':app:preDexDebug'

이와 같은 오류를 보게 되었습니다. Build는 되었는데 왜 Runtime시에 나타날까에 대한 것도 찾아 보았을 때 추가해준 Library중 하나가 AndroidLibrary가 아닌 Java LIbrary로 된 것이 있었습니다. 그리고 개발중에 사용된 java version이 

Java SE 8u25

버전을 사용했더니 문제가 나타났습니다. gradle빌드에 1.6이라는 옵션을 주었지만 해결되지 않았죠 ㅡㅡ.

혹시나 하는 마음에 

Java SE 7u71/72 

를 설치하고 JDK location을 변경해 주었더니 문제가 없어졌습니다.... 아무래도 java8에서 빌드된 java library가 dex로 변경을 할 수 없는 듯 해 보입니다. 이런식으로 해결은 했지만 그래도 좀 찜찜하네요 ㅡㅡ.



안드로이드에서 음악을 재생할 때 가장 많이 사용하는 것으 MediaPlayer 입니다. 사용 방법도 간단하고 Added in API level 1 이 알려주다 싶이 Media 재생을 위한 api로는 가장 오래되고 많이 사용을 하죠. 그럼 Wave 파일에 대해서는 어떻게 재생을 할까요??? AudioTrack이라는 api가 존재합니다. pcm 데이터를 재생시켜줄 수 있는 api인데, 사실 mp3나 aac 같은 파일을 재생하기 위해서 Decoder로 mp3나 aac같은 파일을 다시 pcm데이터인 wave 파일로 만들어야 AudioTrack으로 재생시킬 수 있습니다. 

그럼 mp3나 aac와 같은 파일을 wave로의 Decoder는 api로 존재를 하느냐?.. API level 16인 Jelly Bean(Android 4.1) 부터 AudioCodec 이라는 api가 새로 생겼습니다. AudioCodec은 Encoder와 Decoder 기능을 같이 제공 합니다. 그래서 오디오 정보는 AudioTrack으로 재생을 할 수 있는 것이죠.

생성 방법은

MediaCodec codec = MediaCodec.createDecoderByType(type);
MediaCodec codec = MediaCodec.createEncoderByType(type);

이와 같은 방법으로 MediaCodec을 생성해서 사용할 수 있습니다.



디코딩을 통한 재생하는 메인 코드 입니다. 원리는 해당 버퍼만큼 디코딩하고 그에 해당하는 부분으 AudioTrack을 통한 write로 재생을 하는 방법입니다.
private void decodeLoop()
	{
		ByteBuffer[] codecInputBuffers;
		ByteBuffer[] codecOutputBuffers;

		mExtractor = new MediaExtractor();
		try
		{
			mExtractor.setDataSource(this.mMediaPath);
		}
		catch (Exception e)
		{
			mAudioPlayerHandler.onAudioPlayerError(AudioStreamPlayer.this);
			return;
		}

		MediaFormat format = mExtractor.getTrackFormat(0);
		String mime = format.getString(MediaFormat.KEY_MIME);
		long duration = format.getLong(MediaFormat.KEY_DURATION);
		int totalSec = (int) (duration / 1000 / 1000);
		int min = totalSec / 60;
		int sec = totalSec % 60;

		mAudioPlayerHandler.onAudioPlayerDuration(totalSec);

		Log.d(TAG, "Time = " + min + " : " + sec);
		Log.d(TAG, "Duration = " + duration);

		mMediaCodec = MediaCodec.createDecoderByType(mime);
		mMediaCodec.configure(format, null, null, 0);
		mMediaCodec.start();
		codecInputBuffers = mMediaCodec.getInputBuffers();
		codecOutputBuffers = mMediaCodec.getOutputBuffers();

		int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);

		Log.i(TAG, "mime " + mime);
		Log.i(TAG, "sampleRate " + sampleRate);

		mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, AudioFormat.CHANNEL_OUT_STEREO,
				AudioFormat.ENCODING_PCM_16BIT, AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO,
						AudioFormat.ENCODING_PCM_16BIT), AudioTrack.MODE_STREAM);

		mAudioTrack.play();
		mExtractor.selectTrack(0);

		final long kTimeOutUs = 10000;
		MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
		boolean sawInputEOS = false;
		int noOutputCounter = 0;
		int noOutputCounterLimit = 50;

		while (!sawInputEOS && noOutputCounter < noOutputCounterLimit && !isForceStop)
		{
			if (!sawInputEOS)
			{
				if(isPause)
				{
					if(mState != State.Pause)
					{
						mState = State.Pause;
						
						mAudioPlayerHandler.onAudioPlayerPause();
					}
					continue;
				}
				noOutputCounter++;
				if (isSeek)
				{
					mExtractor.seekTo(seekTime * 1000 * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
					isSeek = false;
				}

				mInputBufIndex = mMediaCodec.dequeueInputBuffer(kTimeOutUs);
				if (mInputBufIndex >= 0)
				{
					ByteBuffer dstBuf = codecInputBuffers[mInputBufIndex];

					int sampleSize = mExtractor.readSampleData(dstBuf, 0);

					long presentationTimeUs = 0;

					if (sampleSize < 0)
					{
						Log.d(TAG, "saw input EOS.");
						sawInputEOS = true;
						sampleSize = 0;
					}
					else
					{
						presentationTimeUs = mExtractor.getSampleTime();

						Log.d(TAG, "presentaionTime = " + (int) (presentationTimeUs / 1000 / 1000));

						mAudioPlayerHandler.onAudioPlayerCurrentTime((int) (presentationTimeUs / 1000 / 1000));
					}

					mMediaCodec.queueInputBuffer(mInputBufIndex, 0, sampleSize, presentationTimeUs,
							sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

					if (!sawInputEOS)
					{
						mExtractor.advance();
					}
				}
				else
				{
					Log.e(TAG, "inputBufIndex " + mInputBufIndex);
				}
			}

			int res = mMediaCodec.dequeueOutputBuffer(info, kTimeOutUs);

			if (res >= 0)
			{
				if (info.size > 0)
				{
					noOutputCounter = 0;
				}

				int outputBufIndex = res;
				ByteBuffer buf = codecOutputBuffers[outputBufIndex];

				final byte[] chunk = new byte[info.size];
				buf.get(chunk);
				buf.clear();
				if (chunk.length > 0)
				{
					mAudioTrack.write(chunk, 0, chunk.length);
					if (this.mState != State.Playing)
					{
						mAudioPlayerHandler.onAudioPlayerPlayerStart(AudioStreamPlayer.this);
					}
					this.mState = State.Playing;
				}
				mMediaCodec.releaseOutputBuffer(outputBufIndex, false);
			}
			else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
			{
				codecOutputBuffers = mMediaCodec.getOutputBuffers();

				Log.d(TAG, "output buffers have changed.");
			}
			else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
			{
				MediaFormat oformat = mMediaCodec.getOutputFormat();

				Log.d(TAG, "output format has changed to " + oformat);
			}
			else
			{
				Log.d(TAG, "dequeueOutputBuffer returned " + res);
			}
		}

		Log.d(TAG, "stopping...");

		releaseResources(true);

		this.mState = State.Stopped;
		isForceStop = true;

		if (noOutputCounter >= noOutputCounterLimit)
		{
			mAudioPlayerHandler.onAudioPlayerError(AudioStreamPlayer.this);
		}
		else
		{
			mAudioPlayerHandler.onAudioPlayerStop(AudioStreamPlayer.this);
		}
	}


사실 이 방법을 사용하는 것은 기존의 MediaPlayer를 사용해서 재생하는 방법보다는 상당히 복잡합니다.

하지만 이 방법을 사용하면 바로 pcm데이터를 통한 가공을 할 수 있다는 장점이 있습니다. MediaPlayer에서도 Visualizer를 통하여 pcm데이터를 가져올 수 있는 방법도 있습니다. 

설명보다는... 그냥 코드로~

https://github.com/drcarter/AudioStreamPlayer


Android LIbrary 프로젝트를 Library add 해서 사용할 수도 있지만, ant jar 를 통한 jar파일로 library를 빌드해서 사용할 수도 있습니다. 단순히 ant jar 만 해주면 jar 파일로 빌드를 해주는데... library project를 ant jar 하게 도면

BUILD FAILED

D:\16.SDK\Android\android-sdk-windows\tools\ant\build.xml:653: The following err

or occurred while executing this line:

D:\16.SDK\Android\android-sdk-windows\tools\ant\build.xml:679: '${renderscript.o

pt.level}' is not a permitted value for com.android.ant.RenderScriptTask$OptLeve

l

이와 같은 오류를 보이면서 BUILD FAILED가 나오게 됩니다. RenderScript option 문제라도 하는데. 이를 해결하는 방법이 은근히 간단했습니다.


project.properties 파일에다가

renderscript.opt.level=O0

이 내용을 추가하고 ant jar 하게 되면

android libarary project가 jar 파일로 빌드되게 됩니다.


	public static Bitmap doGreyscale(Bitmap src)
	{
		final double GS_RED = 0.299;
		final double GS_GREEN = 0.587;
		final double GS_BLUE = 0.114;
		
		int width = src.getWidth();
		int height = src.getHeight();

		Bitmap resultBitmap = Bitmap.createBitmap(width, height, src.getConfig());
		int A, R, G, B;
		int pixel;

		for (int x = 0; x < width; ++x)
		{
			for (int y = 0; y < height; ++y)
			{
				pixel = src.getPixel(x, y);
				A = Color.alpha(pixel);
				R = Color.red(pixel);
				G = Color.green(pixel);
				B = Color.blue(pixel);
				R = G = B = (int) (GS_RED * R + GS_GREEN * G + GS_BLUE * B);
				resultBitmap.setPixel(x, y, Color.argb(A, R, G, B));
			}
		}

		return resultBitmap;
	}



     public static Bitmap doInvert(Bitmap src)
     {
          int A, R, G, B;
          int pixelColor;
          int height = src.getHeight();
          int width = src.getWidth();
         
          Bitmap returnBitmap = Bitmap.createBitmap(width, height, src.getConfig());

          for (int y = 0; y < height; y++)
          {
               for (int x = 0; x < width; x++)
               {
                    pixelColor = src.getPixel(x, y);
                    A = Color.alpha(pixelColor);
                    R = 255 - Color.red(pixelColor);
                    G = 255 - Color.green(pixelColor);
                    B = 255 - Color.blue(pixelColor);
                    returnBitmap.setPixel(x, y, Color.argb(A, R, G, B));
               }
          }

          return returnBitmap;
     }

안드로이드 앱을 개발하다보면 logcat에서 많이 접하게 되는 오류중에 하나가 

java.lang.OutOfMemoryError: bitmap size exceeds VM budget

이 오류이지 않을 까 합니다. 쉽게는 메모리 오류.  앱에서 사용할 수 있는 메모리보다 bitmap을 사용하는데 메모리를 많이 사용하게 되어서 나타나는 오류입니다. 예를 들어서 메모리를 20mb를 사용할 수 있는데 bitmap을 로드할 때 사용할 사용한 메모리가 20mb를 넘어선 것이지요. 그럼 이 상황을 해결할 방법은 어떤게 있을까요?? 


1. sampling option 및 서버에서의 작은 이미지 내려주기.

[BitmapFactory.Options.inSampleSize]

inSampleSize 옵션은, 애초에 decode를 할 때 얼마만큼 줄여서 decoding을 할 지 정하는 옵션 입니다.

inSampleSize 옵션은 1보다 작은 값일때는 무조건 1로 세팅이 되며, 1보다 큰 값, N일때는 1/N 만큼 이미지를 줄여서 decoding 하게 됩니다.

즉 inSampleSize가 2라면 1/2 만큼, 4 이면 1/4만큼 이미지를 줄여서 decoding 해서 Bitmap으로 만들게 주게 됩니다.

decoding의 속도는 2의 지수만큼 비례할 때 속도가 빠르다고 합니다. 


저같은 경우는 Bitmap이미지의 크기가 특정 크기를 넘어가면 inSample옵션을 주어서 이미지의 크기를 줄여서 로드하는 방법을 사용했습니다.

public int inSampleSize

Added in API level 1

If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.


BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap src = BitmapFactory.decodeFile(imagePath, options);


이렇게 BitmapFactory.Options.inSampleSize의 옵션을 사용하는 방법도 있겠지만, 서버에서 이미지를 처음부터 작게 내려주는 것도 방법일겁니다. 만약 thumbnail이미지의 사이즈가 좀 크다 싶으면 서버 개발자와 상의해서 thumbnail이미지를 작게 하는 것도 좋은 방법이겠지요.


2. recycle잘하기.

Bitmap을 recycle을 해야 하는 이유에 대해서는 우선 Android 3.0 (HoneyComb) 이전과 이후의 메모리 모델에 대해서도 이해를 해야 합니다. 간단하게 3.0 이전.. Gingerbread까지는 Bitmap에 Dalvik heap 영역에 bitmap에 로드되지 않고 native heap 영역에 bitmap에 로드 되었습니다. 그래서 GC(Garbage Collection)이후에도 native heap 영역에 로드된 bitmap은 아직 메모리에 해제 되지 않습니다. HoneyComb 부터는 Dalvik heap 영역에 bitmap이 로드되기 때문에 GC 이후에 해제 됩니다. 그럼 Gingerbread까지는 그래서 bitmap에 대해서 recycle을 해줘야 합니다.


- Bitmap 자체가 멤버변수로 된 경우

public class activity extends Activity
{
	ImageView iv;
	Bitmap bitmap;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		iv = (ImageView) findViewById(R.id.imageview);
		bitmap = BitmapFactory.decodeResource(R.drawable.icon);

		iv.setImageBitmap(bitmap);
	}

	@Override
	protected void onDestroy()
	{
		bitmap.recycle();
		bitmap = null;

		super.onDestroy();
	}
}


- ImageView 자체에서 instance를 확인할 경우.

public class activity extends Activity
{
	ImageView iv;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		iv = (ImageView) findViewById(R.id.imageview);
		iv.setImageBitmap(BitmapFactory.decodeResource(R.drawable.icon));
	}

	@Override
	protected void onDestroy()
	{
		Drawable d = iv.getDrawable();
		if (d instanceof BitmapDrawable)
		{
			Bitmap bitmap = ((BitmapDrawable) d).getBitmap();
			bitmap.recycle();
			bitmap = null;
		}
		d.setCallback(null);

		super.onDestroy();
	}
}


3. memory leak 확인하기.

개발 중 메모리릭이 생기는지 안생기는지에 대해서 확인해 보는 방법도 있습니다.

adb shell dumpsys meminfo [packagename]

을 하면 아래와 같은 결과가 출력 됩니다. Pss total은 공유되는 메모리를 포함한 크기이고, Private Dirty는 다른 프로세스와는 공유할 수 없는 메모리(페이지) 크기라는 것. 여기서 Private Dirty는 다르게 생각 해 보면, 이 프로세스가 제거되고나면 바로 반환되어 시스템이(또는 다른 프로세스가) 사용할 수 있는 메모리라고 생각 할 수도 있습니다. 

자게한 내용은 

Android Memory Management: Understanding App PSS

에서 보면 확인해 볼 수 있습니다.



다른 방법으로는 DDMS를 이용하는 방법입니다.

DDMS에서 Heap 영역의 변화를  확인해 볼 수 있습니다.


이외에 hprof 파일 분석을 통한 방법으로 memory leak 을 확인해 볼 수 있는데, 이에 대한 내용은.

제 다른 글 내용인 [Android]ANDROID PERFORMANCE OPTIMIZATION 에서 확인해 보실 수 있습니다.

+ Recent posts