├── .gitignore ├── LICENSE ├── README.md ├── WeakHandler.png ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mvn-local-support.gradle └── src ├── androidTest └── java │ └── com │ └── badoo │ └── mobile │ └── util │ ├── WeakHandlerChainedRefTest.java │ └── WeakHandlerTest.java └── main ├── AndroidManifest.xml └── java └── com └── badoo └── mobile └── util └── WeakHandler.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Idea 26 | .idea 27 | *.iml 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Badoo Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android Weak Handler 2 | ==================== 3 | 4 | Memory safer implementation of android.os.Handler 5 | 6 | Problem 7 | ------- 8 | 9 | Original implementation of Handler always keeps hard reference to handler in queue of execution. 10 | Any object in Message or Runnable posted to `android.os.Handler` will be hard referenced for some time. 11 | If you create anonymous Runnable and call to `postDelayed` with large timeout, that Runnable will be held 12 | in memory until timeout passes. Even if your Runnable seems small, it indirectly references owner class, 13 | which is usually something as big as Activity or Fragment. 14 | 15 | You can read more [on our blog post.](http://techblog.badoo.com/blog/2014/08/28/android-handler-memory-leaks) 16 | 17 | Solution 18 | -------- 19 | 20 | `WeakHandler` is trickier then `android.os.Handler` , it will keep `WeakReferences` to runnables and messages, 21 | and GC could collect them once `WeakHandler` instance is not referenced any more. 22 | 23 | ![Screenshot](WeakHandler.png) 24 | 25 | Usage 26 | ----- 27 | Add reference to your build.gradle: 28 | ```groovy 29 | repositories { 30 | maven { 31 | repositories { 32 | url 'https://oss.sonatype.org/content/repositories/releases/' 33 | } 34 | } 35 | } 36 | 37 | dependencies { 38 | compile 'com.badoo.mobile:android-weak-handler:1.1' 39 | } 40 | ``` 41 | 42 | Use WeakHandler as you normally would use Handler 43 | 44 | ```java 45 | import com.badoo.mobile.util.WeakHandler; 46 | 47 | public class ExampleActivity extends Activity { 48 | 49 | private WeakHandler mHandler; // We still need at least one hard reference to WeakHandler 50 | 51 | protected void onCreate(Bundle savedInstanceState) { 52 | mHandler = new WeakHandler(); 53 | ... 54 | } 55 | 56 | private void onClick(View view) { 57 | mHandler.postDelayed(new Runnable() { 58 | view.setVisibility(View.INVISIBLE); 59 | }, 5000); 60 | } 61 | } 62 | ``` 63 | 64 | Credits 65 | ------- 66 | Weak Handler is brought to you by [Badoo Trading Limited](http://corp.badoo.com) and it is released under the [MIT License](http://opensource.org/licenses/MIT). 67 | 68 | Created by [Dmytro Voronkevych](https://github.com/dmitry-voronkevich) 69 | -------------------------------------------------------------------------------- /WeakHandler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lianzhan/weakHandler/83fc8392f3f3125c35ca32a8f3701feeb954b91f/WeakHandler.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Badoo Trading Limited 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | project.ext.set('VERSION_CODE', 2) 22 | project.ext.set('VERSION_NAME', '1.1') 23 | 24 | buildscript { 25 | repositories { 26 | mavenCentral() 27 | } 28 | 29 | dependencies { 30 | classpath 'com.android.tools.build:gradle:1.2.3' 31 | } 32 | } 33 | 34 | apply plugin: 'com.android.library' 35 | 36 | repositories { 37 | mavenCentral() 38 | } 39 | 40 | dependencies { 41 | compile "com.android.support:support-annotations:22.2.0" 42 | androidTestCompile 'junit:junit:4.12' 43 | androidTestCompile('com.android.support.test:runner:0.2') { 44 | exclude group: 'junit' 45 | } 46 | } 47 | 48 | 49 | android { 50 | compileSdkVersion 22 51 | buildToolsVersion "22.0.1" 52 | 53 | defaultConfig { 54 | versionCode project.VERSION_CODE 55 | versionName project.VERSION_NAME 56 | minSdkVersion 8 57 | targetSdkVersion 22 58 | 59 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 60 | } 61 | } 62 | 63 | if (project.hasProperty('badooMavenScript')) { 64 | apply from: project.badooMavenScript 65 | } else { 66 | apply from: 'mvn-local-support.gradle' 67 | } 68 | 69 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lianzhan/weakHandler/83fc8392f3f3125c35ca32a8f3701feeb954b91f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 02 19:11:40 BST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /mvn-local-support.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Badoo Trading Limited 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | apply plugin: 'maven' 22 | 23 | uploadArchives { 24 | repositories { 25 | mavenDeployer { 26 | repository(url: 'file://' + System.properties['user.home'] + '/.m2/repository') 27 | 28 | pom.project { 29 | name = "Android Weak Handler" 30 | groupId = 'com.badoo.mobile' 31 | artifactId = 'android-weak-handler' 32 | version = project.VERSION_NAME 33 | description = 'Memory safer implementation of android Handler' 34 | url = 'http://github.com/badoo/android-weak-handler' 35 | 36 | scm { 37 | connection 'scm:git:http://github.com/badoo/android-weak-handler/' 38 | developerConnection 'scm:git:http://github.com/badoo/android-weak-handler/' 39 | url 'http://github.com/badoo/android-weak-handler' 40 | } 41 | 42 | licenses { 43 | license { 44 | name 'MIT License' 45 | url 'http://opensource.org/licenses/MIT' 46 | } 47 | } 48 | 49 | developers { 50 | developer { 51 | id 'zloy' 52 | name 'Dmytro Voronkevych' 53 | email 'dmytro.voronkevych@corp.badoo.com' 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/androidTest/java/com/badoo/mobile/util/WeakHandlerChainedRefTest.java: -------------------------------------------------------------------------------- 1 | package com.badoo.mobile.util; 2 | 3 | import android.support.test.runner.AndroidJUnit4; 4 | import android.test.suitebuilder.annotation.SmallTest; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import java.util.concurrent.locks.Lock; 11 | import java.util.concurrent.locks.ReentrantLock; 12 | 13 | import static junit.framework.Assert.assertNull; 14 | import static junit.framework.Assert.assertSame; 15 | 16 | /** 17 | * Tests for {@link com.badoo.mobile.util.WeakHandler.ChainedRef} 18 | * 19 | * @author Dmytro Voronkevych 20 | */ 21 | @SuppressWarnings("ALL") 22 | @SmallTest 23 | @RunWith(AndroidJUnit4.class) 24 | public class WeakHandlerChainedRefTest { 25 | 26 | private Runnable mHeadRunnable; 27 | private Runnable mFirstRunnable; 28 | private Runnable mSecondRunnable; 29 | private Lock mLock; 30 | private WeakHandler.ChainedRef mRefHead; 31 | private WeakHandler.ChainedRef mSecond; 32 | private WeakHandler.ChainedRef mFirst; 33 | private WeakHandler.WeakRunnable mHeadWeakRunnable; 34 | private WeakHandler.WeakRunnable mFirstWeakRunnable; 35 | private WeakHandler.WeakRunnable mSecondWeakRunnable; 36 | 37 | // Creates linked list refHead <-> first <-> second 38 | @Before 39 | public void setUp() { 40 | mLock = new ReentrantLock(); 41 | 42 | mHeadRunnable = new DummyRunnable(); 43 | mFirstRunnable = new DummyRunnable(); 44 | mSecondRunnable = new DummyRunnable(); 45 | 46 | mRefHead = new WeakHandler.ChainedRef(mLock, mHeadRunnable) { 47 | @Override 48 | public String toString() { 49 | return "refHead"; 50 | } 51 | }; 52 | mFirst = new WeakHandler.ChainedRef(mLock, mFirstRunnable) { 53 | @Override 54 | public String toString() { 55 | return "second"; 56 | } 57 | }; 58 | mSecond = new WeakHandler.ChainedRef(mLock, mSecondRunnable) { 59 | @Override 60 | public String toString() { 61 | return "first"; 62 | } 63 | }; 64 | 65 | mRefHead.insertAfter(mSecond); 66 | mRefHead.insertAfter(mFirst); 67 | 68 | mHeadWeakRunnable = mRefHead.wrapper; 69 | mFirstWeakRunnable = mFirst.wrapper; 70 | mSecondWeakRunnable = mSecond.wrapper; 71 | } 72 | 73 | @Test 74 | public void insertAfter() { 75 | assertSame(mFirst, mRefHead.next); 76 | assertSame(mSecond, mRefHead.next.next); 77 | assertNull(mRefHead.next.next.next); 78 | 79 | assertNull(mRefHead.prev); 80 | assertSame(mFirst, mSecond.prev); 81 | assertSame(mRefHead, mFirst.prev); 82 | } 83 | 84 | @Test 85 | public void removeFirst() { 86 | mFirst.remove(); 87 | 88 | assertNull(mFirst.next); 89 | assertNull(mFirst.prev); 90 | 91 | assertSame(mSecond, mRefHead.next); 92 | assertNull(mSecond.next); 93 | assertSame(mRefHead, mSecond.prev); 94 | } 95 | 96 | @Test 97 | public void removeSecond() { 98 | mSecond.remove(); 99 | assertNull(mSecond.next); 100 | assertNull(mSecond.prev); 101 | 102 | assertSame(mFirst, mRefHead.next); 103 | assertSame(mRefHead, mFirst.prev); 104 | assertNull(mFirst.next); 105 | } 106 | 107 | @Test 108 | public void removeFirstByRunnable() { 109 | assertSame(mFirstWeakRunnable, mRefHead.remove(mFirstRunnable)); 110 | assertSame(mRefHead.next, mSecond); 111 | assertSame(mRefHead, mSecond.prev); 112 | assertNull(mFirst.next); 113 | assertNull(mFirst.prev); 114 | } 115 | 116 | @Test 117 | public void removeSecondByRunnable() { 118 | assertSame(mSecondWeakRunnable, mRefHead.remove(mSecondRunnable)); 119 | assertSame(mFirst, mRefHead.next); 120 | assertSame(mRefHead, mFirst.prev); 121 | assertNull(mSecond.next); 122 | assertNull(mSecond.prev); 123 | } 124 | 125 | @Test 126 | public void removeNonExistentRunnableReturnNull() { 127 | assertNull(mRefHead.remove(new DummyRunnable())); 128 | assertSame(mFirst, mRefHead.next); 129 | assertNull(mSecond.next); 130 | assertSame(mFirst, mSecond.prev); 131 | assertSame(mRefHead, mFirst.prev); 132 | } 133 | 134 | private class DummyRunnable implements Runnable { 135 | @Override 136 | public void run() { 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/androidTest/java/com/badoo/mobile/util/WeakHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Badoo Trading Limited 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | package com.badoo.mobile.util; 22 | 23 | import android.os.HandlerThread; 24 | import android.os.SystemClock; 25 | import android.support.test.runner.AndroidJUnit4; 26 | import android.test.FlakyTest; 27 | import android.test.suitebuilder.annotation.MediumTest; 28 | 29 | import com.badoo.mobile.util.WeakHandler.ChainedRef; 30 | 31 | import org.junit.After; 32 | import org.junit.Before; 33 | import org.junit.Test; 34 | import org.junit.runner.RunWith; 35 | 36 | import java.util.Collections; 37 | import java.util.HashSet; 38 | import java.util.Set; 39 | import java.util.concurrent.CountDownLatch; 40 | import java.util.concurrent.LinkedBlockingQueue; 41 | import java.util.concurrent.ThreadPoolExecutor; 42 | import java.util.concurrent.TimeUnit; 43 | import java.util.concurrent.atomic.AtomicBoolean; 44 | import java.util.concurrent.atomic.AtomicReference; 45 | 46 | import static junit.framework.Assert.assertFalse; 47 | import static junit.framework.Assert.assertTrue; 48 | 49 | /** 50 | * Unit tests for {@link com.badoo.mobile.util.WeakHandler} 51 | * 52 | * Created by Dmytro Voronkevych on 17/06/2014. 53 | */ 54 | @SuppressWarnings("ALL") 55 | @MediumTest 56 | @RunWith(AndroidJUnit4.class) 57 | public class WeakHandlerTest { 58 | 59 | private HandlerThread mThread; 60 | private WeakHandler mHandler; 61 | 62 | @Before 63 | public void setup() { 64 | mThread = new HandlerThread("test"); 65 | mThread.start(); 66 | mHandler = new WeakHandler(mThread.getLooper()); 67 | } 68 | 69 | @After 70 | public void tearDown() { 71 | mHandler.getLooper().quit(); 72 | } 73 | 74 | @FlakyTest 75 | @Test 76 | public void postDelayed() throws InterruptedException { 77 | final CountDownLatch latch = new CountDownLatch(1); 78 | 79 | long startTime = SystemClock.elapsedRealtime(); 80 | final AtomicBoolean executed = new AtomicBoolean(false); 81 | mHandler.postDelayed(new Runnable() { 82 | @Override 83 | public void run() { 84 | executed.set(true); 85 | latch.countDown(); 86 | } 87 | }, 300); 88 | 89 | latch.await(1, TimeUnit.SECONDS); 90 | assertTrue(executed.get()); 91 | 92 | long elapsedTime = SystemClock.elapsedRealtime() - startTime; 93 | assertTrue("Elapsed time should be 300, but was " + elapsedTime, elapsedTime <= 330 && elapsedTime >= 300); 94 | } 95 | 96 | @Test 97 | public void removeCallbacks() throws InterruptedException { 98 | final CountDownLatch latch = new CountDownLatch(1); 99 | 100 | long startTime = SystemClock.elapsedRealtime(); 101 | final AtomicBoolean executed = new AtomicBoolean(false); 102 | Runnable r = new Runnable() { 103 | @Override 104 | public void run() { 105 | executed.set(true); 106 | latch.countDown(); 107 | } 108 | }; 109 | mHandler.postDelayed(r, 300); 110 | mHandler.removeCallbacks(r); 111 | latch.await(1, TimeUnit.SECONDS); 112 | assertFalse(executed.get()); 113 | 114 | long elapsedTime = SystemClock.elapsedRealtime() - startTime; 115 | assertTrue(elapsedTime > 300); 116 | } 117 | 118 | @Test(timeout = 30000) 119 | public void concurrentRemoveAndExecute() throws Throwable { 120 | final int repeatCount = 100; 121 | final int numberOfRunnables = 10000; 122 | 123 | // Councurrent cases sometimes very hard to spot, so we will do it by repeating same test 1000 times 124 | // Problem was reproducing always by this test until I fixed WeakHandler 125 | for (int testNum = 0; testNum < repeatCount; ++testNum) { 126 | final AtomicReference mExceptionInThread = new AtomicReference<>(); 127 | 128 | HandlerThread thread = new HandlerThread("HandlerThread"); 129 | // Concurrent issue can occur inside HandlerThread or inside main thread 130 | // Catching both of cases 131 | thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { 132 | @Override 133 | public void uncaughtException(Thread thread, Throwable ex) { 134 | mExceptionInThread.set(ex); 135 | } 136 | }); 137 | thread.start(); 138 | 139 | WeakHandler handler = new WeakHandler(thread.getLooper()); 140 | Runnable[] runnables = new Runnable[numberOfRunnables]; 141 | for (int i = 0; i < runnables.length; ++i) { 142 | runnables[i] = new DummyRunnable(); 143 | handler.post(runnables[i]); // Many Runnables been posted 144 | } 145 | 146 | for (Runnable runnable : runnables) { 147 | handler.removeCallbacks(runnable); // All of them now quickly removed 148 | // Before I fixed impl of WeakHandler it always caused exceptions 149 | } 150 | if (mExceptionInThread.get() != null) { 151 | throw mExceptionInThread.get(); // Exception from HandlerThread. Sometimes it occured as well 152 | } 153 | thread.getLooper().quit(); 154 | } 155 | } 156 | 157 | @Test(timeout = 30000) 158 | public void concurrentAdd() throws NoSuchFieldException, IllegalAccessException, InterruptedException { 159 | ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 10, TimeUnit.SECONDS, new LinkedBlockingQueue(100)); 160 | final Set added = Collections.synchronizedSet(new HashSet()); 161 | final CountDownLatch latch = new CountDownLatch(999); 162 | // Adding 1000 Runnables from different threads 163 | mHandler.post(new SleepyRunnable(0)); 164 | for (int i = 0; i < 999; ++i) { 165 | final SleepyRunnable sleepyRunnable = new SleepyRunnable(i+1); 166 | executor.execute(new Runnable() { 167 | @Override 168 | public void run() { 169 | mHandler.post(sleepyRunnable); 170 | added.add(sleepyRunnable); 171 | latch.countDown(); 172 | } 173 | }); 174 | } 175 | 176 | // Waiting until all runnables added 177 | // Notified by #Notify1 178 | latch.await(); 179 | 180 | ChainedRef ref = mHandler.mRunnables.next; 181 | while (ref != null) { 182 | assertTrue("Must remove runnable from chained list: " + ref.runnable, added.remove(ref.runnable)); 183 | ref = ref.next; 184 | } 185 | 186 | assertTrue("All runnables should present in chain, however we still haven't found " + added, added.isEmpty()); 187 | } 188 | 189 | private class DummyRunnable implements Runnable { 190 | @Override 191 | public void run() { 192 | } 193 | } 194 | 195 | private class SleepyRunnable implements Runnable { 196 | private final int mNum; 197 | 198 | public SleepyRunnable(int num) { 199 | mNum = num; 200 | } 201 | 202 | @Override 203 | public void run() { 204 | try { 205 | Thread.sleep(1000000); 206 | } 207 | catch (Exception e) { 208 | // Ignored 209 | } 210 | } 211 | 212 | @Override 213 | public String toString() { 214 | return String.valueOf(mNum); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/badoo/mobile/util/WeakHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Badoo Trading Limited 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | * 21 | * Portions of documentation in this code are modifications based on work created and 22 | * shared by Android Open Source Project and used according to terms described in the 23 | * Apache License, Version 2.0 24 | */ 25 | package com.badoo.mobile.util; 26 | 27 | import android.os.Handler; 28 | import android.os.Looper; 29 | import android.os.Message; 30 | import android.support.annotation.NonNull; 31 | import android.support.annotation.Nullable; 32 | import android.support.annotation.VisibleForTesting; 33 | 34 | import java.lang.ref.WeakReference; 35 | import java.util.concurrent.locks.Lock; 36 | import java.util.concurrent.locks.ReentrantLock; 37 | 38 | /** 39 | * Memory safer implementation of android.os.Handler 40 | *

41 | * Original implementation of Handlers always keeps hard reference to handler in queue of execution. 42 | * If you create anonymous handler and post delayed message into it, it will keep all parent class 43 | * for that time in memory even if it could be cleaned. 44 | *

45 | * This implementation is trickier, it will keep WeakReferences to runnables and messages, 46 | * and GC could collect them once WeakHandler instance is not referenced any more 47 | *

48 | * 49 | * @see android.os.Handler 50 | * 51 | * Created by Dmytro Voronkevych on 17/06/2014. 52 | */ 53 | @SuppressWarnings("unused") 54 | public class WeakHandler { 55 | private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory 56 | private final ExecHandler mExec; 57 | private Lock mLock = new ReentrantLock(); 58 | @SuppressWarnings("ConstantConditions") 59 | @VisibleForTesting 60 | final ChainedRef mRunnables = new ChainedRef(mLock, null); 61 | 62 | /** 63 | * Default constructor associates this handler with the {@link Looper} for the 64 | * current thread. 65 | * 66 | * If this thread does not have a looper, this handler won't be able to receive messages 67 | * so an exception is thrown. 68 | */ 69 | public WeakHandler() { 70 | mCallback = null; 71 | mExec = new ExecHandler(); 72 | } 73 | 74 | /** 75 | * Constructor associates this handler with the {@link Looper} for the 76 | * current thread and takes a callback interface in which you can handle 77 | * messages. 78 | * 79 | * If this thread does not have a looper, this handler won't be able to receive messages 80 | * so an exception is thrown. 81 | * 82 | * @param callback The callback interface in which to handle messages, or null. 83 | */ 84 | public WeakHandler(@Nullable Handler.Callback callback) { 85 | mCallback = callback; // Hard referencing body 86 | mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler 87 | } 88 | 89 | /** 90 | * Use the provided {@link Looper} instead of the default one. 91 | * 92 | * @param looper The looper, must not be null. 93 | */ 94 | public WeakHandler(@NonNull Looper looper) { 95 | mCallback = null; 96 | mExec = new ExecHandler(looper); 97 | } 98 | 99 | /** 100 | * Use the provided {@link Looper} instead of the default one and take a callback 101 | * interface in which to handle messages. 102 | * 103 | * @param looper The looper, must not be null. 104 | * @param callback The callback interface in which to handle messages, or null. 105 | */ 106 | public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) { 107 | mCallback = callback; 108 | mExec = new ExecHandler(looper, new WeakReference<>(callback)); 109 | } 110 | 111 | /** 112 | * Causes the Runnable r to be added to the message queue. 113 | * The runnable will be run on the thread to which this handler is 114 | * attached. 115 | * 116 | * @param r The Runnable that will be executed. 117 | * 118 | * @return Returns true if the Runnable was successfully placed in to the 119 | * message queue. Returns false on failure, usually because the 120 | * looper processing the message queue is exiting. 121 | */ 122 | public final boolean post(@NonNull Runnable r) { 123 | return mExec.post(wrapRunnable(r)); 124 | } 125 | 126 | /** 127 | * Causes the Runnable r to be added to the message queue, to be run 128 | * at a specific time given by uptimeMillis. 129 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 130 | * The runnable will be run on the thread to which this handler is attached. 131 | * 132 | * @param r The Runnable that will be executed. 133 | * @param uptimeMillis The absolute time at which the callback should run, 134 | * using the {@link android.os.SystemClock#uptimeMillis} time-base. 135 | * 136 | * @return Returns true if the Runnable was successfully placed in to the 137 | * message queue. Returns false on failure, usually because the 138 | * looper processing the message queue is exiting. Note that a 139 | * result of true does not mean the Runnable will be processed -- if 140 | * the looper is quit before the delivery time of the message 141 | * occurs then the message will be dropped. 142 | */ 143 | public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) { 144 | return mExec.postAtTime(wrapRunnable(r), uptimeMillis); 145 | } 146 | 147 | /** 148 | * Causes the Runnable r to be added to the message queue, to be run 149 | * at a specific time given by uptimeMillis. 150 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 151 | * The runnable will be run on the thread to which this handler is attached. 152 | * 153 | * @param r The Runnable that will be executed. 154 | * @param uptimeMillis The absolute time at which the callback should run, 155 | * using the {@link android.os.SystemClock#uptimeMillis} time-base. 156 | * 157 | * @return Returns true if the Runnable was successfully placed in to the 158 | * message queue. Returns false on failure, usually because the 159 | * looper processing the message queue is exiting. Note that a 160 | * result of true does not mean the Runnable will be processed -- if 161 | * the looper is quit before the delivery time of the message 162 | * occurs then the message will be dropped. 163 | * 164 | * @see android.os.SystemClock#uptimeMillis 165 | */ 166 | public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { 167 | return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis); 168 | } 169 | 170 | /** 171 | * Causes the Runnable r to be added to the message queue, to be run 172 | * after the specified amount of time elapses. 173 | * The runnable will be run on the thread to which this handler 174 | * is attached. 175 | * 176 | * @param r The Runnable that will be executed. 177 | * @param delayMillis The delay (in milliseconds) until the Runnable 178 | * will be executed. 179 | * 180 | * @return Returns true if the Runnable was successfully placed in to the 181 | * message queue. Returns false on failure, usually because the 182 | * looper processing the message queue is exiting. Note that a 183 | * result of true does not mean the Runnable will be processed -- 184 | * if the looper is quit before the delivery time of the message 185 | * occurs then the message will be dropped. 186 | */ 187 | public final boolean postDelayed(Runnable r, long delayMillis) { 188 | return mExec.postDelayed(wrapRunnable(r), delayMillis); 189 | } 190 | 191 | /** 192 | * Posts a message to an object that implements Runnable. 193 | * Causes the Runnable r to executed on the next iteration through the 194 | * message queue. The runnable will be run on the thread to which this 195 | * handler is attached. 196 | * This method is only for use in very special circumstances -- it 197 | * can easily starve the message queue, cause ordering problems, or have 198 | * other unexpected side-effects. 199 | * 200 | * @param r The Runnable that will be executed. 201 | * 202 | * @return Returns true if the message was successfully placed in to the 203 | * message queue. Returns false on failure, usually because the 204 | * looper processing the message queue is exiting. 205 | */ 206 | public final boolean postAtFrontOfQueue(Runnable r) { 207 | return mExec.postAtFrontOfQueue(wrapRunnable(r)); 208 | } 209 | 210 | /** 211 | * Remove any pending posts of Runnable r that are in the message queue. 212 | */ 213 | public final void removeCallbacks(Runnable r) { 214 | final WeakRunnable runnable = mRunnables.remove(r); 215 | if (runnable != null) { 216 | mExec.removeCallbacks(runnable); 217 | } 218 | } 219 | 220 | /** 221 | * Remove any pending posts of Runnable r with Object 222 | * token that are in the message queue. If token is null, 223 | * all callbacks will be removed. 224 | */ 225 | public final void removeCallbacks(Runnable r, Object token) { 226 | final WeakRunnable runnable = mRunnables.remove(r); 227 | if (runnable != null) { 228 | mExec.removeCallbacks(runnable, token); 229 | } 230 | } 231 | 232 | /** 233 | * Pushes a message onto the end of the message queue after all pending messages 234 | * before the current time. It will be received in callback, 235 | * in the thread attached to this handler. 236 | * 237 | * @return Returns true if the message was successfully placed in to the 238 | * message queue. Returns false on failure, usually because the 239 | * looper processing the message queue is exiting. 240 | */ 241 | public final boolean sendMessage(Message msg) { 242 | return mExec.sendMessage(msg); 243 | } 244 | 245 | /** 246 | * Sends a Message containing only the what value. 247 | * 248 | * @return Returns true if the message was successfully placed in to the 249 | * message queue. Returns false on failure, usually because the 250 | * looper processing the message queue is exiting. 251 | */ 252 | public final boolean sendEmptyMessage(int what) { 253 | return mExec.sendEmptyMessage(what); 254 | } 255 | 256 | /** 257 | * Sends a Message containing only the what value, to be delivered 258 | * after the specified amount of time elapses. 259 | * @see #sendMessageDelayed(android.os.Message, long) 260 | * 261 | * @return Returns true if the message was successfully placed in to the 262 | * message queue. Returns false on failure, usually because the 263 | * looper processing the message queue is exiting. 264 | */ 265 | public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { 266 | return mExec.sendEmptyMessageDelayed(what, delayMillis); 267 | } 268 | 269 | /** 270 | * Sends a Message containing only the what value, to be delivered 271 | * at a specific time. 272 | * @see #sendMessageAtTime(android.os.Message, long) 273 | * 274 | * @return Returns true if the message was successfully placed in to the 275 | * message queue. Returns false on failure, usually because the 276 | * looper processing the message queue is exiting. 277 | */ 278 | public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { 279 | return mExec.sendEmptyMessageAtTime(what, uptimeMillis); 280 | } 281 | 282 | /** 283 | * Enqueue a message into the message queue after all pending messages 284 | * before (current time + delayMillis). You will receive it in 285 | * callback, in the thread attached to this handler. 286 | * 287 | * @return Returns true if the message was successfully placed in to the 288 | * message queue. Returns false on failure, usually because the 289 | * looper processing the message queue is exiting. Note that a 290 | * result of true does not mean the message will be processed -- if 291 | * the looper is quit before the delivery time of the message 292 | * occurs then the message will be dropped. 293 | */ 294 | public final boolean sendMessageDelayed(Message msg, long delayMillis) { 295 | return mExec.sendMessageDelayed(msg, delayMillis); 296 | } 297 | 298 | /** 299 | * Enqueue a message into the message queue after all pending messages 300 | * before the absolute time (in milliseconds) uptimeMillis. 301 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 302 | * You will receive it in callback, in the thread attached 303 | * to this handler. 304 | * 305 | * @param uptimeMillis The absolute time at which the message should be 306 | * delivered, using the 307 | * {@link android.os.SystemClock#uptimeMillis} time-base. 308 | * 309 | * @return Returns true if the message was successfully placed in to the 310 | * message queue. Returns false on failure, usually because the 311 | * looper processing the message queue is exiting. Note that a 312 | * result of true does not mean the message will be processed -- if 313 | * the looper is quit before the delivery time of the message 314 | * occurs then the message will be dropped. 315 | */ 316 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 317 | return mExec.sendMessageAtTime(msg, uptimeMillis); 318 | } 319 | 320 | /** 321 | * Enqueue a message at the front of the message queue, to be processed on 322 | * the next iteration of the message loop. You will receive it in 323 | * callback, in the thread attached to this handler. 324 | * This method is only for use in very special circumstances -- it 325 | * can easily starve the message queue, cause ordering problems, or have 326 | * other unexpected side-effects. 327 | * 328 | * @return Returns true if the message was successfully placed in to the 329 | * message queue. Returns false on failure, usually because the 330 | * looper processing the message queue is exiting. 331 | */ 332 | public final boolean sendMessageAtFrontOfQueue(Message msg) { 333 | return mExec.sendMessageAtFrontOfQueue(msg); 334 | } 335 | 336 | /** 337 | * Remove any pending posts of messages with code 'what' that are in the 338 | * message queue. 339 | */ 340 | public final void removeMessages(int what) { 341 | mExec.removeMessages(what); 342 | } 343 | 344 | /** 345 | * Remove any pending posts of messages with code 'what' and whose obj is 346 | * 'object' that are in the message queue. If object is null, 347 | * all messages will be removed. 348 | */ 349 | public final void removeMessages(int what, Object object) { 350 | mExec.removeMessages(what, object); 351 | } 352 | 353 | /** 354 | * Remove any pending posts of callbacks and sent messages whose 355 | * obj is token. If token is null, 356 | * all callbacks and messages will be removed. 357 | */ 358 | public final void removeCallbacksAndMessages(Object token) { 359 | mExec.removeCallbacksAndMessages(token); 360 | } 361 | 362 | /** 363 | * Check if there are any pending posts of messages with code 'what' in 364 | * the message queue. 365 | */ 366 | public final boolean hasMessages(int what) { 367 | return mExec.hasMessages(what); 368 | } 369 | 370 | /** 371 | * Check if there are any pending posts of messages with code 'what' and 372 | * whose obj is 'object' in the message queue. 373 | */ 374 | public final boolean hasMessages(int what, Object object) { 375 | return mExec.hasMessages(what, object); 376 | } 377 | 378 | public final Looper getLooper() { 379 | return mExec.getLooper(); 380 | } 381 | 382 | private WeakRunnable wrapRunnable(@NonNull Runnable r) { 383 | //noinspection ConstantConditions 384 | if (r == null) { 385 | throw new NullPointerException("Runnable can't be null"); 386 | } 387 | final ChainedRef hardRef = new ChainedRef(mLock, r); 388 | mRunnables.insertAfter(hardRef); 389 | return hardRef.wrapper; 390 | } 391 | 392 | private static class ExecHandler extends Handler { 393 | private final WeakReference mCallback; 394 | 395 | ExecHandler() { 396 | mCallback = null; 397 | } 398 | 399 | ExecHandler(WeakReference callback) { 400 | mCallback = callback; 401 | } 402 | 403 | ExecHandler(Looper looper) { 404 | super(looper); 405 | mCallback = null; 406 | } 407 | 408 | ExecHandler(Looper looper, WeakReference callback) { 409 | super(looper); 410 | mCallback = callback; 411 | } 412 | 413 | @Override 414 | public void handleMessage(@NonNull Message msg) { 415 | if (mCallback == null) { 416 | return; 417 | } 418 | final Handler.Callback callback = mCallback.get(); 419 | if (callback == null) { // Already disposed 420 | return; 421 | } 422 | callback.handleMessage(msg); 423 | } 424 | } 425 | 426 | static class WeakRunnable implements Runnable { 427 | private final WeakReference mDelegate; 428 | private final WeakReference mReference; 429 | 430 | WeakRunnable(WeakReference delegate, WeakReference reference) { 431 | mDelegate = delegate; 432 | mReference = reference; 433 | } 434 | 435 | @Override 436 | public void run() { 437 | final Runnable delegate = mDelegate.get(); 438 | final ChainedRef reference = mReference.get(); 439 | if (reference != null) { 440 | reference.remove(); 441 | } 442 | if (delegate != null) { 443 | delegate.run(); 444 | } 445 | } 446 | } 447 | 448 | static class ChainedRef { 449 | @Nullable 450 | ChainedRef next; 451 | @Nullable 452 | ChainedRef prev; 453 | @NonNull 454 | final Runnable runnable; 455 | @NonNull 456 | final WeakRunnable wrapper; 457 | 458 | @NonNull 459 | Lock lock; 460 | 461 | public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) { 462 | this.runnable = r; 463 | this.lock = lock; 464 | this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this)); 465 | } 466 | 467 | public WeakRunnable remove() { 468 | lock.lock(); 469 | try { 470 | if (prev != null) { 471 | prev.next = next; 472 | } 473 | if (next != null) { 474 | next.prev = prev; 475 | } 476 | prev = null; 477 | next = null; 478 | } finally { 479 | lock.unlock(); 480 | } 481 | return wrapper; 482 | } 483 | 484 | public void insertAfter(@NonNull ChainedRef candidate) { 485 | lock.lock(); 486 | try { 487 | if (this.next != null) { 488 | this.next.prev = candidate; 489 | } 490 | 491 | candidate.next = this.next; 492 | this.next = candidate; 493 | candidate.prev = this; 494 | } finally { 495 | lock.unlock(); 496 | } 497 | } 498 | 499 | @Nullable 500 | public WeakRunnable remove(Runnable obj) { 501 | lock.lock(); 502 | try { 503 | ChainedRef curr = this.next; // Skipping head 504 | while (curr != null) { 505 | if (curr.runnable == obj) { // We do comparison exactly how Handler does inside 506 | return curr.remove(); 507 | } 508 | curr = curr.next; 509 | } 510 | } finally { 511 | lock.unlock(); 512 | } 513 | return null; 514 | } 515 | } 516 | } 517 | --------------------------------------------------------------------------------