amirrajan / rubymotion-applied

RubyMotion documentation provided by the community. Submit a pull request to the docs for a free one year indie subscription.
Apache License 2.0
49 stars 10 forks source link

Retain cycle between two blocks #92

Open amirrajan opened 6 years ago

amirrajan commented 6 years ago

From Slack:

Do blocks passed to a method in RM retain a weakref to their closure?

Context? The lambda assigned stay in scope with the object it's assigned to and is GC'ed with the object (generally speaking)

It's a part of the RubyMotion's virtual machine proc.c.

Which (in spirit) is equivalent to: https://github.com/ruby/ruby/blob/trunk/proc.c

Here’s a very contrived example:

class One

  def initialize
    @two = Two.new do
      # do callback stuff
    end
  end

end

class Two

  def initialize(&callback)
    @callback = callback
  end

end

@one = One.new
commit 3e7ebcd168cfd50532741862b92abfe89ec0d6fc
Author: Watson <watson1978@gmail.com>
Date:   Wed Feb 8 12:17:54 2017 +0900

    [RM-534] create weak reference inside proc object

commit 2e49fc1c197da03d61fa5cd14cf6ee28cc99d89e
Author: Watson <watson1978@gmail.com>
Date:   Wed Jun 8 09:23:10 2016 +0900

    improve Hash#{each, each_pair} performance

    When retrives {key, value} as block parameter,
    skip creating Array object via `rb_assoc_new()`

    * before
          user     system      total        real
      1.500000   0.280000   1.780000 (  1.786080)

    * after
          user     system      total        real
      0.250000   0.000000   0.250000 (  0.248885)

    * code
      hash = {}
      100.times do |i|
        hash[i.to_s] = i
      end

      Benchmark.bm do |x|
        x.report do
          100000.times do
            hash.each do |k, v|
            end
          end
        end
      end

When I run the contrived example, there is a retain cycle between @one and @two without calling .weak! when assigning the block to the instance variable

when both go out of scope they should be GCed within the runtime. Any indication that's not happening?

Yea, unless I’m doing something wrong:

class One
  attr_reader :two

  def initialize
    @two = Two.new do
      puts 'hi'
    end
  end

  def dealloc
    puts "deallocating One"
    super
  end

end

class Two

  def initialize(&cb)
    # @cb = cb.weak!
    @cb = cb
  end

  def callcb
    @cb.call
  end

  def dealloc
    puts "deallocating Two"
    super
  end

end

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    rootViewController = UIViewController.alloc.init
    rootViewController.title = 'retain-test'
    rootViewController.view.backgroundColor = UIColor.whiteColor

    navigationController = UINavigationController.alloc.initWithRootViewController(rootViewController)

    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = navigationController
    @window.makeKeyAndVisible

    one = One.new

    one.two.callcb

    true
  end
end
amirrajan commented 6 years ago

It’s not a bug. It’s how it should work given the implementation (as far as I understand) doesn’t resolve circular references (yet).

I renamed the code so it should be clearer what’s happening..

class Parent
  def dealloc
    puts "dealloc: #{self}"
    super
  end

  def initialize
    @two = Child.new do
      # do callback stuff
    end
  end
end

class Child
  def dealloc
    puts "dealloc: #{self}"
    super
  end

  def initialize(&callback)
    @callback = callback
    #@callback = callback.weak!
  end
end
puts "here 1"
@one = Parent.new
puts "here 2"
@one = nil
puts "here 3"

The block retains self — unless weak! is invoked on it, like most of said. So Parent retains Child and Child retains Child. Typically, you want to avoid that. The easiest way in this case is to always call weak! on the block and if the block has explicit references to another object that you don’t want to retain (and hence end up with a cyclic reference), you want to create it outside the block: weak_obj = WeakRef.new(obj) too and then use weak_obj instead of obj inside that block. But this means the WeakRef may not always be valid. And in those cases, you’d have to check that it is before you use it. In addition, if you pass the WeakRef somewhere else, you sometimes need to pass weak_obj.self instead of weak_obj if you ultimately want to store a strong reference to that original object.

I think that’s about it for cyclic references/weak references in blocks. Remember them and it should be ok. Otherwise, it’s very very easy to mess this up.

When I do, I add dealloc implementations and click around to make sure things I want to be released get released as expected…

Probably useful to use this if you like to keep block references (I keep a lot of them): https://github.com/hboon/purplish-accessors/blob/master/lib/purplish-accessors/class.rb#L33-L43 GitHub hboon/purplish-accessors purplish-accessors - Add variations of attr_accessor for RubyMotion for iOS & macOS

This:

  def block_attr_accessor(*my_accessors)
    my_accessors.each do |accessor|
      define_method(accessor) do
        instance_variable_get("@#{accessor}")
      end

      define_method("#{accessor}=") do |accessor_value|
        instance_variable_set("@#{accessor}", accessor_value ? accessor_value.weak! : nil)
      end
    end
  end
hboon commented 6 years ago

Ah. Made a typo. Should be: “So Parent retains Child and Child retains Parent too”.

amirrajan commented 6 years ago

@hboon gave you collaborator access.