[HN Gopher] NSCopyObject, the griefer that keeps on griefing
___________________________________________________________________
NSCopyObject, the griefer that keeps on griefing
Author : chmaynard
Score : 104 points
Date : 2024-07-16 13:34 UTC (1 days ago)
(HTM) web link (wadetregaskis.com)
(TXT) w3m dump (wadetregaskis.com)
| JKCalhoun wrote:
| > And yet, Apple still use NSCopyObject themselves to this very
| day
|
| On one hand, I'm not surprised. Change at that low a level is
| danger. The most risk-averse path is probably what Apple did:
| leave the classes calling NSCopyObject() alone, and really really
| warn developers not to use it (and be sure not to introduce any
| _new_ Apple classes that call it). (The author raises the obvious
| problem though with subclassing ... would be embarrassing for
| Apple to also tell devs to stop subclassing NSCell)
|
| I think with how dynamic the OS has shown itself to be, we
| generally hope that problems like this just "go away". Apple is
| probably hoping people, decades on now, are no longer subclassing
| NSCell -- have moved off of NSAnimation....
|
| Interesting read. I always like to learn how things work under
| the hood.
|
| It's a funny thing that when you are introduced to a new language
| or framework -- like ObjectiveC and Cocoa were to me, it feels
| like a thing that works in a magical way. And initially you treat
| it like the black box it appears to be. You adhere to things you
| are told like, _call AutoRelease() before returning an object if
| you expect the caller to retain_ or whatever - not really knowing
| what all that means. Only later finding out that the "magic" of
| retain/release is just a count that is incremented and
| decremented.
|
| In time the veneer of magic fades and you remind yourself that
| the thing was written in C after all.
| jwells89 wrote:
| I doubt there's too many people subclassing NSCell directly
| these days, but unless I'm missing something (entirely
| possible), it's difficult to avoid subclassing NSButtonCell in
| a lot of cases where you want a stock yet lightly customized
| control. That's where I'd expect to see the bulk of third party
| NSCell subclassing.
| chuckadams wrote:
| > Only later finding out that the "magic" of retain/release is
| just a count that is incremented and decremented.
|
| Pretty sure most devs know that's what reference counting
| ultimately boils down to (it's right in the name after all),
| but many papers over the decades testify to the fact that
| maintaining that counter in a way that's both fast and reliable
| turns out to be surprisingly tricky.
| isodev wrote:
| > Ironically, Swift's attempts to prevent incorrect code actually
| make it harder to write correct code
|
| Yes, and strict concurrency deserves a post on itself. I wonder
| how NSCopyObject works with actor isolation?
| favorited wrote:
| It's been deprecated since 2012, so it probably shouldn't be
| called from APIs introduced 3 years ago anyway.
| isodev wrote:
| I think probably Xcode should say something if one tries to.
| (concurrency checking is brand new in Swift 6).
|
| Like the post says, it's not always clear if you're working
| with a class which inherits from NSCopyObject, so how does
| one "break out" of strict concurrency to copy an object like
| this?
| galad87 wrote:
| The articles says it's used mostly in old UI components
| like NSCell, that can only be used on the main thread.
| NSImage & NSImageRep have questionable thread safety, and I
| don't know how often NSAnimation is copied on a non main
| thread.
| isodev wrote:
| And is SwiftUI's @MainActor annotation "main thread"
| enough for this? e.g. if a View with @MainActor
| annotation renders a child which is a
| NSCell/NSImage/NSImageRep/NSAnimation wrapped in a
| representable - all is good? I don't know why Apple keeps
| adding things to Swift without removing the old things.
| pjmlp wrote:
| This is a good overview of how even with ARC Objective-C's
| automatic memory management is rather fragile, and how the
| tracing GC initial approach was an herculean attempt to make it
| work right without crashes and memory corruption.
| umanwizard wrote:
| From my time at then-Facebook: ARC is a huge pain at scale
| (hundreds of developers contributing to the same codebase)
| because anyone anywhere can cause a retain cycle.
|
| E.g. imagine if you open some news feed story, then go back to
| news feed so the view controller you opened is lost, but the
| person who implemented that page was an android dev who
| recently switched to iOS and doesn't know about weak pointers,
| and had something deep in the view hierarchy retaining a
| pointer to the root... now everything associated with that page
| is leaked, which can include megabytes of decoded images or
| worse, videos...
|
| The issue was compounded by the fact that, at least at the
| time, their interview process primarily rewarded regurgitating
| memorized answers to leetcode problems as quickly as possible.
| refulgentis wrote:
| Random color commentary you might appreciate: I grew up on
| iOS for several years, then went to Google, and after a
| couple years migrated to working on Android's Springboard
| equivalent.
|
| One of the odder things to me about Android is this could
| happen too, with what felt like ~same rate as iOS. There
| wasn't nearly as good tooling as Instruments, instead it was
| ~always caught by testing but it was so much more work:
| testing was done on a batch of commits, so if there was a
| regression, there needed to be an additional layer of
| bisecting + test engineer in the loop to let you know it
| happened.
|
| If someone does Android regularly and sees this, I'm very
| curious what you use for detecting cycles: my iOS habit was
| to, every month or so, and with each release, sit down with
| this tool called "Instruments" that displayed live objects
| and run through the app, and see if there was any unexpected
| objects representing screens / core workflows piling up.
|
| I have a feeling this is totally possible on Android (maybe
| that Square leak canary library?), and it was more manually
| processed at Google because there was no way to enforce SWEs
| actually did it, and we couldn't(?)/didn't put the Square
| library in the OS.
| umanwizard wrote:
| Doesn't Android have a tracing garbage collector? How do
| retain cycles cause leaks?
| refulgentis wrote:
| _Really_ handwaving here, tl;dr in a pure cycle scenario
| its resolved but it seemed it didn't help much compared
| to the number of footguns available. ex. felt like on iOS
| you'd cover 90% of cases by just "just do [weak self] in
| every dispatch_async"
|
| In Android you have to remember that, and there's this
| god-object called Context that you use to retrieve
| strings, images, and layouts (~XIBs). Each Activity
| (ViewController) has one and its incredibly easy to
| accidentally retain it, and thus the whole screen.
|
| The standard way of handling button presses / whatever
| IBActions are now also would cause it, and these heavily
| used things called BroadcastReceivers would usually be
| spun up when an Activity was started, and instantly form
| a cycle with the Activity that for some reason can't be
| noticed easily.
|
| Sorry for the handwaving, it's been very strange spending
| so much calendar time working on something I had to
| understand a lot less. (tl;dr: solo founder that shipped
| a point of sale system to 2,000 restaurants starting on
| iPad 1.0, had to get to 0 leaks all by myself. versus the
| warm safe blanket of bigco).
|
| This Medium article is an excellent overview I used to
| remind myself, though the AI art is incredibly shitty,
| enough to make me wonder if the article is plagirized.
| https://medium.com/@naeem0313/top-10-android-memory-leak-
| cau...
| umanwizard wrote:
| Makes sense!
| thought_alarm wrote:
| Historical fun fact:
|
| In the original version of Objective-C and NextStep (1988-1994),
| the common base class (Object) provided an implementation of
| `copyFromZone:` that did an exact memcpy of the object, a la
| NSCopyObject. In other words, NSCopyObject was the default
| behavior for all Obj-C objects.
|
| It was still up to each subclass to ensure that copyFromZone:
| worked correctly with its own data (not all classes supported
| it).
|
| AppKit's `Cell` class provided this implementation:
| - copyFromZone:(NXZone *)zone { Cell *retval;
| retval = [super copyFromZone:zone]; if
| (cFlags1.freeText && contents) retval->contents
| = NXCopyStringBufferFromZone(contents, zone); return
| retval; }
|
| Here it needs to make a copy of its `contents` string, using
| NXCopyStringBufferFromZone, when the copy of Cell is expecting to
| free that memory (cFlags1.freeText).
|
| OpenStep introduced reference counting and the NSCopying
| protocol, and removed the `copyWithZone:` implementation in
| NSObject.
|
| So the equivalent implementation in OpenStep's NSCell class could
| be: - (id)copyWithZone:(NSZone *)zone {
| NSCell *retval; retval = NSCopyObject(self, 0, zone);
| [retval->contents retain]; return retval; }
| ChrisMarshallNY wrote:
| I just avoid using the copy(_:) method. I feel that it's one of
| those "code smell" flags. If I find myself contemplating using
| it, I probably need to revisit my design. This is the kind of
| stuff that does not age well.
|
| Those of us "of a certain age," may remember the pre-MacOSX
| CopyBits[0] function.
|
| That was fun. :P
|
| [0]
| http://preserve.mactech.com/articles/develop/issue_06/Othmer...
| klodolph wrote:
| Lots of fuss to make sure you stay on the CopyBits fast path...
| no masking, rectangular regions, no format conversions, color
| table has identical ctSeed, and you can even get a pointer to a
| lower-level, specialized version of CopyBits if you need.
| meindnoch wrote:
| If you have objects storing NS(String|Array|Dictionary)s, you
| ought to copy them before storing, otherwise you might
| inadvertently end up storing an
| NSMutable(String|Array|Dictionary), which can get updated
| behind your back, producing hard to debug issues.
| KerrAvon wrote:
| `copy` alone is not a red flag. There are definitely cases
| where you need to use it for safety -- mutable pair types like
| property list types, for example. But you can't trust an object
| like -- for example -- NSWindow to do something sane with it
| unless the behavior is explicitly documented.
| ChrisMarshallNY wrote:
| Well, most of my work, these days, is in UIKit, so I'm fairly
| used to "let" not meaning much.
|
| I also tend to rewrite the code completion implicit unwraps
| (!) with explicit ones (?). Forces me to acknowledge that I'm
| skating on thin ice.
|
| If there's something that I need to keep around, I make a
| local let copy of just that property, as opposed to the whole
| shooting match that contains it, or I look up the value in a
| JiT manner. I tend to have a lot of computed properties. I'll
| often extend base classes and structs, to add computed
| accessors.
| xenadu02 wrote:
| As with many problems this can be handled by a layer of
| indirection.
|
| Have your subclass push all its data into a storage object that
| itself is a simple ObjC type held in a standard ARC-managed
| property/ivar. Then the NSCopyObject memcpy copies the pointer to
| your storage and the ARC fixup ensures the retain count of the
| storage object is correct.
|
| This is the simplest way to resolve the problem for something
| like NSCell without anyone making future code changes needing to
| think about it too hard or accidentally introducing a regression.
| nox101 wrote:
| The issue that stuck out to me is the fact that top hits of "how
| to do X" on the web, often lead to bad solutions, bad practices,
| outdated info. And, they keep spreading forever. Someone finds
| the bad solution, copies it into their code, someone else
| references their code, copies the bad solution again.
|
| I have no solutions to fix this but I sometimes see it happen
| where someone asks a question on S.O. or Reddit. Someone posts a
| bad-practice solution that happens to work. It's marked as the
| answer, upvoted, and now it spreads like an infection.
| j1elo wrote:
| They need to filter out possible malicious actors so a downvote
| alone wouldn't cause the question to be disregarded... but
| still, whenever you see that, cast your vote!
|
| Also, a comment is the most powerful tool you have. I've
| benefited multiple times from kind souls that added a comment
| in an incorrect answer, to the tone of "as of 2024 this is a
| bad solution and it's much better to do as told here
| [link...]". So try to add a comment too if you spot bad
| answers, please :-)
| asveikau wrote:
| Sometimes I even notice when you go into comment sections
| trying to say that certain techniques are not best practices,
| people call you elitist, say you're making nothing more than
| appeals to authority, and say there's nothing wrong with
| (whatever equivalent of) NSCopyObject, so get off your high
| horse.
| Dunati wrote:
| And now we have AI coding assistants training on that code as
| well
| BobaFloutist wrote:
| This would be less of an issue if official documentation was
| less universally terrible.
| linksnapzz wrote:
| If only the software in question had been written by a company
| worth nearly four trillion dollars, which would allow them to
| spend some money on comprehensive documentation that was kept
| up to date.
| JohnMakin wrote:
| It's good for a career, though, especially if you enjoy
| cleaning up messes
| tempodox wrote:
| > If your superclass uses NSCopyObject, it's now your problem
| just as much as if you'd used NSCopyObject directly, whether you
| like it or not.
|
| One of the more insidious drawbacks of OOP: The tight coupling
| introduced by inheritance.
| derefr wrote:
| AFAIK the pure platonic ideal of OOP (i.e. the thing that
| Smalltalk and Self do) makes no mention of subclassing --
| that's bolted-on brain-damage that's become conflated with OOP.
|
| The way a "real" OOP language would do subclassing, would just
| be composition: wrapping an instance of the "parent" and
| proxying messages up to it.
|
| If such a language wanted to "support inheritance in the
| language" (i.e. put syntax sugar around that wrapping), the
| result would probably look something like Golang's anonymous
| embedded struct fields -- where embedding a "parent" object of
| type X, gives your wrapping object Y default proxy methods to
| call the X instance's methods of the same name; and default
| proxy getters/setters for the X instance's fields; both of
| which you can override as you please / are implicitly shadowed
| by methods+fields of the same name declared on the wrapper
| type.
| wahern wrote:
| Smalltalk and Self use prototyping for inheritance, like
| JavaScript and Lua. It's a dynamically typed version of
| subclassing, where your parent is itself a live object, not
| just a definition. I suppose it's proxy-like, except
| forwarding of messages/method invocations is automatic and
| part of the language semantics, and the context object (self)
| is the one used in the caller's invocation.
|
| I've not used Objective C much, but I believe the core of
| Objective C works the same--non-overridden methods (message
| handlers) of an instantiated object are invoked using the
| parent prototype's instance definition (which I believe in
| Objective C is somewhat confusingly called a class object?).
| But it also supports so-called protocols, aka interfaces in
| other languages, which because it's a more familiar paradigm
| seems to be how most people associate OOP with Objective C.
| txstrt wrote:
| Txstrt
| olliej wrote:
| > And yet, Apple still use NSCopyObject themselves to this very
| day
|
| Of course they do. The existing behavior involves NSCopyObject
| (for decades at this point) and user code written against those
| existing APIs, therefore depends on that behavior. Even if
| removing the NSCopyObject usage would make things "better" in
| principle, doing so would likely break existing code so it simply
| isn't an option.
|
| Hence it's deprecated, and all the documentation says "don't use
| this", but the system still does, not because Apple is being
| hypocritical, but because it has to support existing code. That's
| just the reality of providing ABI stability on any OS: sometimes
| you have to do distasteful things - an example I'm acutely
| familiar with is with JSC's API on 32bit platforms: the internal
| representation of js values is 64bit, but the API is 32bit so
| sometimes accessing a property can result in GC allocation of an
| object to wrap immediate values.
___________________________________________________________________
(page generated 2024-07-17 23:07 UTC)