西藏游记

西藏,新疆,内蒙是国内我最想去的三个地方,它们分别对应着雪山,沙漠和草原,这次凑了7天假,连着两个周末,总共十一天,和朋友一起去西藏逛了一圈。

由于只有十一天的时间,不算长,所以没有选择坐火车进藏,而是先从南京飞到重庆,再到重庆转机,飞到拉萨,目前国内能直飞拉萨的好像只有成都,重庆,西宁,西安了。进拉萨的机票不算贵,南京到重庆 300 多,重庆到拉萨 600 多,总共才 1000 多一点。但是有个很坑的地方是飞出拉萨的机票特别贵,回去时买的拉萨飞西安的就差不多 1700 。所以如果有时间的话,最好还是飞机进藏,火车出藏,这样既能欣赏到火车穿出青藏高原的沿路美景,还比较省钱。<!--more-->

Day1 -- 重庆

南京飞到重庆,早上差点没赶上飞机,幸好滴滴师傅给力,上了高速全程 120km/h 往机场赶,到了机场安检的时候已经有广播在喊我们的名字了,有惊无险。到了重庆,打车去市区的路上,一直跟司机师傅交流,问他哪好吃好玩的,中午就吃了他推荐的一家叫周师兄的火锅,他推荐的还有来凤鱼,璧山兔,不过已经没有胃口吃了。下午逛了李子坝,皇冠梯,凯旋梯,长江索道,晚上逛了洪崖洞,吃了串串,就不重点介绍这些了,因为此行的目的是西藏。

Day2 --拉萨

早上尝了碗重庆小面,就出发去重庆机场,飞往拉萨的贡嘎机场,值得一提的是,飞机上一定要选个靠窗的座位,因为从飞机上俯瞰青藏高原的景色是在太壮观了。

到了贡嘎机场,这里海拔 3600,一下飞机我身体上并没啥高原反应,朋友除了太阳穴有点疼,其他都还好。然后我们坐机场大巴到拉萨市中心,大约 70 分钟,车票 30 元。一下大巴,侧面就看到了布达拉宫,有点激动,拎着箱子跑一圈想到正面看看布达拉宫,没跑几步,就明显感到吃力,喘不过气,这大概就是高原反应吧。然后就放弃了,想着明天可以过来好好看看,不急这么一会。便打了车,去了之前定好的客栈,客栈环境还挺不错,200 左右一晚,老板,老板娘人都很好,后面的西藏环行也是包他的车去的。在客栈跟老板娘聊天聊了很长时间,了解了当地的一些情况。

晚上去老板娘推荐的一家藏餐厅,吃了藏面,藏鸡蛋,牛肉饼,甜茶。晚上去看了当地的一个著名的演出:文成公主实景剧场。当时买的票价 260,坐在不同区域价格是不一样的,有比这更贵的,也有更便宜的。个人感觉演出还不错的,主要介绍的是松赞干布迎娶文成公主,从这里以及之后游览的布达拉宫和大昭寺可以看出藏王松赞干布在当地藏族人民的声望还是很高的,是他统一了西藏的各个部落,有点像中国秦始皇的感觉吧。

Day3 -- 布达拉宫,大昭寺

早上睡到自然醒,就去了八廓街,是拉萨著名的转经道和商业中心, 感觉没啥好看的,中午吃了家很有名的网红藏餐厅,叫央卿仓藏餐厅,点了个藏牦牛火锅,特色烤蘑菇,以及酥油茶,总共 240, 味道一般,没有想象中的好吃,主要是吃不惯藏菜的口味, 其实在西藏,更多的餐厅是川菜,因为当地有很多四川人,从川菜和藏菜在全国的普及度也可以看出来,全国人民更爱吃川菜啦。吃完后,由于布达拉宫预约的是下午 2 点,所以,我们先去了布达拉宫广场上各种摆拍布达拉宫,毕竟是标志性建筑,50 块钱人民币上印的就是它。

到了下午 2 点,开始进入布达拉宫,门票200,爬上布达拉宫是真的累,因为刚来西藏,还没完全适应这高原,基本上爬两步就休息一会。到了布达拉宫里面,里面就是各种佛像,想请的导游,因为没有导游讲解真的是啥也看不懂,但是由于疫情,里面已经没导游了 ,然后我们就跟着别的旅游团的导游,蹭着听,学习到了不少藏传佛教的知识,比如藏传佛教的两大领袖: 达赖喇嘛班禅额尔德尼 的灵童转世这一奇特的习俗。

出了布达拉宫,去大昭寺与小红书上认识的两个妹子碰面,然后一起逛了大昭寺,后来也是跟她们一起包车游的西藏。大昭寺里也就是一些佛像,我们也请了导游,由于之前在布达拉宫也听过了导游讲的藏传佛教,所以再听感觉也没啥意思,没逛多久就出来了 。大昭寺门票好像是 100,个人感觉进了布达拉宫就没必要去大昭寺了,尤其是不信佛的人。晚上去吃了家尼泊尔餐厅,叫娜玛瑟德餐厅,还不错,尼泊尔餐厅在拉萨也挺有名的。

Day4 -- 巴松错

从今天开始,客栈老板开着他的车带我们环行西藏,环行路线是我们从别的领队那复制过来的,后来客栈老板分析这路线其实是不太合理的,这就导致我们后来在路上的时间太长了,玩的景点就变少了。

从拉萨出发,向东往林芝方向开,因为林芝海拔比较低,只有 2000 多米,一般来西藏会先去林芝适应下高原,再慢慢的去高海拔地区地区玩。林芝在西藏属于开发的比较完善的旅游区,所以路都修的比较好,车子很好开。

第一站,我们先来到了巴松错,距离拉萨 360 公里,门票 170。作为来到西藏看的第一个自然景观,还是很新奇的,用一句话描述这里就是:青山,绿水,蓝天,白云皆汇与此。还是第一次见到如此碧绿的湖面,山上盘卧的白云,感觉在山上伸手就能摸到,因为在西藏的云都压的很低,可能跟高海拔有关。

其实来西藏,看的最多的就是各种''错''了,错在藏语里的意思就是湖。这里有的错是绿色的,比如这里的巴松错,还有后面看到的色林错,有的错是蓝色的,比如羊卓雍措,纳木错。据说这和湖里的矿物质元素以及光的反射有关。

逛完巴松错,已经下午5点多了,我们启程去林芝市的酒店住下,晚上吃了当地有名的石锅鸡藏香猪,这一天就 over 了。

Day5 -- 雅鲁藏布大峡谷

这天早上在酒店吃完早饭,上车准备出发的时候,酒店老板拿着几块白色围巾一样的东西走过来,这时司机示意我们把车门打开,说这是敬献哈达,是藏族的一种祝福礼仪,第一次切实的感受到热情的藏族人民。

车子开了两小时左右,就到了雅鲁藏布大峡谷景区,门票 240,峡谷中穿梭奔腾的河流是雅鲁藏布江,雅鲁藏布大峡谷是世界上最大的峡谷,从雅鲁藏布大峡谷观景台可以看到中国最美山峰--南迦巴瓦峰。因此景区内有了“到世界最大峡谷,观中国最美山峰”的标语。南迦巴瓦在藏语里的意思是“直刺天空的长矛”,南迦巴瓦峰海拔 7782 米,世界第 28 高峰,属于喜马拉雅山脉,位于喜马拉雅山脉最东端,因南迦巴瓦峰十次九不遇,常年被云雾缭绕,全年只有 40 天左右能看清它的全貌,因此又被称为“羞女峰”。所以,很正常,这次我们来看到的南迦巴瓦峰也是被云挡住了一部分。

从雅鲁藏布大峡谷出来,开车两小时,来到了鲁朗小镇,坐落在深山老林之中,这是一片典型高原山地草甸狭长地带,也是我们今晚住宿的地方,到小镇上找了个民宿,环境一般。然后就去鲁朗林海里采蘑菇。此时已经晚上8点了,但是天还是很亮,因为跟内地有两个小时的时差。

Day6 -- 拉姆拉错

这一天我们早上7点就出发了,这边天还没亮,前往拉姆拉错,拉姆拉错是西藏三大圣湖之一,另外两个是纳木错和玛旁雍错。 因为距离比较远,这一天基本在车上度过,车上跟司机闲聊,他说拉姆拉错近两年都没啥人去,旅行团也很少有人去那的,除非是信徒啥的,因为被奉为圣湖,班禅在寻找达赖喇嘛的转世灵童时,会来到拉姆拉错前观察湖面所显现的异象来确定寻访方向,据说有缘之人可以在湖面上看到自己的前世今生。当然我觉得这是扯淡,因为我在湖前闭眼,双手在胸前合上虔诚的跪了几分钟,然后睁眼啥也没看到。下午差不多3点,到了拉姆拉错景区,这里海拔 5000 米,要想看到湖,还得爬山,从山上往下看湖。这座山爬的是真累,体验到了缺氧的感觉,爬了一个多小时,终于看到了拉姆拉错,湖距离我们还是很远的,说实话,真的很普通,通俗的说就是一个大水池子,没看出啥特别的。山上也没几个游客,这个地方不是很推荐来,这就是我前面说的规划的西藏环行路线不合理的地方之一。另外近两年这个地方是不允许把拍的照片上传到网上的,所以这里就不上传湖的图片了。不过在海拔 5000 米的高原爬山这种体验还是挺爽的。

Day7 -- 羊卓雍错

羊卓雍错俗称羊湖,在抖音上非常火,在这里我第一次看到了跟天一样蓝的湖水,真的是太美了,这里距离拉萨只有 100 多 km,如果在拉萨当天就可以来回,真的是来西藏最值得去的地方之一,我无法用语言来描述这里。只能放图片了自己感受。羊湖玩完之后,我们向日喀则,这个也是此次路线不合理的地方,因为日喀则一般是作为去珠峰的中转地,去珠峰的路上会到那住宿休息一晚,而此次行程因为时间不够没法去珠峰,所以也不应该跑去这么老远的日喀则来住宿,而去日喀则的路上也就经过了个卡若拉冰川,拍了几张照,属实没啥好看的。这就导致接下来的一天,前往纳木错基本在车上度过,因为日喀则距离纳木错有 500km 左右。我们到日喀则已经差不多晚上 9 点了,我们在日喀则市里找了个新疆的清真餐厅,吃了个新疆大盘鸡,分量是真的多。

Day8 -- 色林错

由于前面路线规划的问题,导致今天坐了十个小时的车啊,途径的景点只有色林错,它是西藏第一大湖泊及中国第二大咸水湖,并且它的面积还在不断增大。这个湖还是很好看的,湖面呈绿色,但手机感觉总拍不出眼睛看到的感觉。在色林错待了一会,就继续开始前往纳木错景区。晚上 8 点左右,到了纳木错景区外,本来预定了房车在这里过夜,但是本来说好的价格可能由于今晚来的人多了,那人临时抬高价格让我们很不爽,最后没住房车,司机联系了当地的居民,住了当地居民自己空出的一个房子里,150/人。在这里环境算是不错的了,因为据说还有那种 20 人的大通铺。就是房子的位置有点恐怖,周围的房子没一个亮灯的,漆黑一片,厕所也没有,想上厕所只能贴近大自然了,不过也已经习惯了,,因为车上一路贴近大自然也不是一两次了..... 晚上这里的星空特别漂亮,因为海拔高,在没有云的情况下可以看到满天繁星。

Day9 -- 纳木错-圣象天门

来西藏的最后一站,也是我来西藏最想去的地方,这个地方在抖音上被吹到爆了。在通往圣象天门的路上,有一段必经的30公里的搓衣板路,会颠到你怀疑人生,司机说它曾经自己开车带客走这条路,然后车门被颠坏了。之后他就不敢走这条路了,都是让游客坐景区专门的车到圣象天门,车票 100,门票 80。

圣象天门位于纳木错北岸的恰多郎卡岛上,海拔 4700 多米,当地人说,这里是纳木错念青唐古拉的婚姻契约地,这里代表永恒与圣洁。下面还是直接上图感受吧。

Day10 && Day11 西安 返程

开始返程,从拉萨出发,到西安转机回南京,在我之前想去的西安待了一天,看了西安兵马俑,完美结束。

最后附上六世达赖喇嘛同时也是西藏历史上著名的诗人仓央嘉措的一首诗,它刻在了那古拉山口的石碑上:

那一年

我磕长头匍匐在山路

不为觐见

只为贴着你的温暖

那一世

我转山转水转佛塔

不为修来生

只为途中与你相见

java反射获得泛型参数getGenericSuperclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Person<T> {

}

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Student extends Person<Student> {
public static void main(String[] args) {
Student st=new Student();
Class clazz=st.getClass();
//getSuperclass()获得该类的父类
System.out.println(clazz.getSuperclass());
//getGenericSuperclass()获得带有泛型的父类
//Type是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
Type type=clazz.getGenericSuperclass();
System.out.println(type);
//ParameterizedType参数化类型,即泛型
ParameterizedType p=(ParameterizedType)type;
//getActualTypeArguments获取参数化类型的数组,泛型可能有多个
Class c=(Class) p.getActualTypeArguments()[0];
System.out.println(c);
}
}

打印结果:

1
2
3
class com.test.Person
com.test.Person<com.test.Student>
class com.test.Student

java 动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 代理的接口
public interface ManToolsFactory {
void saleManTools(String size);
}


public class AaFactory implements ManToolsFactory {
@Override
public void saleManTools(String size) {
System.out.println("按需求定制了一个size为"+size+"的model");
}
}

// 代理类
public class MarkCompany implements InvocationHandler {

/*持有的真实对象*/
private Object factory;

public Object getFactory() {
return factory;
}

public void setFactory(Object factory) {
this.factory = factory;
}

/*通过Proxy获得动态代理对象*/
public Object getProxyInstance(){
return Proxy.newProxyInstance(factory.getClass().getClassLoader(),
factory.getClass().getInterfaces(),this);
}

/*通过动态代理对象方法进行增强*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {


Object result = method.invoke(factory, args);
return result;
}


public class Client {

public static void main(String[] args) {
/*动态代理模式---------------*/
ManToolsFactory aafactory = new AaFactory();
MarkCompany markCompany = new MarkCompany();

markCompany.setFactory(aafactory);
ManToolsFactory employee1
= (ManToolsFactory)markCompany.getProxyInstance();
employee1.saleManTools("E");
}
}

1
2


java 线程池

为什么要使用线程池

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。创建线程关闭线程花销是比较大的。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。<!-- more -->

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。 (参考 https://www.zhihu.com/question/41134816/answer/1086805195)

线程池的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class UseThreadPool {
//工作线程
static class Worker implements Runnable
{

private String taskName;
private Random r = new Random();

public Worker(String taskName){
this.taskName = taskName;
}

public String getName() {
return taskName;
}

@Override
public void run(){
System.out.println(Thread.currentThread().getName()
+" process the task : " + taskName);
SleepTools.ms(r.nextInt(100)*5);
}
}

static class CallWorker implements Callable<String>{

private String taskName;
private Random r = new Random();

public CallWorker(String taskName){
this.taskName = taskName;
}

public String getName() {
return taskName;
}

@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()
+" process the task : " + taskName);
return Thread.currentThread().getName()+":"+r.nextInt(100)*5;
}

}

public static void main(String[] args)
throws InterruptedException, ExecutionException
{

ExecutorService pool = new ThreadPoolExecutor(2,4,3,TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10),
new ThreadPoolExecutor.DiscardOldestPolicy());
for(int i=0;i<6;i++) {
Worker worker = new Worker("worker_"+i);
pool.execute(worker);
}
for(int i=0;i<6;i++) {
CallWorker callWorker = new CallWorker("callWorker_"+i);
Future<String> result = pool.submit(callWorker);
System.out.println(result.get());
}
pool.shutdown();
}
}

线程池中几个参数的含义

1
2
3
4
5
6
7
8
ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(corePoolSize,// 核心线程数  
maximumPoolSize, // 最大线程数
keepAliveTime, // 闲置线程存活时间
TimeUnit.MILLISECONDS,// 时间单位
new LinkedBlockingDeque<Runnable>(),// 线程队列
Executors.defaultThreadFactory(),// 线程工厂
new AbortPolicy()// 队列已满,而且当前线程数已经超过最大线程数时的异常处理策略
);

四种拒绝策略

CallerRunsPolicy : 当线程池和队列都满时,任务将会被任务的调用方线程执行,如果线程池关闭,那么任务将会被抛弃 AbortPolicy :当线程池和队列都满时,再有任务进来直接抛出RejectedExecutionException异常 DiscardPolicy: 当线程池和队列都满时,再有任务进来,默默的将任务抛弃 DiscardOldestPolicy: 当线程池和队列都满时,再有任务进来,抛弃最老的未处理的任务即当前队列中排在最前面的任务,然后重试该新进来的任务,如果线程池关闭,那么任务将会被抛弃

阻塞队列BlockingQueue

add(Object): 把 Object 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true, 否则招聘异常

**offer(Object): **表示如果可能的话,将 Object 加到 BlockingQueue 里, 即如果 BlockingQueue 可以容纳,则返回true,否则返回false.

put(anObject): 把 Object 加到 BlockingQueue 里, 如果 BlockQueue 没有空间,则调用此方法的线程被阻塞直到BlockingQueue里面有空间再继续.

poll(time): 取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null

**take(): **取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空, 阻塞进入等待状态直到Blocking有新的对象被加入为止

核心线程数的设置

任务类型: cpu密集型 不要超过cpu并行线程数:Runtime.getRuntime().availableProcessors()+1

io密集型 Runtime.getRuntime().availableProcessors() x2

线程进入顺序

corePoolSize ---> BlockingQueue ---> maximumPoolSize

若最大线程数也满了,再进入的线程按设置的拒绝策略执行。

java内存

Android Studio 导入classes.jar包

Android SDK 中有很多带有 @hide 注解的 api,这些API为系统隐藏的 api,上层应用无法直接调用。我们开发时如果必须使用有两种方式,一是利用java反射机制。二则是在我们项目中导入 Android 系统源码编译出的classes.jar 包,该 jar 包生成在out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar。这里我重点介绍Android Studio如何导入classes.jar.<!--more-->

1.考虑到 libs 目录下可能会有其他 jar 包,我们在app目录下建立 syslibs 文件夹,并将 classes.jar 包放入该路径,因为这里的 classes.jar 包只用于编译,无须打包进 apk 中。

2.右键工程-->【Open Module Settings】,点击左侧【Dependencies】,选择app,点击【+】选择【2 Jar Dependency】,添加jar包,如下图。

3.经过步骤2之后,Module的build.gradle中,dependencies自动增加依赖配置 如下:

implementation files('syslibs\\classes.jar')

修改为:

1
compileOnly files('syslibs\\classes.jar')

这么做是因为我们使用该jar包只用于编译,不打包进apk中,该包里包含的是Android SDK,当apk运行到手机里时,手机里有运行所需的sdk环境。

4.project 的 build.gradle添加如下代码:

1
2
3
4
5
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs.add('-Xbootclasspath/p:app\\syslibs\\classes.jar')
}
}

添加位置如下:

5.修改jar包优先级,在Moduels的app.iml文件中找到我们的classes.jar包所在行

1
<orderEntry type="library" name="Gradle: __local_aars__:D.\AndroidStudioProjects\Blue\app\syslibs\classes.jar:unspecified@jar" level="project" />

将其移动到

1
<orderEntry type="jdk" jdkName="Android API 28 Platform" jdkType="Android SDK" />

之上。

这么做是为了让项目编译时优先用我们提供的jar包里的SDK,而不是原有Android SDK。

6.经过第5步之后,就能正常使用我们jar里的SDK了,但是,每次编译之后,app.iml 中的

1
<orderEntry type="jdk" jdkName="Android API 28 Platform" jdkType="Android SDK" />

又会被移动到最上面。下次编译时我们又得将他移动到

1
<orderEntry type="library" name="Gradle: __local_aars__:D.\AndroidStudioProjects\Blue\app\syslibs\classes.jar:unspecified@jar" level="project" />

的后面,这样比较麻烦。网上提供了一种一劳永逸的方法,将下面的代码加入到Moudle的build.gradle里:

preBuild {
    doLast {
 
    def imlFile = file(project.name + ".iml")
 
    println 'Change ' + project.name + '.iml order'
 
    try {
 
        def parsedXml = (new XmlParser()).parse(imlFile)
 
        def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
 
        parsedXml.component[1].remove(jdkNode)
 
        def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform"
 
        new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK'])
 
        groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
 
    } catch (FileNotFoundException e) {
 
        // nop, iml not found
 
    }
 
   }
}

加入位置如下:

至此,classes.jar导入完成,我们可以在Android Studio里可以直接调用系统隐藏的api里,如果遇到某些系统隐藏的api在使用时报红,一般为ide为bug,可以尝试clean project,然后rebuild project恢复正常了

Android 自定义属性

本文以 ToggleButton 为例,介绍 Android 自定义属性的基本流程,代码在:https://github.com/qiracle/ToggleButton 。<!--more-->

定义 attrs.xml

在values目录下增加 attrs.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<resources>


<!-- 声名属性集的名称 -->
<declare-styleable name="MyToggleBtn">

<!-- 声名一个属性 name是my_background 类型为 引用类型 引用资源ID -->
<attr name="my_background" format="reference" />

<!-- 声名一个属性 name是my_slide_btn 类型为 引用类型 引用资源ID -->
<attr name="my_slide_btn" format="reference" />

<!-- 声名一个属性 name是curr_state 类型为 boolean 类型-->
<attr name="curr_state" format="boolean" />

</declare-styleable>


</resources>

在布局文件中引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!--命名空间 qiracle ,给下面使用-->
xmlns:qiracle="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<cn.qiracle.customattr.MyToggleButton
android:id="@+id/my_toggle_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
qiracle:my_background="@drawable/switch_background"
qiracle:my_slide_btn="@drawable/slide_button"
qiracle:curr_state="false"
testAttrs="hello"
/>

</RelativeLayout>

在代码里设置属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 在布局文件中声名的view,创建时由系统自动调用。
* @param context 上下文对象
* @param attrs 属性集
*/

public MyToggleButton(Context context, AttributeSet attrs) {
super(context, attrs);


//无命名空间测试

String testAttrs = attrs.getAttributeValue(null, "testAttrs");

System.out.println("testAttrs===:"+testAttrs);

/*
* AttributeSet 对XML文件解析后的结果,封装为 AttributeSet 对象。
* 存储的都是原始数据。仅对数据进行简单加工。
*/


int count = attrs.getAttributeCount();
// 打印MyToggleButton所有属性 ,也就是上面布局文件里的9个
for (int i = 0; i < count; i++) {
String name = attrs.getAttributeName(i);
String value = attrs.getAttributeValue(i);
// System.out.println("name : "+name+" value : "+value);
}


//获得自定义的属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn);

int N = ta.getIndexCount();
for (int i = 0; i < N; i++) {
/*
* 获得某个属性的ID值
*/

int itemId = ta.getIndex(i);
switch (itemId) {
case R.styleable.MyToggleBtn_curr_state:
currState = ta.getBoolean(itemId, false);

break;
case R.styleable.MyToggleBtn_my_background:
backgroundId = ta.getResourceId(itemId, -1);
if(backgroundId == -1){
throw new RuntimeException("请设置背景图片");
}
backgroundBitmap = BitmapFactory.decodeResource(getResources(), backgroundId);

break;
case R.styleable.MyToggleBtn_my_slide_btn:
slideBtnId = ta.getResourceId(itemId, -1);
slideBtn = BitmapFactory.decodeResource(getResources(), slideBtnId);

break;

default:
break;
}

}
ta.recycle(); //回收ta,不回收as会提示

initView();
}

Android 性能优化之布局优化

在Android应用开发中,常用的布局方式主要有LinearLayout、RelativeLayout、FrameLayout等,通过这些布局可以实现各种各样的界面。我们需要知道如何高效地使用这些布局方式来组织UI控件,布局的好坏影响到绘制的时间,本节将通过减少Layout层级减少测量绘制时间提高复用性三个方面来优化布局,优化的目的就是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性节省开发和维护成本。 <!--more-->

减少层级

层级越少,测试和绘制的时间就越短,通常减少层级有以下两个常用方案: 合理使用 RelativeLayout 和 LinearLayout。 合理使用Merge。

(1)RelativeLayout与LinearLayout

使用 RelativeLayout 减少层级:

!

通过Hierarchy View来查看下层级情况:

!

可以看到一共有7级,使用RelativeLayout进行优化,达到相同的布局效果,并且RelativeLayout允许子元素指定它们相对于其他元素或父元素的位置,有最大自由度的布局属性,而且布局层次最浅,占用内存最少。

!

这样就可以减少两个层级,用一个RelativeLayout就可以达到显示的效果,再使用Hierarchy View来查看层级,可以看到减少到5层。

但ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中子View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向互相依赖,就需要横向、纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高,在布局上RelativeLayout不如LinearLayout快

但是如果布局本身层次太深,还是推荐用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但在体验上影响不会特别大,如果优化掉两层仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。

根据以上分析,可以总结出以下几点布局原则: 尽量使用RelativeLayout和LinearLayout。 在布局层级相同的情况下,使用LinearLayout。 用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。

注意 由于Android的碎片化程度很高,市面上的屏幕尺寸也是各式各样,使用RelativeLayout能使构建的布局适应性更强,构建出来的UI布局对多屏幕的适配效果更好,通过指定UI控件间的相对位置,使不同屏幕上布局的表现基本保持一致。当然,也不是所有情况下都得使用相对布局,根据具体情况选择和搭配使用其他布局方式来实现最优布局。

(2)Merge的使用

从名字上就可以看出,Merge就是合并的意思。使用它可以有效优化某些符合条件的多余的层级。使用Merge的场合主要有以下两处:

在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout。 在Activity中整体布局,根元素需要是FrameLayout。

我们仍以前面的布局为例,在页面增加一个自定义控件TopBar:

其中TopBar的XML布局如下:

显示结果下图所示。这种布局在一些列表的Item中非常常见,而且列表中Item本身的层级比较深,因此优化显得更有意义。

我们使用HierarchyView查看增加TopBar后的布局层级,如图。可以看到,就是这么简单的一个布局,却把层级增加了两级,从图中很明显地看出TopBar后一层的LinearLayout是多余的,这时可以使用Merge把这一层消除。

使用Merge来优化布局,使用Merge标签替换LinearLayout后,原来的LinearLayout属性也没有用了,修改后的代码如下

运行后再使用Hierarchy View查看当前层级

这样就把多余的LinearLayout消除了,原理是在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中,这样就保证了不会引入额外的层级。

注意 如果Merge代替的布局元素为LinearLayout,在自定义布局代码中将LinearLayout的属性添加到引用上,如垂直或水平布局、背景色等。

但Merge不是所有地方都可以任意使用,有以下几点要求:

Merge只能用在布局XML文件的根元素。

使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inf late(int, ViewGroup, boolean))。

不能在ViewStub中使用Merge标签。原因就是ViewStub的inf late方法中根本没有attachToRoot的设置。

这一节讲了如何减少层级,那么在Android系统中,多少层才是合理的呢?当然是越少越好,但从Lint检查的配置上看,超过10层才会报警,实际上在开发时,随着产品设计的丰富和多样性,很容易超过10层,根据实际开发过程中超过15层就要重视并准备做优化,20层就必须修改了。在实在没有办法优化的情况下,需要把复杂的层级用自绘控件来实现,自绘控件中的图层层级再多,在布局上也只是一层,但这样也会带来过度绘制的问题,后面会讲。

注意 在Activiy的总布局中使用Merge,但又想设置整体的属性(布局方式或背景色),可以不使用setContentView方法加载Layout,而使用(id/content)将FrameLayout取出来,在代码中手动加载布局,但如果层级压力不大(小于10级),则没有必要,因为这样代码的维护性较差。

提高显示速度

我们在开发的过程中会碰到这样的场景或者显示逻辑:某个布局当中的子布局非常多,但并不是所有元素都同时显示出来,而是二选一或者N选一,打开这个界面根据不同的场景和属性显示不同的Layout。例如:一个页面对不同的用户(未登录、普通用户、会员)来说,显示的布局不同。或者,有些用户喜欢对不同的元素使用INVISIBLE或者GONE隐藏,通过设计元素的visable属性来控制,这样虽然达到了隐藏的目的,但效率非常低,原因是即使将元素隐藏,它们仍在布局中,仍会测试和解析这些布局。Android提供了ViewStub控件来解决这个场景。

ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inf late()时,ViewStub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。这样,就可以使用ViewStub来设置是否显示某个布局。

下面的代码是两个ViewStub通过不同的初始化来加载两个不同的布局,以满足用户的需求。

在调用时,根据需求切换不同的Layout,这样可以提高页面初始化的速度,使用代码如下:

ViewStub显示有两种方式,上面代码使用的是inf ate方法,也可以直接使用ViewStub.setVisibiltity(View.Visible)方法。 使用ViewStub时需要注意以下几点:

  1. ViewStub只能加载一次,之后ViewStub对象会被置为空。换句话说,某个被ViewStub指定的布局被加载后,就不能再通过ViewStub来控制它了。所以它不适用于需要按需显示隐藏的情况。

  2. ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的View,还是使用visibility属性。

  3. VIewStub中不能嵌套Merge标签。

不过这些限制都无伤大雅,我们还是能够用ViewStub来做很多事情,ViewStub的主要使用场景如下: 在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。 想要控制显示与隐藏的是一个布局文件,而非某个View。

注意 因为ViewStub只能Inf late一次之后会被置空无法继续使用ViewStub来控制布局。所以当需要在运行时不止一次显示和隐藏某个布局时,使用ViewStub是无法实现的。这时只能使用View的可见性来控制。

布局复用

我们在开发应用时还会碰到另一个常见的场景,就是一个相同的布局在很多页面(Activity或Fragment)会用到,如果给这些页面的布局文件都统一加上相同的布局代码,维护起来就很麻烦,可读性也差,一旦需要修改,很容易有漏掉的地方,Android的布局复用可以通过<include>标签来实现,就像提取代码公用部分一样,在编写Android布局文件时,也可以将相同的部分提取出来,在使用时,用<include>添加进去。

例如,在大部分应用中,基本上所有的应用都会带有头部栏(TopBar),主要是显示标题和返回键功能,这样只需要维护一份代码,就可以修改所有的显示效果。

提示 类似于TopBar的这类常用控件,包括菜单,可以把具体实现抽象到页面的基类(BaseActivity)中,这样布局和具体的实现都收归到一个地方,方便维护。 提高布局效率的方法总体来说就是减少层级,提高绘制速度和布局复用。影响布局效率主要有以下几点:

布局的层级越少,加载速度越快。 减少同一层级控件的数量,加载速度会变快。 一个控件的属性越少,解析越快。

根据本节的分析,对优化的总结如下: 尽量多使用RelativeLayout或LinearLayout,不要使用绝对布局AbsoluteLayout。 将可复用的组件抽取出来并通过< include />标签使用。 使用< ViewStub />标签加载一些不常用的布局。 使用< merge />标签减少布局的嵌套层次。 尽可能少用wrap_content, wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。 删除控件中的无用属性。

避免过度绘制

过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。

当设计上追求更华丽的视觉效果时,我们很容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳性能,必须尽量减少Overdraw情况发生。

我们一般在XML布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是: XML布局->控件有重叠且都有设置背景 View自绘-> View.OnDraw里面同一个区域被绘制多次

如何查看是否有过度绘制:

在手机的“设置”→“开发者选项”中打开“显示GPU过度重绘”开关(注:对未默认开启硬件加速的界面需要同时打开“强制进行GPU渲染”开关)。

打开后可以根据不同的颜色观察UI上的Overdraw情况,蓝色、淡绿、淡红、深红代表4种不同程度的Overdraw情况,不同颜色的含义如下: 无色:没有过度绘制,每个像素绘制了1次。 蓝色:每个像素多绘制了1次。大片的蓝色还是可以接受的。如果整个窗口是蓝色的,可以尝试优化减少一次绘制。 绿色:每个像素多绘制了2次。 淡红:每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。 深红:每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。 我们的目标是尽量减少红色Overdraw,看到更多的蓝色区域。

如何避免过度绘制

1.布局上的优化

在XML布局上,如果出现了过度绘制的情况,可以使用Hierarchy View来查看具体的层级情况,可以通过XML布局优化来减少层级。需要注意的是,在使用XML文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点: 移除XML中非必需的背景,或根据条件设置。

移除Window默认的背景。

按需显示占位背景图片。

使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除,代码如下:

注意 针对 ListView 中的 Avatar ImageView 的设置,在 getView 的代码中,判断是否获取对应的 Bitmap,获取 Avatar的图像之后,把 ImageView 的 Background 设置为 Transparent,只有当图像没有获取到时,才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。

2.自定义View优化

事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义View来实现,虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义View(通常重写了onDraw方法), Android系统无法检测在onDraw中具体会执行什么操作,无法监控并自动优化,也就无法避免Overdraw了。

但是在自定义View中可以通过**canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用canvas.quickreject()**来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义View避免OverDraw的案例。

快速判断Canvas是否需要绘制:Canvas.QuickReject:

在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。

避免绘制越界:Canvas.ClipRect:

每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op.INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。这个API可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

本文参考:罗彧成的《Android应用性能优化最佳实践》一书。强烈推荐此书!

读《深入理解Java虚拟机》总结<三.java中的新生代和老年代内存>

概念

新生代:新生代中98%的对象都是朝生夕死 (1个eden 区(80%) 和 2个survivor区(10%))。 老年代:存活对象存留的时间比较久。

MinorGC:发生在新生代的垃圾回收。很频繁,速度快。 FullGC:发生在老年代的垃圾回收。发生FullGC通常会伴随一次MinorGC(并非绝对),FullGC会比MinorGc慢10倍以上。<!--more-->

内存分配与回收策略:

1.对象优先在Eden区进行分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC

2.对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾回收以获取足够的连续空间来安置他们。

3.长期存活的对象将进入老年代

虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到一定程度(默认15)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过设置参数-XX:MaxTenuringThreshold。

4 动态对象年龄判定

为了更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

5 空间分配担保

在发生MinorGC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败:如果允许,那只会进行MinorGC;如果不允许,则也要改为进行一次FullGC。取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次MinorGC存活后的对象突增,远远高于平均值得话,依然会导致担保失败。如果出现了担保失败,那只好在失败后重新发起FullGC。虽然担保失败时绕的圈子是最大的,但大部分情况下还是会将HandlePromotionFailure 开关打开,避免Full GC过于频繁。

Android layout_weight 计算方式

假设:LinearLayout为android:orientation="horizontal", layout_weight属性值分别为1、2、2

第一种情况:每个控件的宽度属性都为android:layout_width="0dp",那么额外的空为(手机的宽度假设为X)X-0-0-0=X,那么根据sdk上所述:<!--more--> 第一个控件的宽度为 0+(1/(1+2+2))*X=X/5 第二个控件的宽度为 0+(2/(1+2+2))*X=2X/5 第三个控件的宽度为 0+(2/(1+2+2))*X=2X/5

第二种情况:每个控件的宽度属性都为android:layout_width="match_parent",那么额外的空间就是X-X-X-X=-2X 第一个控件的宽度为 X+(1/(1+2+2))(-2X)=3X/5 第二个控件的宽度为 X+(2/(1+2+2))(-2X)=X/5 第三个控件的宽度为 X+(2/(1+2+2))*(-2X)=X/5

第三种情况:其中第一个控件宽度属性为android:layout_width="match_parent",其他两个控件宽度为android:layout_width="0dp",额外的空间为X-X-0-0=0 第一个控件的宽度为 X+(1/(1+2+2))*0=X 第二个控件的宽度为 0+(2/(1+2+2))*0=0 第三个控件的宽度为 0+(2/(1+2+2))*0=0

最终结论:layout_weight的公式 控件的宽度/高度 = 控件的width/height值+(该控件的weight值/所有控件的weight的和)×额外的空间 额外的空间=手机的宽度/高度-所有控件的宽度/高度

注:如果属性为android:layout_width="wrap_content" 则计算时,总宽/高要减去控件本身占据的宽/高

转自:https://blog.csdn.net/Dazlly/article/details/13767343

Android屏幕适配 px,dp,dpi及density的关系与深入理解

PX(pixel):

即传统计算机语言中描述的像素,在Android则代表绝对像素。

之所以Android中不推荐使用这种单位,正是因为不同生产厂商,不同品牌,不同屏幕的设备,其分辨率亦不一。

举例来说,我们现在将某个Button的width设为160px,则会出现如下情况:

在分辨率为“320宽”的设备里,该按钮显示占屏幕宽度一半;

在分辨率为“640宽”的设备里,该按钮显示占屏幕宽度的四分之一;

<!--more-->

DPI(Dots Per Inch):

为了避免上面说到的使用px在屏幕适配中带来的问题,Android引入了一个新的单位:dp/dip。

而在理解“dp”之前,我们更有必要先了解一下另一个概念。正是:dpi。

也有人讲dpi称为“屏幕密度”。其含义则是:每英寸所打印的点数,既每一英寸的屏幕所包含的像素数。

举例来说,假设现在有一台“宽2英寸,长3英寸”的设备,则:

当该设备分辨率为“320x480”,则dpi值为160。 当该设备分辨率为“640x960”,则dpi值为320。 而“dpi”值越高的设备,其屏幕显示画面的效果也就越精细。

使用场景:

正是因为dpi值其代表的特性,所以android项目的资源文件下存在以下目录:

drawable-ldpi ( 当dpi为120时,使用此目录下的资源) drawable-mdpi ( 当dpi为160时,使用此目录下的资源) drawable-hdpi ( 当dpi为240时,使用此目录下的资源) drawable-xhdpi ( 当dpi为320时,使用此目录下的资源) drawable-xxhdpi ( 当dpi为480时,使用此目录下的资源) Android正是根据设备DPI值得不同,选择清晰度不同的资源使用,完成屏幕的适配。

DP/DIP(device independent pixels):

与我们之前谈到的绝对密度“px”对应,Android中引入的“dp”代表的则是“设备独立像素”。

该单位是为支持WVGA、HVGA和QVGA而使用的,其不再依赖像素本身,而是和屏幕密度相关。

在Android当中规定:在屏幕密度为“160dpi”的情况下,则刚好“1dp = 1px”。

注:当屏幕密度为“320dpi”时,则“1dp = 2px”,以此类推.......

也正是因此,让我们得以保证了:控件在不同密度的屏幕上显示一致,既完成屏幕适配。

使用场景:

让我们回到上面说到的使用px造成的控件显示问题,此时我们将使用新的单位“dp”。于是:

在分辨率320x480(既dpi为160)的设备下,则160dp等价于160px,按钮占屏幕宽的一半。 在分辨率640x960(既dpi为320)的设备下,则160dp等价于320px,按钮依然占屏幕宽的一半。

Density:

就这个单词本身直接翻译的意思而言,其也代表“密度”。但需要注意的是,在Android中,其实并非如此。 注意我们这里指的是,通过代码“context.getResources().getDisplayMetrics().density”获取的“density”值。 而通过该方法获取到的该值,实际上是等价于“dpi / 160”的一个结果值。也就是说: “getResources().getDisplayMetrics().density” = “getResources().getDisplayMetrics().densityDpi / 160”

看到这样一个解析,聪明的人大概已经能预见什么了。我们似乎发现了某种关联: 在Android里:“dpi = 160,则1dp = 1px”、“dpi = 320,则1dp = 2px”。以此类推。 到此你已经发现,dp,px与160之间存在着某种规律:“1dp = (dpi / 160)px” 换算一下,最终得到公式: dp = density * px

到了这里我们明白了,其实Android提供的该值,也就是为了让我们在dp与px之间做转换。 归根结底,其目的还是为了帮助我们做屏幕适配。

使用场景: 虽然使用dp在xml文件中定义控件尺寸,能够很好的帮助我们完成适配。 但很多时候,我们也会需要在Java代码中动态的去设定控件的尺寸。

但由于在代码中的尺寸设定,基本都被默认为了px单位。 所以这个时候就可以借助“density”来帮我们完成dp与px的转换,从而完成适配。

1
2
3
4
public static int dip2px(Context context, float dipValue){ 
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(dipValue * scale + 0.5f);
}

1
2
3
4
public static int px2dip(Context context, float pxValue){ 
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(pxValue / scale + 0.5f);
}

原文链接:https://blog.csdn.net/ghost_Programmer/article/details/50042805

|