Reputation: 464
I have created an app that fetches data (job ads) from an XML file on my server and displays those job ads in my application. When I checked my app, it was using 30-50mb which I believe is too much for such a simple app. I tried running it on some lower end devices (24mb heap size) and it works okay, a bit laggy but no OOM or any other crashes, so I don't think it's a memory leak.
I'm using global arrays with an initial size of 100 each, and once the XML file is loaded, arrays get resized to the number of nodes in the XML file.
Global variables
private final Integer MAX_SIZE = 100;
public String[] ID = new String[MAX_SIZE];
public String[] Category = new String[MAX_SIZE];
public String[] Title = new String[MAX_SIZE];
public String[] Content = new String[MAX_SIZE];
public String[] Link = new String[MAX_SIZE];
public Integer[] JobID = new Integer[MAX_SIZE];
public String[] nID = new String[MAX_SIZE];
public String[] nCategory = new String[MAX_SIZE];
public String[] nTitle = new String[MAX_SIZE];
public String[] nContent = new String[MAX_SIZE];
public String[] nLink = new String[MAX_SIZE];
public int[] nJobID = new int[MAX_SIZE];
public int[] nExists = new int[MAX_SIZE];
This is the code I use to fetch the XML and load entries into variables
private class LOADXML extends AsyncTask<String, Void, Void> {
@Override
protected void onPreExecute() {
if (swipeLayout != null) {
if (!swipeLayout.isRefreshing()) {
((MainActivity) getActivity()).load();
}
} else {
((MainActivity) getActivity()).load();
}
Arrays.fill(((MainActivity) getActivity()).jobCOUNT, 0);
}
@Override
protected Void doInBackground(String... urls) {
if (getActivity() == null) {
cancel(true);
}
if (!((MainActivity) getActivity()).fromNotif && ((MainActivity) getActivity()).refresh) {
try {
XMLParser parser = new XMLParser();
String xml = parser.getXmlFromUrl(URL); // getting XML
Document doc = parser.getDomElement(xml); // getting DOM element
Context cx = getActivity();
WriteXMLToFile(xml, cx);
NodeList nl = doc.getElementsByTagName(KEY_JOB);
listSize = 0;
SetArrayLength(nl.getLength());
// looping through all item nodes <item>
for (int i = 0; i < nl.getLength(); i++) {
if (getActivity() == null) {
cancel(true);
}
Element e = (Element) nl.item(i);
listSize += 1;
setGlobalVars(listSize - 1, parser.getValue(e, KEY_ID), parser.getValue(e, KEY_CATEGORY), parser.getValue(e, KEY_TITLE), parser.getValue(e, KEY_CONTENT), parser.getValue(e, KEY_LINK), parser.getValue(e, KEY_JOBID));
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
Activity activity = getActivity();
if (activity != null && !isCancelled()) {
((MainActivity) getActivity()).unload();
((MainActivity) getActivity()).refresh = false;
updateCountVar();
updateList();
if (swipeLayout.isRefreshing()) {
LinearLayout container = (LinearLayout) vw.findViewById(frag_container);
container.removeAllViews();
if (swipeLayout != null) {
swipeLayout.setRefreshing(false);
isRefreshing = false;
}
showJobBox(li, vw);
} else {
showJobBox(li, vw);
}
}
}
}
And this is the part of code that is in charge of populating the fragment with data
listSize = ((MainActivity) getActivity()).nID.length;
for (Integer i = 0; i < listSize; i++) {
if (filterEntry(i) && (Integer.valueOf(((MainActivity) getActivity()).nJobID[i]) != null)) {
// Layout params
@SuppressLint("InflateParams") View item_layout = inflater.inflate(R.layout.job_box, null, false);
LinearLayout container = (LinearLayout) view.findViewById(frag_container);
TextView category = (TextView) item_layout.findViewById(R.id.jobBox_category);
TextView title = (TextView) item_layout.findViewById(R.id.jobBox_title);
TextView description = (TextView) item_layout.findViewById(R.id.jobBox_description);
TextView jobBtn = (TextView) item_layout.findViewById(R.id.jobBox_button);
TextView jobIDbox = (TextView) item_layout.findViewById(R.id.jobIDbox);
ImageButton imgBtn = (ImageButton) item_layout.findViewById(R.id.imageButton);
TextView newLabel = (TextView) item_layout.findViewById(R.id.jobBox_new);
Integer nExistsInteger = ((MainActivity) getActivity()).nExists[i];
if (nExistsInteger == 0) {
newLabel.setVisibility(View.VISIBLE);
}
// ##############################################
int id = i + 10000;
item_layout.setId(id);
category.setText(((MainActivity) getActivity()).nCategory[i]);
title.setText(((MainActivity) getActivity()).nTitle[i]);
description.setText(((MainActivity) getActivity()).nContent[i]);
Integer ido = ((MainActivity) getActivity()).nJobID[i];
jobIDbox.setText("Šifra oglasa: " + String.valueOf(ido));
final Integer a = i;
jobBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String url = ((MainActivity) getActivity()).Link[a];
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
i.setData(Uri.parse(url));
startActivity(i);
}
});
imgBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Studentski posao");
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, ((MainActivity) getActivity()).nTitle[a]
+ " - " + ((MainActivity) getActivity()).nLink[a]
+ " (Via http://bit.ly/StudentServis)");
startActivity(Intent.createChooser(sharingIntent, "Podijeli koristeći"));
}
});
RelativeLayout.LayoutParams params1 = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
if (i > 0) {
params1.addRule(RelativeLayout.ALIGN_BOTTOM, id - 1);
item_layout.setLayoutParams(params1);
} else {
item_layout.setPadding(10, 10, 10, 10);
}
container.addView(item_layout);
if (i == listSize - 1) {
@SuppressLint("InflateParams") View item_layout_dummy = inflater.inflate(R.layout.dummy, null);
container.addView(item_layout_dummy);
}
((MainActivity) getActivity()).ID[i] = ((MainActivity) getActivity()).nID[i];
((MainActivity) getActivity()).Category[i] = ((MainActivity) getActivity()).nCategory[i];
((MainActivity) getActivity()).Title[i] = ((MainActivity) getActivity()).nTitle[i];
((MainActivity) getActivity()).Content[i] = ((MainActivity) getActivity()).nContent[i];
((MainActivity) getActivity()).Link[i] = ((MainActivity) getActivity()).nLink[i];
((MainActivity) getActivity()).JobID[i] = ((MainActivity) getActivity()).nJobID[i];
}
}
I made sure that when the fragment is switched (to a different category), if data has already been loaded, LOADXML doInBackground is skipped.
My question is, is there a more efficient and memory friendlier way to achieve this? This is my first app that has dynamic content.
Upvotes: 1
Views: 87
Reputation: 38241
Phase 1) Use objects.
Ditch arrays. The way you use them is ineffective, the system can do much work for you. You will at least half the memory used.
In your activity instead of numerous arrays (duplicated also) have just one list:
private ArrayList<Job> jobs = new ArrayList<Job>();
Along with it create a Holder pattern class for your jobs.
public static class Job {
public int jobId;
public String id, category, title, content, link;
public boolean exists;
}
The above saves you from ineffectively managing your data using multiple arrays.
Now rewrite your XML loading task to load all jobs into the list in the background. Only when that is done start updating views.
private class LoadXml extends AsyncTask> {
@Override
protected void onPreExecute() {
//
}
@Override
protected List<Job> doInBackground(String... urls) {
try {
final List<Job> jobs = new ArrayList<Job>();
// this method is written in pseudo code as I don't know anything about parsing XML
// assume one url containing all jobs
String xml = getXmlFromUrl(urls[0]);
// loop through each Job element
for(Element e : doc) {
// create a new Job instance
Job job = new Job();
// load the Job data
job.id = e.getAttribute("id");
// etc.
// add the Job to the list
jobs.add(job);
}
// return Job list
return jobs;
} catch (Exception ex) {
ex.printStackTrace();
// if anything fails, return null to indicate
return null;
}
}
@Override
protected void onPostExecute(List<Job> result) {
// send the list to activity and let it handle it
((MainActivity)getActivity()).updateJobList(result);
// MainActivity.updateJobList() will contain pretty much what was here.
}
}
Phase 2) Limit data usage
You do not need to download all your jobs at once. Download ten, when the user reaches end of the list allow him to download another ten. But that topic is beyond this post.
Upvotes: 2