How do you ensure that an app built for an earlier release of Windows Phone continues to work great on Windows Phone 8?
Whether you participate in the SDK preview program Todd Brix talked about today or not, here are some tips for future proofing your apps and readying them for the next release of Windows Phone. There are three aspects to this issue that I’ll touch on:
- Compatibility: Even though almost everything is different, our app platform maintains a high degree of backward compatibility. So, from the perspective of the platform APIs, you generally don’t have to do anything – your app should just continue to work, without any changes. Where this becomes a little less cut-and-dried is if you’re doing anything unusual or unsupported in your app (more below).
- Version resilience: You can’t predict the future, so the whole notion of “future proofing” is a little misleading. However, once the future has arrived, you still need to deal with it: how do you make sure your app runs correctly on multiple versions of the platform which support growing sets of features?
- Hardware resilience: Third, Windows Phone 8 also introduces support for a wider range of physical devices. This means that there are more optional hardware features, and a wider range of capabilities within the non-optional features. A smart developer will accommodate this by building flexibility into his app, to handle the conditions where some feature is not available, or is less capable.
API changes
To get a feel for how APIs might change between versions of the platform, consider the changes from 7.0 to 7.1. These are documented here. Most changes were additive – that is, new APIs were added, so these wouldn’t have any breaking impact on existing apps. The few changes that weren’t additive generally fall into the category of ‘improved consistency with the API surface overall’. That is, anomalies were fixed – and these were almost all edge cases anyway.
You can expect more of the same in the future: the existing API surface will be brought forward into the Windows Phone 8 world, inconsistencies will be brought into alignment, and it will be massively expanded to support the many new features on the new platform.
Version resilience
How do you cope with version differences? How do you even discover version differences? One technique for dynamic API discovery is runtime reflection. This is a problematic technique: it can be used to increase version resilience, and it can be used to bypass the public API surface (thereby reducing version resilience).
In general, the use of reflection is discouraged in Windows Phone apps. The public surface of the app platform has been very carefully designed to provide your app with the maximum functionality in a cohesive manner. If you bypass this carefully-constructed surface via reflection, you can easily stray into areas which are not supported or which may change underneath you in later versions. For example, it is possible to use reflection to access non-public members, and this is usually a bad idea. One case where reflection might be useful is for the ‘light-up’ scenario, where you check to see you’re running on a version of the platform that has some feature that you’re interested in.
For example, in 7.0, the SystemTray class exposed only one property: IsVisible. 7.1 added four new properties. So, you might query for say the BackgroundColor, and if you’re running on a version of the platform that supports this, you can get (and/or set) its value. This is a reasonable use of reflection for positive version-resilience – because it is restricted to documented public members, eg:
Type t = typeof(SystemTray); PropertyInfo pi = t.GetProperty("BackgroundColor", BindingFlags.Public | BindingFlags.Static); if (pi != null) { pi.SetValue(null, Colors.Red, null); }
Obfuscation
To help protect IP, you might consider obfuscating your code, for example with the freely downloadable Dotfuscator Windows Phone Edition from PreEmptive. The only catch with this is that the Silverlight framework makes extensive use of reflection behind the scenes – and this makes it difficult for an obfuscator to analyze your assemblies correctly to figure out what level of obfuscation is safe.
Too much obfuscation may result in runtime errors. Just because your obfuscated code works on one version of the platform doesn’t mean the same obfuscated code will work the same way on another version. For version-resilience, therefore, you should choose obfuscation options conservatively. Specifically, you should generally avoid the code optimization features of obfuscators that eliminate unused code and data, coalesce strings, merge assemblies, and so on.
Architectural decoupling
A common version resilience technique is to optimize the decoupling between components in your app. This doesn’t make an individual app version resilient – rather, it facilitates building multiple versions of your app for variant platforms, where you simply swap out different versions of particular components/assemblies per variation.
For different versions of Windows Phone, this is largely unnecessary – recall that the app platform itself acts as a version-resiliency wrapper over different underlying platform specifics. We’ve done the hard work for you! Where this approach will become more useful going forward is in the scenario where you want to target both Windows Phone 8 and Windows 8 with the same app. However, that’s getting a little ahead of myself – and I’ll talk more about this when the Windows Phone 8 SDK is released.
Platform variation
The first release of Windows Phone had a very tightly-restricted hardware specification. This meant that developers had the luxury of being able to target one single platform. As the market for Windows Phone has expanded, so has the range of hardware configurations that these new markets demand.
Increasingly, therefore, the set of hardware characteristics is more fluid – more capabilities are becoming optional. This applies already in 7.1 to features such as the compass, accelerometer, gyroscope and camera, as documented here. A smart developer already knows to build conditional code that tests for the existence or configuration of particular hardware features, via the IsSupported or IsXXXSupported properties, eg:
StringBuilder builder = new StringBuilder(); if (Gyroscope.IsSupported) builder.AppendLine("gyroscope"); if (Compass.IsSupported) builder.AppendLine("compass"); if (Camera.IsCameraTypeSupported(CameraType.Primary)) builder.AppendLine("primary camera"); if (Camera.IsCameraTypeSupported(CameraType.FrontFacing)) builder.AppendLine("front-facing camera"); MessageBox.Show(builder.ToString());
You can do the same thing with other capabilities that the user might enable conditionally, such as network connectivity or data roaming, eg:
if (DeviceNetworkInformation.IsCellularDataEnabled) builder.AppendLine("cellular data enabled"); if (DeviceNetworkInformation.IsCellularDataRoamingEnabled) builder.AppendLine("cellular data roaming enabled");
Memory variation
Clause 5.2.5 of the technical certification requirements warns you that “An application must not exceed 90 MB of RAM usage, except on devices that have more than 256 MB of memory.” Note that this doesn’t say how much memory you can use on a device that has more than 256 MB of memory. Effectively, you’re guaranteed to have 90 MB of memory, and you might get more on some devices – and you might not.
The bottom line is that you should do all you can to constrain your app to run within 90 MB of memory. If you do, it will have the widest possible target market (that is, all Windows Phone devices in the known universe). If you don’t, then you’re choosing to exclude your application from some part of that market. Even if you don’t exclude your app explicitly, it may perform so badly on low-memory devices that the user will uninstall it and might give you a bad review.
So, while the 90 MB target is seen by some as merely a guideline, any such guidelines are likely to become more critical as the range of device hardware opens up. In the specific case of low memory, you can go to the extreme of explicitly opting out your application, so that it does not get installed on a low-memory device. This is documented here.
If you don’t want to restrict your market, you can instead tailor your functionality to allow for reduced memory. Here you can use the TryGetValue technique to see if the DeviceExtendedProperties.ApplicationWorkingSetLimit is available. TryGetValue is defined in the IDictionary interface, and you would normally use it in open-ended collection types such as IsolatedStorageSettings or NavigationContext.QueryString. However, this technique has been highjacked in the DeviceExtendedProperties class specifically to support additional version-dependent features. ApplicationWorkingSetLimit will tell you whether you’re running on 7.1 or 7.1.1. If the value returned is less than 90 MB, you know you’re running on a low-memory device, and can tailor your functionality accordingly.
If ApplicationWorkingSetLimit is not found, then you’re not running on 7.1.1, which means there is no system paging, which in turn means that the app’s working set limit is the same as its commit limit. You can get the commit limit from the DeviceStatus.ApplicationMemoryUsageLimit property.
long ninetyMb = 90 * 1024 * 1024; long workingSetLimit; object ws; if (DeviceExtendedProperties.TryGetValue("ApplicationWorkingSetLimit", out ws)) { workingSetLimit = Convert.ToInt64(ws); } else { workingSetLimit = DeviceStatus.ApplicationMemoryUsageLimit; } if (workingSetLimit < ninetyMb) { MessageBox.Show("Low-memory device: functionality is reduced."); }
More details on this technique are here. The ApplicationMemoryUsageLimit property on the DeviceStatus class tells you the maximum amount of memory that your application can allocate. Subtracting ApplicationCurrentMemoryUsage from ApplicationMemoryUsageLimit tells you how much more space you have to grow. So, to build resiliency in a world where some devices have more memory than others, you can make dynamic checks, and tailor your behavior accordingly – just as you do with optional hardware.
In summary: consider light-up scenarios, don’t use reflection recklessly, be conservative with obfuscation, test for optional hardware and user-enabled features, and pay close attention to memory guidelines.