0

If I call the following RoboVM method with any non-null argument:

public static void runOnUiThread(final Runnable runnable) {
    System.out.println("Inside runOnUiThread():");
    System.out.println("  Null-check: "+(runnable==null));

    NSOperation operation = new NSOperation() {

        @Override
        public void main() {
            System.out.println("Inside main():");
            System.out.println("  Null-check: "+(runnable==null));
            runnable.run();                 // NullPointerException here?!? How???
            System.out.println("  main() completed");
        }
    };

    NSOperationQueue.getMainQueue().addOperation(operation);        
}

it outputs:

Inside runOnUiThread():
  Null-check: false
Inside main():
  Null-check: true
java.lang.NullPointerException
    at RoboVMTools$1.main(RoboVMTools.java)
    at org.robovm.apple.foundation.NSOperation.$cb$main(NSOperation.java)
    at org.robovm.apple.uikit.UIApplication.main(Native Method)
    at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
    at Main.main(Main.java)

What on earth is going on??? And more importantly, how can I work around it?

  • I tried adding operation.addStrongRef(runnable); right before NSOperationQueue.... No difference.
  • I also tried moving the anonymous inner class into its own class that has a private final field to store the runnable which is passed into its constructor. Same result.

Am I just missing something totally obvious???

Markus A.
  • 12,349
  • 8
  • 52
  • 116

2 Answers2

1

You are right about the GC. Your NSOperation instance is garbage collected before the operation is invoked from the Objective-C side. When NSOperationQueue calls into the Java side a new instance of your NSOperation anonymous class will be created which doesn't have a reference to the Runnable instance but rather null and the result is a NullPointerException getting thrown.

The way you resolved it using addStrongRef() is correct though only the mainQueue.addStrongRef(operation) and the corresponding removeStrongRef() calls should be sufficient:

public static void runOnUiThread(final Runnable runnable) {

    final NSOperationQueue mainQueue = NSOperationQueue.getMainQueue();

    NSOperation operation = new NSOperation() {

        @Override
        public void main() {
            runnable.run();
            mainQueue.removeStrongRef(this);
        }
    };

    mainQueue.addStrongRef(operation);
    mainQueue.addOperation(operation);      
}

This will prevent the Java operation instance (and any Java objects reachable from it like the Runnable) from being GCed until the Objective-C NSOperationQueue instance is deallocated. As the Objective-C side queue is a singleton it won't get deallocated during the lifetime of the app.

The RoboVM NSOperationQueue Java class provides a version of the addOperation() method that takes a Runnable. When using this method RoboVM will take care of retaining the Runnable instance while it's needed by the Objective-C side for you. The same is true for any method that takes a @Block annotated parameter of type Runnable or any of the org.robovm.objc.block.VoidBlock* or org.robovm.objc.block.Block* interfaces.

Using this addOperation() method your code simply becomes:

public static void runOnUiThread(Runnable runnable) {
    NSOperationQueue.getMainQueue().addOperation(runnable);      
}

PS. The GC used by RoboVM has nothing to do with the Apple garbage collector so Apple's docs won't help you understand problems like this.

ntherning
  • 1,160
  • 7
  • 8
  • That makes perfect sense. Thank you. +1 for `addOperation(Runnable)`. I completely missed that! :) By the way: RoboVM is absolutely amazing! I am sooooo glad I don't have to deal with Objective-C (yuck!)! You guys are awesome! Thank you so much! The best day of my life will be when there's a RoboVM-version that can build apps for Windows Phone! ;) Would that be hard, given the infrastructure you already have? Or are you already working on it? :) – Markus A. Jun 07 '15 at 15:32
  • I'm happy to hear that you like RoboVM! No, we're not working on Windows Phone support. MS recently announced [Project Astoria](https://dev.windows.com/en-us/uwp-bridges/project-astoria) which will bring Android apps to Windows Phone. Once that's out porting RoboVM to Windows Phone will not make much sense. Would have been an interesting challenge though... :-) – ntherning Jun 08 '15 at 18:41
0

Well... This fixes it:

public static void runOnUiThread(final Runnable runnable) {

    final NSOperationQueue mainQueue = NSOperationQueue.getMainQueue();

    NSOperation operation = new NSOperation() {

        @Override
        public void main() {
            runnable.run();

            mainQueue.removeStrongRef(runnable);
            mainQueue.removeStrongRef(this    );
        }
    };

    mainQueue.addStrongRef(runnable );
    mainQueue.addStrongRef(operation);

    mainQueue.addOperation(operation);      
}

But don't ask my why this is necessary. The Apple docs say "In garbage-collected applications, the queue strongly references the operation object." So, operation.addStrongRef(runnable); as I tried earlier should have been sufficient as the operation object should be referenced by the queue anyways. But I guess the world doesn't always work the way I interpret the documentation.

Markus A.
  • 12,349
  • 8
  • 52
  • 116