Pushing the Limits Erik Hellman, Spotify google.com/+ErikHellman @ErikHellman Android Programming
Pushing the Limits Erik Hellman, Spotify google.com/+ErikHellman @ErikHellman
Android Programming
2
wiley.com/go/ptl/androidprogramming
3
Improved memory management
Efficient multi-threading and -processing
Using Android components correctly
Improved memory management4
Use a static factory method whenever possible5
Effective Java (2nd ed.): Item 1!“A second advantage of static factory methods is that, unlike constructors, they are not required to create a new object each time they’re invoked.”
Message.java6
public final class Message implements Parcelable { /*package*/ Message next; ! private static final Object sPoolSync = new Object(); private static Message sPool; private static int sPoolSize = 0; ! private static final int MAX_POOL_SIZE = 50; ! public static Message obtain() { synchronized (sPoolSync) { if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; sPoolSize-‐-‐; return m; } } return new Message(); }
public void recycle() { clearForRecycle(); ! synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } } }
How to get more memory - Part 1
7
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name=".MyApplication" android:largeHeap="true" android:theme="@style/AppTheme" > … </application>
How to get more memory - Part 2
8
<service android:name="se.hellsoft.apptl.app.MyService" android:enabled="true" android:exported="false" android:process="se.hellsoft.apptl.service"> </service>
How to get more memory - Part 3
9
data = malloc(sizeof(data_struct));
10
Improved memory management
Efficient multi-threading and -processing
Using Android components correctly
11
Always use a Handler!*
* Except when you shouldn’t…
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); HandlerThread backgroundThread = new HandlerThread("background"); backgroundThread.start(); mBackgroundHandler = new Handler(backgroundThread.getLooper(), this); mUiHandler = new Handler(this); } !@Override protected void onDestroy() { super.onDestroy(); mBackgroundHandler.removeCallbacksAndMessages(null); mBackgroundHandler.getLooper().quit(); }
12
Always use a Handler!*
* Except when you shouldn’t…
public void onStartBackgroundJob(View view) { mBackgroundHandler.obtainMessage(BACKGROUND_JOB).sendToTarget(); } !@Override public boolean handleMessage(Message msg) { switch (msg.what) { case BACKGROUND_JOB: Bitmap result = null; // Processing goes here... mUiHandler.obtainMessage(UPDATE_UI, result).sendToTarget(); break; case UPDATE_UI: ((ImageView) findViewById(R.id.photo)) .setImageBitmap((Bitmap) msg.obj); break; } return true; }
Why Handler?
13
• Scheduling of messages
• Cancelling of messages
• Reduces GC calls
Using the process attribute
14
Why a separate process?
15
• Better isolation from crashes
• Binder transactions uses a thread pool
• Double the memory!
16
Improved memory management
Efficient multi-threading and -processing
Using Android components correctly
Application component
17
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name=“.MyApplication" android:theme="@style/AppTheme" > ! ... </application>
Android Singletons using Context
18
public class MySingleton { private static MySingleton sInstance; private Context mContext; ! private MySingleton(Context context) { mContext = context; } public MySingleton getInstance(Context context) { if(sInstance == null) { sInstance = new MySingleton(context.getApplicationContext()); } return sInstance; } public void doStuffThatRequireContext() { // Do stuff here... } }
Application Context limitations
19
• Inflating layout uses default theme
• Starting Activity creates a new task
20
Activity - saving state
Activity.onSaveInstanceState()
Activity.onPause()
or
21
onSaveInstanceState() is not called…
• …when user presses back
• …when you call Activity.finish()
22
Fragment23
android.app.Fragment
android.support.v4.app.Fragment
or
Support library can be upgraded!24
dependencies { compile 'com.android.support:appcompat-‐v7:+' compile 'com.android.support:support-‐v4:19.1.+' compile fileTree(dir: 'libs', include: ['*.jar']) }
Typical Fragment crash25
public static class FirstFragment extends Fragment { private ImageView mImageView; ! @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); mImageView = (ImageView) rootView.findViewById(R.id.photo); ! new AsyncTask<Void, Void, Bitmap>() { @Override protected Bitmap doInBackground(Void... params) { SystemClock.sleep(10000); // Fake long processing... return BitmapFactory.decodeResource(getResources(), R.drawable.happy_android); } ! @Override protected void onPostExecute(Bitmap bitmap) { mImageView.setImageBitmap(bitmap); } }.execute(); ! return rootView; } }
Typical Fragment crash26
E/AndroidRuntime( 1245): FATAL EXCEPTION: AsyncTask #1 E/AndroidRuntime( 1245): Process: se.hellsoft.apptl.app, PID: 1245 E/AndroidRuntime( 1245): java.lang.RuntimeException: An error occured while executing doInBackground() E/AndroidRuntime( 1245): at android.os.AsyncTask$3.done(AsyncTask.java:300) E/AndroidRuntime( 1245): at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355) E/AndroidRuntime( 1245): at java.util.concurrent.FutureTask.setException(FutureTask.java:222) E/AndroidRuntime( 1245): at java.util.concurrent.FutureTask.run(FutureTask.java:242) E/AndroidRuntime( 1245): at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231) E/AndroidRuntime( 1245): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) E/AndroidRuntime( 1245): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) E/AndroidRuntime( 1245): at java.lang.Thread.run(Thread.java:841) E/AndroidRuntime( 1245): Caused by: java.lang.IllegalStateException: Fragment FirstFragment{52836468} not attached to Activity E/AndroidRuntime( 1245): at android.support.v4.app.Fragment.getResources(Fragment.java:601) E/AndroidRuntime( 1245): at se.hellsoft.apptl.app.MainActivity$FirstFragment$1.doInBackground(MainActivity.java:115) E/AndroidRuntime( 1245): at se.hellsoft.apptl.app.MainActivity$FirstFragment$1.doInBackground(MainActivity.java:109) E/AndroidRuntime( 1245): at android.os.AsyncTask$2.call(AsyncTask.java:288) E/AndroidRuntime( 1245): at java.util.concurrent.FutureTask.run(FutureTask.java:237) E/AndroidRuntime( 1245): ... 4 more
27
Use a Loader whenever possible and cancel remaining requests in onPause()/onDetach()!
Binding to Services
28
@Override public IBinder onBind(Intent intent) { return null; // NEVER do this }
Binding to Services
29
@Override public IBinder onBind(Intent intent) { String action = intent.getAction(); if(ACTION_FIRST_BINDER.equals(action)) { return mFirstBinder; } else if(ACTION_SECOND_BINDER.equals(action)) { return mSecondBinder; } else { throw new RuntimeException("Unexpected error!"); } }
Binding to Services
30
@Override public IBinder onBind(Intent intent) { Uri data = intent.getData(); String param = data.getQueryParameter("param"); if(FIRST_BINDER.equals(param)) { return mFirstBinder; } else if(SECOND_BINDER.equals(param)) { return mSecondBinder; } else { throw new RuntimeException("Unexpected error!:"); } }
Service considerations
31
• A binder Intent is identified by the action string and data Uri!
!
•onBind() and onUnbind() only called once per unique Intent
•START_STICKY will give a null Intent when Service is restarted by the system
• Binder calls run on local thread for process-local service
Most common ContentProvider mistake :)
32
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = mDatabaseHelper.getReadableDatabase(); int match = sUriMatcher.match(uri); Cursor cursor = null; switch (match) { case ALL_ROWS: cursor = db.query(Contract.TABLE_NAME, projection, selection, selectionArgs, "", "", sortOrder); break; case SINGLE_ROW: String id = uri.getLastPathSegment(); cursor = db.query(Contract.TABLE_NAME, projection, "_id = ?", new String[]{id}, "", "", sortOrder); break; } if(cursor != null) { cursor.setNotificationUri(getContext().getContentResolver(), uri); } return cursor; }
Most common ContentProvider mistake :)
33
@Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); int match = sUriMatcher.match(uri); switch (match) { case ALL_ROWS: long newId = db.insert(Contract.TABLE_NAME, "", values); if(newId != -‐1) { getContext().getContentResolver().notifyChange(uri, null); } return Uri.withAppendedPath(uri, String.valueOf(newId)); default: return null; } }
Don’t forget bulkInsert() !!!
34
@Override public int bulkInsert(Uri uri, ContentValues[] values) { SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); int match = sUriMatcher.match(uri); int inserted = 0; switch (match) { case TASKS_CODE: try { db.beginTransaction(); for (ContentValues value : values) { long id = db.insert(Contract.TABLE_NAME, "", value); if (id <= 0) throw new SQLException("Failed with inserting."); inserted++; } db.setTransactionSuccessful(); getContext().getContentResolver().notifyChange(uri, null); } finally { db.endTransaction(); } } return inserted; }
Useful Broadcasts - Auto-starting
35
<receiver android:name="se.hellsoft.myapp.AutoStartReceiver" > <intent-‐filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-‐filter> </receiver>
<receiver android:name=“se.hellsoft.myapp.AutoStartReceiver" android:process=“:autostarter”> <intent-‐filter> <action android:name="android.intent.action.USER_PRESENT" /> </intent-‐filter> </receiver>
Useful Broadcasts - Monitoring the network
36
<receiver android:name=“se.hellsoft.myapp.NetworkMonitor"> <intent-‐filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> <action android:name="android.net.wifi.supplicant.CONNECTION_CHANGE" /> <action android:name="android.net.wifi.WIFI_AP_STATE_CHANGED" /> </intent-‐filter> </receiver>
37
“Many problems with Android apps can be fixed with a proper use of the Handler class.” - Erik Hellman
Get my book at wiley.com/go/ptl/androidprogramming