There are at least two significant memory leaks in Yaaic, which cause
the client to force close after a few hours with an
OutOfMemoryException:
(1) The IRCService holds Conversation objects, which contain a
MessageListAdapter, which have references to the ConversationActivity
context. This causes Activity contexts to outlast the Activity, causing
a significant memory leak over time.
Fix this by holding the MessageListAdapter in the ConversationActivity's
DeckAdapter instead of in the Conversation objects. The DeckAdapter's
lifecycle matches that of the Activity, so this prevents the leak.
(2) Every call to DeckAdapter.getView()/renderConversation() creates a
new MessageListView and adds it to the deck. But adding the view to
the deck causes the deck to take a reference to the view, leaking the
MessageListView until the Activity is finished. (This has the effect of
exacerbating the first leak, since the Activity context holds a
reference to the deck.)
Fix this leak by caching MessageListViews in the DeckAdapter, and
returning an existing MessageListView for a Conversation in getView() if
one already exists.
---
.../org/yaaic/activity/ConversationActivity.java | 11 ++-
.../src/org/yaaic/activity/ServersActivity.java | 2 +-
application/src/org/yaaic/adapter/DeckAdapter.java | 84 ++++++++++++++++----
.../yaaic/listener/ConversationClickListener.java | 6 +-
application/src/org/yaaic/model/Conversation.java | 19 -----
5 files changed, 78 insertions(+), 44 deletions(-)
diff --git a/application/src/org/yaaic/activity/ConversationActivity.java
b/application/src/org/yaaic/activity/ConversationActivity.java
index c72a871..5892215 100644
--- a/application/src/org/yaaic/activity/ConversationActivity.java
+++ b/application/src/org/yaaic/activity/ConversationActivity.java
@@ -52,6 +52,7 @@ import org.yaaic.model.User;
import org.yaaic.receiver.ConversationReceiver;
import org.yaaic.receiver.ServerReceiver;
import org.yaaic.view.ConversationSwitcher;
+import org.yaaic.view.MessageListView;
import android.app.Activity;
import android.app.AlertDialog;
@@ -230,7 +231,7 @@ public class ConversationActivity extends Activity
implements ServiceConnection,
// Fill view with messages that have been buffered while paused
for (Conversation conversation : mConversations) {
- mAdapter = conversation.getMessageListAdapter();
+ mAdapter = deckAdapter.getItemAdapter(conversation.getName());
if (mAdapter != null) {
mAdapter.addBulkMessages(conversation.getBuffer());
@@ -396,7 +397,7 @@ public class ConversationActivity extends Activity
implements ServiceConnection,
return;
}
- MessageListAdapter adapter = conversation.getMessageListAdapter();
+ MessageListAdapter adapter = deckAdapter.getItemAdapter(target);
while(conversation.hasBufferedMessages()) {
Message message = conversation.pollBufferedMessage();
@@ -517,8 +518,10 @@ public class ConversationActivity extends Activity
implements ServiceConnection,
{
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
if (deckAdapter.isSwitched()) {
- switcher.showNext();
- switcher.removeView(deckAdapter.getSwitchedView());
+ MessageListView canvas = (MessageListView)
deckAdapter.getView(deckAdapter.getPositionByName(deckAdapter.getSwitchedName()),
null, switcher);
+ canvas.setLayoutParams(new
Gallery.LayoutParams(switcher.getWidth()*85/100, switcher.getHeight()));
+
canvas.setTranscriptMode(MessageListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
+ canvas.setDelegateTouchEvents(true);
deckAdapter.setSwitched(null, null);
return true;
}
diff --git a/application/src/org/yaaic/activity/ServersActivity.java
b/application/src/org/yaaic/activity/ServersActivity.java
index f516dc2..cfcf38d 100644
--- a/application/src/org/yaaic/activity/ServersActivity.java
+++ b/application/src/org/yaaic/activity/ServersActivity.java
@@ -344,4 +344,4 @@ public class ServersActivity extends ListActivity
implements ServiceConnection,
}
}
-}
\ No newline at end of file
+}
diff --git a/application/src/org/yaaic/adapter/DeckAdapter.java
b/application/src/org/yaaic/adapter/DeckAdapter.java
index a316e26..b6edd24 100644
--- a/application/src/org/yaaic/adapter/DeckAdapter.java
+++ b/application/src/org/yaaic/adapter/DeckAdapter.java
@@ -41,16 +41,28 @@ import android.widget.TextView;
*/
public class DeckAdapter extends BaseAdapter
{
- private LinkedList<Conversation> conversations;
+ private LinkedList<ConversationInfo> conversations;
private MessageListView currentView;
private String currentChannel;
+ public class ConversationInfo {
+ public Conversation conv;
+ public MessageListAdapter adapter;
+ public MessageListView view;
+
+ public ConversationInfo(Conversation conv) {
+ this.conv = conv;
+ this.adapter = null;
+ this.view = null;
+ }
+ }
+
/**
* Create a new DeckAdapter instance
*/
public DeckAdapter()
{
- conversations = new LinkedList<Conversation>();
+ conversations = new LinkedList<ConversationInfo>();
}
/**
@@ -58,7 +70,7 @@ public class DeckAdapter extends BaseAdapter
*/
public void clearConversations()
{
- conversations = new LinkedList<Conversation>();
+ conversations = new LinkedList<ConversationInfo>();
}
/**
@@ -71,15 +83,50 @@ public class DeckAdapter extends BaseAdapter
}
/**
+ * Get ConversationInfo on item at position
+ */
+ private ConversationInfo getItemInfo(int position) {
+ if (position >= 0 && position < conversations.size()) {
+ return conversations.get(position);
+ }
+ return null;
+ }
+
+ /**
* Get item at position
*/
@Override
public Conversation getItem(int position)
{
- if (position >= 0 && position < conversations.size()) {
- return conversations.get(position);
+ ConversationInfo convInfo = getItemInfo(position);
+ if (convInfo != null) {
+ return convInfo.conv;
+ } else {
+ return null;
}
- return null;
+ }
+
+ /**
+ * Get MessageListAdapter belonging to a conversation
+ *
+ * @param position Position of the conversation in the deck
+ */
+ public MessageListAdapter getItemAdapter(int position) {
+ ConversationInfo convInfo = getItemInfo(position);
+ if (convInfo != null) {
+ return convInfo.adapter;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get MessageListAdapter belonging to a conversation
+ *
+ * @param name Name of the conversation
+ */
+ public MessageListAdapter getItemAdapter(String name) {
+ return getItemAdapter(getPositionByName(name));
}
/**
@@ -99,7 +146,7 @@ public class DeckAdapter extends BaseAdapter
*/
public void addItem(Conversation conversation)
{
- conversations.add(conversation);
+ conversations.add(new ConversationInfo(conversation));
notifyDataSetChanged();
}
@@ -114,10 +161,10 @@ public class DeckAdapter extends BaseAdapter
{
// Optimization - cache field lookups
int mSize = conversations.size();
- LinkedList<Conversation> mItems = this.conversations;
+ LinkedList<ConversationInfo> mItems = this.conversations;
for (int i = 0; i < mSize; i++) {
- if (mItems.get(i).getName().equalsIgnoreCase(name)) {
+ if (mItems.get(i).conv.getName().equalsIgnoreCase(name)) {
return i;
}
}
@@ -187,17 +234,21 @@ public class DeckAdapter extends BaseAdapter
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
- Conversation conversation = getItem(position);
+ ConversationInfo convInfo = getItemInfo(position);
// Market stack traces prove that sometimes we get a null converstion
// because the collection changed while a view is requested for an
// item that does not exist anymore... so we just need to reply with
// some kind of view here.
- if (conversation == null) {
+ if (convInfo == null || convInfo.conv == null) {
return new TextView(parent.getContext());
}
- return renderConversation(conversation, parent);
+ if (convInfo.view != null) {
+ return convInfo.view;
+ } else {
+ return renderConversation(convInfo, parent);
+ }
}
/**
@@ -207,16 +258,17 @@ public class DeckAdapter extends BaseAdapter
* @param parent The parent view (context)
* @return The rendered MessageListView
*/
- public MessageListView renderConversation(Conversation conversation,
ViewGroup parent)
+ private MessageListView renderConversation(ConversationInfo convInfo,
ViewGroup parent)
{
MessageListView list = new MessageListView(parent.getContext(),
parent);
+ convInfo.view = list;
list.setOnItemClickListener(MessageClickListener.getInstance());
- MessageListAdapter adapter = conversation.getMessageListAdapter();
+ MessageListAdapter adapter = convInfo.adapter;
if (adapter == null) {
- adapter = new MessageListAdapter(conversation,
parent.getContext());
- conversation.setMessageListAdapter(adapter);
+ adapter = new MessageListAdapter(convInfo.conv,
parent.getContext());
+ convInfo.adapter = adapter;
}
list.setAdapter(adapter);
diff --git a/application/src/org/yaaic/listener/ConversationClickListener.java
b/application/src/org/yaaic/listener/ConversationClickListener.java
index 0b02db7..a3d8b91 100644
--- a/application/src/org/yaaic/listener/ConversationClickListener.java
+++ b/application/src/org/yaaic/listener/ConversationClickListener.java
@@ -28,7 +28,7 @@ import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
-import android.widget.TableLayout.LayoutParams;
+import android.widget.Gallery.LayoutParams;
import android.widget.ViewSwitcher;
/**
@@ -61,13 +61,11 @@ public class ConversationClickListener implements
OnItemClickListener
{
Conversation conversation = adapter.getItem(position);
- MessageListView canvas = adapter.renderConversation(conversation,
switcher);
+ MessageListView canvas = (MessageListView) adapter.getView(position,
null, switcher);
canvas.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
canvas.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
canvas.setDelegateTouchEvents(false); // Do not delegate
adapter.setSwitched(conversation.getName(), canvas);
- switcher.addView(canvas);
- switcher.showNext();
}
}
diff --git a/application/src/org/yaaic/model/Conversation.java
b/application/src/org/yaaic/model/Conversation.java
index 0d32bc4..43f3013 100644
--- a/application/src/org/yaaic/model/Conversation.java
+++ b/application/src/org/yaaic/model/Conversation.java
@@ -22,8 +22,6 @@ package org.yaaic.model;
import java.util.LinkedList;
-import org.yaaic.adapter.MessageListAdapter;
-
/**
* Base class for conversations
*
@@ -48,7 +46,6 @@ public abstract class Conversation
private final LinkedList<Message> buffer;
private final LinkedList<Message> history;
private final String name;
- private MessageListAdapter adapter;
private int status = 1;
/**
@@ -149,22 +146,6 @@ public abstract class Conversation
}
/**
- * Store the adapter of this conversation
- */
- public void setMessageListAdapter(MessageListAdapter adapter)
- {
- this.adapter = adapter;
- }
-
- /**
- * Get the MessageList Adapter of this conversation if known
- */
- public MessageListAdapter getMessageListAdapter()
- {
- return adapter;
- }
-
- /**
* Set status of conversation
*
* @param status
--
1.7.2.5