Friday, April 27, 2012

Do not create objects!!!

I posted this as an answer to a post about optimization at stackoverflow. This version is a bit more extended.


Something to think about: DO NOT overuse String, for example in a huge loop. This will create a lot of String objects that have to be GC'ed. The "Bad coding" example will produce 2 string objects every loop. Next example will produce only one final String and a single stringbuilder. This makes a huge difference when optimizing huge loops for speed. I used stringbuilder a lot when making my Wordlist Pro Android app, and it got really speedy when going through 270000 words in no time.
    //Bad coding:
    String s = "";
    for(int i=0;i<999999;i++){
        s = "Number=";
        s = s + i;
        System.out.println(s);
    }

    //Better coding
    final String txt = "Number=";
    StringBuilder sb = new StringBuilder();
    for(int i=0;i < 999999;i++){
        sb.setLength(0);
        sb.append(txt);
        sb.append(i);
        System.out.println(sb);
    }

As an example I wrote this short program to test the real difference in speed. My results are as follows (with my HTC Desire):


Normal String execution time(min-max): 1553ms-1703ms
StringBuilder execution time(min-max): 233ms-402ms


My tests shows that the StringBuilder is roughly about 4-6 times faster than the String in this test.


Most important thing here, is that when running the String version, the GC is invoked at least twice everytime I press the button. StringBuilder does not invoke the garbage collection at all.


Test it your self and keep an eye on the log when you press the buttons. Just copy and paste the code below.
/r0b

HelloWorldActivity.java


public class HelloWorldActivity extends Activity {
    private final static String TAG = "HELLO_DOUBLE";
    private static long startTime = 0;
    private static long estimatedTime = 0;
    private static final int LOOPS = 100000;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button bst = (Button) findViewById(R.id.button_string);
        bst.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TextView tv1 = (TextView) findViewById(R.id.text1);
                tv1.setText("NormalString:  "+ST()+"ms");
            }
        });
        Button bstb = (Button) findViewById(R.id.button_stringbuilder);
        bstb.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TextView tv2 = (TextView) findViewById(R.id.text2);
                tv2.setText("StringBuilder: "+SB()+"ms");
            }
        });
    }


    // String = Bad coding:
    private static long ST(){
        startTime = System.currentTimeMillis();    
        String s = "";
        for(int i=0;i < LOOPS;i++){
            s = "Number=";
            s = s + i;
            //Do something
        }
        estimatedTime = System.currentTimeMillis() - startTime;
        return estimatedTime;
    }


    // StringBuilder = Better coding
    private static long SB(){
        startTime = System.currentTimeMillis();    
        final String txt = "Number=";
        StringBuilder sb = new StringBuilder();
        for(int i=0;i < LOOPS;i++){
            sb.setLength(0);
            sb.append(txt);
            sb.append(i);
            //Do something
        }
        estimatedTime = System.currentTimeMillis() - startTime;
        return estimatedTime;
    }


}


main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >


<Button
android:id="@+id/button_string"
android:text="STRING"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</Button>


<TextView
android:id="@+id/text1"
android:text="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</TextView>


<Button
android:id="@+id/button_stringbuilder"
android:text="STRINGBUILDER"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</Button>


<TextView
android:id="@+id/text2"
android:text="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</TextView>


</LinearLayout>

Wednesday, April 25, 2012

Rounding a double in Java/Android

Rounding a double can be done in many ways, and I will list some of them by inserting them into an Android helloworld example.
Run the example and you will see the differences.

HelloWorldActivity.java
public class HelloWorldActivity extends Activity {
    private final static String TAG = "HELLO_DOUBLE";
    private final static int STARTLOOPS = 0;
    private final static int ENDLOOPS = 20;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        StringBuilder sb = new StringBuilder();
        double my_number = 3.72323252463374574432642356;
        double value = 0.0;

        sb.append("Pow10 Round\n");
        for(int i = STARTLOOPS ; i < ENDLOOPS ; i++) {
            value = pow10Round(my_number, i);
            sb.append("*"+i+"="+value+"\n");
        }
     
        sb.append("BigDecimal\n");
        for(int i = STARTLOOPS ; i < ENDLOOPS ; i++) {
            value = bdRound(my_number, i, BigDecimal.ROUND_HALF_UP);
            sb.append("*"+i+"="+value+"\n");
        }

        sb.append("NumberFormat\n");
        for(int i = STARTLOOPS ; i < ENDLOOPS ; i++) {
            sb.append("*"+i+"="+formatDouble(my_number,0,i)+"\n");
        }

        TextView tv = (TextView) findViewById(R.id.text);
        tv.setText(sb.toString());
    }

    public static double pow10Round(double unrounded,int precision) {
        double pow = Math.pow(10, precision);
        return Math.round(unrounded * pow) / pow;
    }

    public static double bdRound(double unrounded, int precision, int roundingMode){
        BigDecimal bd = new BigDecimal(unrounded);
        BigDecimal rounded = bd.setScale(precision, roundingMode);
        return rounded.doubleValue();
    }

    public static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance();
    public static String formatDouble(double doubleToFormat, int min, int max) {
        NUMBER_FORMAT.setMaximumFractionDigits(max);
        NUMBER_FORMAT.setMinimumFractionDigits(min);
        return NUMBER_FORMAT.format(doubleToFormat);
    }

}

main.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="fill_parent" >
</TextView>
</LinearLayout>
</ScrollView>

Bye for now.
/r0b

Tuesday, April 24, 2012

Java binary search in a sorted textfile


When making the Android app 'Wordlist Pro', I needed a fast binary search for finding single words in a huge A-Z sorted wordlist file. I was looking all over the internet for a working ready-to-go solution. I could not find a well working one, so I had to make my own version out of what I found. The result was working perfectly.

This is what I ended up with.

target = the word in the file you are looking for


    public boolean binarySearch(RandomAccessFile raf, String target)
            throws IOException {

        raf.seek(0);
        int counter = 0;
        String line = raf.readLine();

        if (line.compareTo(target) == 0) {
            return true;
        }

        long low = 0;
        long high = raf.length();
        long p = -1;

        while (low < high) {

            long mid = (low + high) >>> 1;
            p = mid;
            while (p >= 0) {
                raf.seek(p);
                char c = (char) raf.readByte();
                if(c == '\n')
                    break;
                p--;
            }
            if(p < 0)
                raf.seek(0);

            line = raf.readLine();

            if( (line == null) || line.compareTo(target) < 0)
                low = mid + 1;
            else
                high = mid;
        }

        p = low;
        while (p >= 0){
            raf.seek(p);
            if(((char) raf.readByte()) == '\n')
                break;
            p--;
        }
        if(p < 0)
            raf.seek(0);

        while(true){
            line = raf.readLine();
            if(line == null || !line.equals(target))
                break;
            return true;
        }

        // Nothing found
        return false;

    }

To use it, you can do like this:

            try{
                File f = new File("MyLargeSortedFile.txt");
                if( f.isFile() && f.canRead() && (f.length() > 0) ){
                    RandomAccessFile raf = new RandomAccessFile(f, "r");
                    if(binarySearch(raf, s)){
                        // Match found.
                        raf.close();
                        return true;
                    }
                    // No Match
                    raf.close();
                }
            }catch(FileNotFoundException e){
            }catch(IOException e){
            }


Use it to whatever you like.
'+1' license applies
(press the +1 button)

/r0b

Monday, April 23, 2012

Listview, soundboard & Sherlock

Here we go again. This is a complete code for a simple listview with an image and a textview. When item is pressed, it plays a soundfile.
It features a listadapter, backpress toast and uses the Sherlock library found here: http://actionbarsherlock.com/. Dont forget including the Android compability library as well if you use Sherlock.

ActionBarSherlock

You can easily remake it to a normal listactivity by removing the "Sherlock" and just extend ListActivity.
Place the sound and imagefiles in assets folder named: 0.ogg, 1.ogg ... 0.png, 1.png ...

In my app, the result looks like this. The app is found here:
https://play.google.com/store/apps/details?id=se.ernell.sound.fartingzoo


Its easy to remake for your own looks and colors.
Just edit main.xml and list_item.xml

SoundboardActivity.java

public class SoundboardActivity extends SherlockListActivity {

    private final static String  TAG   = "SOUNDBOARD";
    private final static boolean debug = false;//debug mode
    private final static String  SOUND_EXTENSION = ".ogg";
    private final static String  IMAGE_EXTENSION = ".png";

    private MediaPlayer         mMediaPlayer;
    private AssetFileDescriptor mAssetFileDescriptor;

    private Toast mToast;
    private long  mLastBackPressTime = 0;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);
        setVolumeControlStream(AudioManager.STREAM_MUSIC);
        mMediaPlayer = new MediaPlayer();

        try{
            String[] titles = getResources().getStringArray(R.array.titles_array);
            List<String> al = new ArrayList<String>();
            for(int i=0;i<titles.length;i++)
                al.add(titles[i]);

            CustomAdapter ca = new CustomAdapter(this, R.layout.list_item, al);
            setListAdapter(ca);

            ListView lv = getListView();
            lv.setOnItemClickListener(new OnItemClickListener() {
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    try{
                        if ( mMediaPlayer.isPlaying() )
                            mMediaPlayer.stop();
                        mMediaPlayer.reset();
                        mAssetFileDescriptor = getAssets().openFd(position + SOUND_EXTENSION);
                        mMediaPlayer.setDataSource(mAssetFileDescriptor.getFileDescriptor(), mAssetFileDescriptor.getStartOffset(), mAssetFileDescriptor.getLength());
                        mMediaPlayer.prepare();
                        mMediaPlayer.start();
                    } catch (Exception e) {
                            Log.d(TAG, "Exception: "+e.getLocalizedMessage());
                    }
                }
            });
        } catch (Exception e) {
            Log.d(TAG, "Exception in onCreate(): "+e.getLocalizedMessage());
        }
    }

    @Override
    public void onBackPressed() {
        if ( mLastBackPressTime < (System.currentTimeMillis() - 3000) ) {
            mToast = Toast.makeText(this, "Press back again to close the app", 4000);
            mToast.show();
            mLastBackPressTime = System.currentTimeMillis();
        } else {
            if (mToast != null)
                mToast.cancel();
            super.onBackPressed();
        }
    }

    public static class CustomAdapter extends ArrayAdapter<String>{

        private static LayoutInflater mInflater;
        private static List<String>   mItems;
        private static int            mTextViewResourceId;
        private static Context        mContext;

        public CustomAdapter(SoundboardActivity context, int textViewResourceId, List<String> items) {
            super(context, textViewResourceId, items);
            mContext = context;
            if(textViewResourceId == 0)
                mTextViewResourceId = R.layout.list_item;
            else
                mTextViewResourceId = textViewResourceId;
            mItems = items;
            mInflater = LayoutInflater.from(context);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;

            if (convertView == null) {
                convertView = mInflater.inflate(mTextViewResourceId, null);
                holder = new ViewHolder();
                holder.text = (TextView)  convertView.findViewById(R.id.text);
                holder.icon = (ImageView) convertView.findViewById(R.id.img);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.text.setText(  mItems.get(position) );
            String filename = position + IMAGE_EXTENSION;
            try{
                Bitmap bm = getBitmapFromAsset(filename);
                if(bm != null){
                    holder.icon.setVisibility(View.VISIBLE);
                    holder.icon.setImageBitmap(bm);
                }else{
                    Log.d(TAG,"Error: Bitmap asset ["+filename+"] == null");
                }
            }catch(IOException ioe){
                Log.d(TAG,"IOException: Error retrieving image ["+filename+"] from asset folder. Message: "+ioe.getLocalizedMessage());
                holder.icon.setVisibility(View.GONE);
            }
            return convertView;

        }

        private static Bitmap getBitmapFromAsset(String strName) throws IOException{
            AssetManager assetManager = mContext.getAssets();
            InputStream istr = assetManager.open(strName);
            Bitmap bitmap = BitmapFactory.decodeStream(istr);
            istr.close();
            return bitmap;
        }

        static class ViewHolder {
            TextView text;
            ImageView icon;
        }

    }

}


main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/se.ernell.adp"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <!-- ListView -->

    <ListView
        android:id="@android:id/list"
        android:layout_marginTop="5dp"
        android:layout_width="fill_parent"
        android:layout_height="0dip"
        android:layout_weight="1"
        android:background="@android:color/transparent"
        android:cacheColorHint="#00000000"
        android:divider="#ff33b5e5"
        android:dividerHeight="0.1dp"
        android:drawSelectorOnTop="false"
        android:footerDividersEnabled="false"
        android:headerDividersEnabled="false"
        android:paddingBottom="0dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="0dp" />

</LinearLayout>



list_item.xml

Change imageview layout_width+height to your prefs.


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

    <ImageView
        android:id="@+id/img"
        android:scaleType="centerCrop"
        android:layout_width="100dp"
        android:layout_height="100dp" />

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:paddingLeft="10dp"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >

        <TextView
            android:layout_width="fill_parent"
            android:id="@+id/text"
            android:layout_height="wrap_content"
            android:textStyle="bold"
            android:textColor="#33b5e5"
            android:textSize="24dp" />
    </LinearLayout>

</LinearLayout>

Include this in strings.xml
For every item in the array, make sure there is a .ogg + .png file in assets.
In this case you need 0.ogg, 1.ogg, 2.ogg, 0.png, 1.png, 2.png

    <!-- Sound titles -->
    <string-array name="titles_array">
        <item>Itemtext 0</item>
        <item>Itemtext 1</item>
        <item>Itemtext 2</item>
    </string-array>

Use the code as you wish.
/r0b