Skip to content
Uwe Kubosch edited this page Jun 16, 2013 · 11 revisions

Here is a description of the most typical startup use case for Ruboto: Starting the initial Activity. We will try to update this page as Ruboto and JRuby evolves. If you see anything you can improve, please add your contribution.

We will use the Ruboto Benchmark Client as an example: https://github.com/ruboto/ruboto_benchmark_client

  • It all starts by Android forking the "Zygote" process and adding the apk classes to the class path.
  • Android reads the AndroidManifest.xml file and loads the Activity class that is marked as the launcher activity:
        <activity android:label='@string/app_name' android:name='StartupTimerActivity'>
            <intent-filter>
                <action android:name='android.intent.action.MAIN'/>
                <category android:name='android.intent.category.LAUNCHER'/>
            </intent-filter>
        </activity>
  • At this time the class initialization code of the Activity class and its super classes is run. In the benchmark app, this is when we start measuring the total startup time.
package org.ruboto.benchmarks;

public class StartupTimerActivity extends org.ruboto.EntryPointActivity {
    public static final long START = System.currentTimeMillis();
    public static long jrubyStart;
    public static long jrubyLoaded;
    public static long fireRubotoActivity;
    public static long scriptLoaded;
    public static Long stop;
    public static long platformInstallationStart;
    public static long platformInstallationDone;
}
package org.ruboto;

public class EntryPointActivity extends org.ruboto.RubotoActivity {
    private static final int INSTALL_REQUEST_CODE = 4242;
...
    private static final String RUBOTO_APK = "RubotoCore-release.apk";
    private static final String RUBOTO_URL = "http://ruboto.org/downloads/" + RUBOTO_APK;
}
package org.ruboto;

public class RubotoActivity extends android.app.Activity implements org.ruboto.RubotoComponent {
    public static final String THEME_KEY = "RUBOTO_THEME";
...
}
  • After the class has been loaded and initialised, it is instantiated. At this time the object initialization code is run.
package org.ruboto.benchmarks;

public class StartupTimerActivity extends org.ruboto.EntryPointActivity {
}
package org.ruboto;

public class EntryPointActivity extends org.ruboto.RubotoActivity {
    private int splash = 0;
    private ProgressDialog loadingDialog;
    private boolean dialogCancelled = false;
    private BroadcastReceiver receiver;
    private long enqueue;
    java.io.File localFile;
    protected boolean appStarted = false;
...
}
package org.ruboto;

public class RubotoActivity extends android.app.Activity implements org.ruboto.RubotoComponent {
    private final ScriptInfo scriptInfo = new ScriptInfo();
    private String remoteVariable = null;
    Bundle[] args;
...
}
  • Next the object constructor with no arguments is called. It is usually not implemented. When no constructor is defined, a default public no-args constructor is created implicitly.

  • After the object is instantiated, Android calls onCreate(bundle) on the object. This happens before the object is displayed. As of Ruboto 0.13.0, the launcher activity has no custom onCreate method. The magic starts in EntryPointActivity. Here we set the class name of the Ruby class that will be created for this Activity. This will implicitly set the name of the script file that the Ruby code will be loaded from. We also detect if we should display a progress dialog or a splash layout during JRuby initialization. This is done by checking if res/layout/splash.xml is defined. Last we check if JRuby is already initialised. In our case it is not, so we fire the initJRuby(true)method before calling RubotoActivity.onCreate(bundle). The super call is required, or Android will kill our Activity. initJRuby(true) will display the startup progress screen through showProgress()and then start the JRuby initialization in a background thread.

package org.ruboto;

public class EntryPointActivity extends org.ruboto.RubotoActivity {
...
    public void onCreate(Bundle bundle) {
        getScriptInfo().setRubyClassName(getClass().getSimpleName());

        localFile = new java.io.File(getFilesDir(), RUBOTO_APK);
        try {
            splash = Class.forName(getPackageName() + ".R$layout").getField("splash").getInt(null);
            requestWindowFeature(android.view.Window.FEATURE_NO_TITLE);
        } catch (Exception e) {
            splash = -1;
        }

        if (JRubyAdapter.isInitialized()) {
            appStarted = true;
        } else {
            initJRuby(true);
        }
        super.onCreate(bundle);
    }
...
}

In RubotoActivity in our case we only store the intent for later use, and call super.onCreate(bundle) since JRuby still is not initialised.

package org.ruboto;

public class RubotoActivity extends android.app.Activity implements org.ruboto.RubotoComponent {
...
    public void onCreate(Bundle bundle) {
       ...
        scriptInfo.setFromIntent(getIntent());

        if (JRubyAdapter.isInitialized() && scriptInfo.isReadyToLoad()) {
    	    ScriptLoader.loadScript(this);
    	    ScriptLoader.callOnCreate(this, (Object[]) args);
        } else {
            super.onCreate(bundle);
        }
    }
...
}

The startup progress screen has now been displayed, and JRuby is initialising in the background. When JRuby initialization is completed, the progress dialog is removed, and the onCreate(bundle)method on Ruby object is called, followed by calls to onStart()and onResume() calls on the Ruby object.

JRuby initialization

The initialization of JRuby happens in JRubyAdapter.setUpJRuby(EntryPointActivity.this).

First we have an optional section to force expansion of the object heap. You can configure this in ruboto.yml with the heap_alloc: 13 setting. It triggers two garbage collections directly, but subsequent garbage collections will be GC_CONCURRENT and much faster than GC_FOR_ALLOC.

    public static synchronized boolean setUpJRuby(Context appContext, PrintStream out) {
        if (!initialized) {
            // BEGIN Ruboto HeapAlloc
            // @SuppressWarnings("unused")
            // byte[] arrayForHeapAllocation = new byte[13 * 1024 * 1024];
            // arrayForHeapAllocation = null;
            // END Ruboto HeapAlloc

Next we set if we are running a debug or release build.

            setDebugBuild(appContext);
            Log.d("Setting up JRuby runtime (" + (isDebugBuild ? "DEBUG" : "RELEASE") + ")");

Next come system properties that change the behaviour of JRuby. These are currently not configurable else where, but it is likely that we will add configuration settings for some of them in ruboto.yml at some time.

            System.setProperty("jruby.compile.mode", "OFF"); // OFF OFFIR JITIR? FORCE FORCEIR
            // System.setProperty("jruby.compile.backend", "DALVIK");
            System.setProperty("jruby.bytecode.version", "1.6");
            System.setProperty("jruby.interfaces.useProxy", "true");
            System.setProperty("jruby.management.enabled", "false");
            System.setProperty("jruby.objectspace.enabled", "false");
            System.setProperty("jruby.thread.pooling", "true");
            System.setProperty("jruby.native.enabled", "false");
            // System.setProperty("jruby.compat.version", "RUBY2_0"); // RUBY1_9 is the default in JRuby 1.7
            System.setProperty("jruby.ir.passes", "LocalOptimizationPass,DeadCodeElimination");
            System.setProperty("jruby.backtrace.style", "normal"); // normal raw full mri

            // Uncomment these to debug/profile Ruby source loading
            // System.setProperty("jruby.debug.loadService", "true");
            // System.setProperty("jruby.debug.loadService.timing", "true");

            // Used to enable JRuby to generate proxy classes
            System.setProperty("jruby.ji.proxyClassFactory", "org.ruboto.DalvikProxyClassFactory");
            System.setProperty("jruby.class.cache.path", appContext.getDir("dex", 0).getAbsolutePath());

Next we try to load the JRuby ScriptingContainer. This is the main entry class for using JRuby embedded. The scripting container can be loaded from this app or from the RubotoCore app.

            ClassLoader classLoader;
            Class<?> scriptingContainerClass;
            String apkName = null;

            try {
                scriptingContainerClass = Class.forName("org.jruby.embed.ScriptingContainer");
                System.out.println("Found JRuby in this APK");
                classLoader = JRubyAdapter.class.getClassLoader();
                try {
                    apkName = appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), 0).sourceDir;
                } catch (NameNotFoundException e) {}
            } catch (ClassNotFoundException e1) {
                String packageName = "org.ruboto.core";
                try {
                    PackageInfo pkgInfo = appContext.getPackageManager().getPackageInfo(packageName, 0);
                    apkName = pkgInfo.applicationInfo.sourceDir;
                    RUBOTO_CORE_VERSION_NAME = pkgInfo.versionName;
                } catch (PackageManager.NameNotFoundException e2) {
                    System.out.println("JRuby not found in local APK:");
                    e1.printStackTrace();
                    System.out.println("JRuby not found in platform APK:");
                    e2.printStackTrace();
                    return false;
                }

                System.out.println("Found JRuby in platform APK");
                classLoader = new PathClassLoader(apkName, JRubyAdapter.class.getClassLoader());

                try {
                    scriptingContainerClass = Class.forName("org.jruby.embed.ScriptingContainer", true, classLoader);
                } catch (ClassNotFoundException e) {
                    // FIXME(uwe): ScriptingContainer not found in the platform APK...
                    e.printStackTrace();
                    return false;
                }
            }

After the ScriptingContainer class has been loaded, we call some configuration methods on it to tailor the behaviour. The methods are called using reflection because we don't know which class loader it is loaded from.

            try {
                String jrubyHome = "file:" + apkName + "!/jruby.home";
                System.setProperty("jruby.home", jrubyHome);
                if (out != null) {
                  output = out;
                }
                Class rubyClass = Class.forName("org.jruby.Ruby", true, scriptingContainerClass.getClassLoader());
                Class rubyInstanceConfigClass = Class.forName("org.jruby.RubyInstanceConfig", true, scriptingContainerClass.getClassLoader());

                Object config = rubyInstanceConfigClass.getConstructor().newInstance();
                rubyInstanceConfigClass.getMethod("setDisableGems", boolean.class).invoke(config, true);
                rubyInstanceConfigClass.getMethod("setLoader", ClassLoader.class).invoke(config, classLoader);

                if (output != null) {
                    rubyInstanceConfigClass.getMethod("setOutput", PrintStream.class).invoke(config, output);
                    rubyInstanceConfigClass.getMethod("setError", PrintStream.class).invoke(config, output);
                }

Next we create the JRuby instance!

                // This will become the global runtime and be used by our ScriptingContainer
                rubyClass.getMethod("newInstance", rubyInstanceConfigClass).invoke(null, config);

A bit more settings before we are ready...

                Class scopeClass = Class.forName("org.jruby.embed.LocalContextScope", true, scriptingContainerClass.getClassLoader());
                Class behaviorClass = Class.forName("org.jruby.embed.LocalVariableBehavior", true, scriptingContainerClass.getClassLoader());

Finally we can create the ScriptingContainer!

                ruby = scriptingContainerClass
                         .getConstructor(scopeClass, behaviorClass)
                         .newInstance(Enum.valueOf(scopeClass, localContextScope), 
                                      Enum.valueOf(behaviorClass, localVariableBehavior));

Even more settings...

                Method setClassLoaderMethod = ruby.getClass().getMethod("setClassLoader", ClassLoader.class);
                setClassLoaderMethod.invoke(ruby, classLoader);

                Thread.currentThread().setContextClassLoader(classLoader);

Set up load path.

                if (appContext.getFilesDir() != null) {
                    String defaultCurrentDir = appContext.getFilesDir().getPath();
                    Log.d("Setting JRuby current directory to " + defaultCurrentDir);
                    callScriptingContainerMethod(Void.class, "setCurrentDirectory", defaultCurrentDir);
                } else {
                    Log.e("Unable to find app files dir!");
                }

                addLoadPath(scriptsDirName(appContext));
    	          put("$package_name", appContext.getPackageName());

Finally we are done!

                initialized = true;
            } catch (ClassNotFoundException e) {
                handleInitException(e);
            } catch (IllegalArgumentException e) {
                handleInitException(e);
            } catch (SecurityException e) {
                handleInitException(e);
            } catch (InstantiationException e) {
                handleInitException(e);
            } catch (IllegalAccessException e) {
                handleInitException(e);
            } catch (InvocationTargetException e) {
                handleInitException(e);
            } catch (NoSuchMethodException e) {
                handleInitException(e);
            }
        }
        return initialized;
    }

Further reading

Clone this wiki locally