Android lifecycle and null pointer problem
I have an application which uses a SQL database. This is encapsulated by a SQLiteOpenHelper class. When the splash screen launches, it calls init on a DataProvider class which stores a protected static instance of the SQLiteOpenHelper. init simply calls the constructor of the SQLiteOpenHelper:
public class UKMPGData extends SQLiteOpenHelper
{
public UKMPGData(Context context, String databaseName)
{
super(context, databaseName, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db)
{
//create table and set up triggers etc
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
onCreate(db);
}
}
public class UKMPGDataProvider
{
protected static UKMPGData uKMpgData;
public static void init(Context aApplicationContext, String aDatabaseName)
{
uKMpgData = new UKMPGData(applicationContext, databaseName);
}
public static void close()
{
uKMpgData.close();
}
}
I then had two further classes which extended UKMPGDataProvider and therefore had access to uKMpgData. These classes retrieve and store particular types of data from the database. Eg.
public class VehicleDataProvider extends UKMPGDataProvider
{
public static Cursor getVehicles()
{
Cursor cursor = null;
SQLiteDatabase db = uKMpgData.getReadableDatabase();
cursor = db.query(VEHICLE_TABLE_NAME, GET_VEHICLES_FROM_CLAUSE, null, null, null, null, ORDER_BY);
return cursor;
}
//...
}
This all seemed to work fine until I noticed that if the application was running and then forced to the background, if it was left for a number of hours, when the app was brought back to the foreground I would get a null pointer in an Activity class which called getVehicles() (see above). It turns out, uKMpgData was no longer referencing an object.
I understand that Android can kill processes when it is deemed necessary, but don't understand what happened to my app to get the null pointer - if my app's process was killed, then wouldn't a new instance of the app be launched? In other words, a new SplashScreen would initialise the database object and therefore no null pointer exception.
I must be missing something - what state was my app in to 开发者_开发知识库have memory reclaimed (the reference to the database object) but to display the last visible activity upon relaunch.
Incidentally, the bug is now fixed. VehicleDataProvider and the other similar class no longer extend the superclass data provider (UKMPGDataProvider), which now holds a private reference to ukMpgData. All methods which touch the database now go through UKMPGDataProvider which will check for null and reinitialise if necessary.
Thanks in advance, Barry
When Android kills your application to reclaim memory, your Application/Activity objects are all unloaded and will no longer exist. However, Android saves certain information so that your application can be restored to that approximate state when you try to run it again.
Part of this includes information about the state of the Activity stack (i.e. which Activities were running) before it was destroyed. As you noticed, Android restores your application to the Activity that was last running, and not the beginning of your app (the splash screen). This is a 'feature', and I can only assume it is an attempt to provide a seamless experience for the user who should not even notice that the app was unloaded.
There is no reason why Android should recreate your splash screen (even if it still existed in the Activity stack) before recreating the current Activity. That kind of dependency is not encouraged, and is not something you can rely on as Android loads/unloads Activities as it sees fit. In general, having static state that is initialised in previous activities is a sure way to run into problems in this situation. Since the splash screen was not recreated, it won't have initialiased the UKMPGDataProvider
before you are using it in another Activity, hence the NullPointerException
.
You can solve this in two ways.
Initialise the
UKMPGDataProvider
inside Activity.onCreate(Bundle) of each Activity that uses it. If one of these Activities are being restored, it is guaranteed to get a callback toonCreate
and hence the data provider will always be initialised.Initialise the
UKMPGDataProvider
inside Application.onCreate(). This is guaranteed to be called before any Activity is started, even in the even of memory reclaimation/restoration. If you do not yet have a customApplication
class, you should create a subclass and then point to it in yourAndroidManifest.xml
for it to be used.
As an aside, implementing Activity.onSaveInstanceState(Bundle) is one way to save additional state relevant to your application, which will be given back to you in Activity.onCreate(Bundle)
(it is the Bundle
argument) upon restoration. It is meant for transient state only, such as some view state that arose after some user actions (in particular the state of custom views). It is not suitable for this case, since you can generate a new UKMPGDataProvider
easily and it does not have any state linked to the previous session.
It would be best to put all data initialization in onStart() or onResume(), and not onCreate(). If you move these initializations to one of these areas, it'll likely work better. Remember that these can be called in exactly the instances that you mentioned, if the app stops for a while and then comes back. But the corresponding onStop() or onPause() would be called first.
I suspect that part of your memory is being reclaimed, but not all of it, and if you do as I suggested above, it'll fix your problem.
You are not showing any of your Activity life cycle handlers. Check this out: http://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle
In your case you want to first look at onStop() and onRestart().
I suggest you initialize your SQLiteOpenHelper object in your Activity onCreate() ,close it in onDestroy() and pass your Activity Context object to it.
This way your SQLiteOpenHelper object will be associated correctly with your activity life cycle
The problem is that when your application is resumed, your objects are no longer there, because they where cleaned in order to gain memory for other apps.
So when you call
public static Cursor getVehicles()
the object uKMpgData is null because you have not called to the init method. You have some solutions for that. The easier is that if you have access to the context init the value before calling it. Doing something like:
Cursor cursor = null;
SQLiteDatabase db = getuKMpgData().getReadableDatabase();
.....
And having the method
public static UKMPGData getuKMpgData(){
if(uKMpgData==null){
Context cnt= //Some method for retrieving the context
String dbname="myDB";
init(cnt,dbname)
}
return uKMpgData;
}
If you don't know how to retrieve the context from a place where you can't call to getContext() or getApplicationContext() methods. There is a nice trick you can use.
Check THIS ANSWER
You should call VehicleDataProvider.init () in your activities onResume () method.
And you should NOT access VehicleDataProvider before onResume in the life cylcle, and no more after onPause.
It is not really a good idea to use static members in android. I would recommend a general redesign to get rid of statics like (I hope there are no typos)
public class UKMPGDataProvider
{
protected UKMPGData uKMpgData;
protected UKMPGDataProvider(Context aApplicationContext, String aDatabaseName)
{
uKMpgData = new UKMPGData(applicationContext, databaseName);
}
public void close()
{
uKMpgData.close();
}
}
and now your VehicleDataProvider
public class VehicleDataProvider extends UKMPGDataProvider
{
public VehicleDataProvider(Context aApplicationContext, String aDatabaseName)
{
super (aApplicationContext, aDatabaseName);
}
public Cursor getVehicles()
{
Cursor cursor = null;
SQLiteDatabase db = uKMpgData.getReadableDatabase();
cursor = db.query(VEHICLE_TABLE_NAME, GET_VEHICLES_FROM_CLAUSE, null, null, null, null, ORDER_BY);
return cursor;
}
//...
}
And now your Activity
.. extends Activity {
private VehicleDataProvider vehicle;
@Override
protected void onResume() {
super.onResume();
vehicle = new VehicleDataProvider (this, "database.db");
...
}
@Override
protected void onPause() {
vehicle.close ();
vehicle = null;
super.onPause();
}
...
}
When you get null pointer exception it is very like you access vehicle to early or too late.
It is absolute necessary to release the database in onPause. OnPause happens e.g. when an other activity becomes visible. In this case any memory occupied by the database should be released.
android is designed as an low memory os, release memory as soon as possible. This behavior is uncommon when you are coming from other java based environments.
Hope this helps
精彩评论